mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-30 06:12:06 -07:00
fix(ci): restore coverage lane compatibility
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
id: TASK-242
|
||||||
|
title: Fix stats server Bun fallback in coverage lane
|
||||||
|
status: In Progress
|
||||||
|
assignee: []
|
||||||
|
created_date: '2026-03-29 07:31'
|
||||||
|
labels:
|
||||||
|
- ci
|
||||||
|
- bug
|
||||||
|
milestone: cleanup
|
||||||
|
dependencies: []
|
||||||
|
references:
|
||||||
|
- 'PR #36'
|
||||||
|
priority: high
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
Coverage CI fails when `startStatsServer` reaches the Bun server seam under the maintained source lane. Add a runtime fallback that works when `Bun.serve` is unavailable and keep the stats-server startup path testable.
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
<!-- AC:BEGIN -->
|
||||||
|
- [ ] #1 `bun run test:coverage:src` passes in GitHub CI
|
||||||
|
- [ ] #2 `startStatsServer` uses `Bun.serve` when present and a Node server fallback otherwise
|
||||||
|
- [ ] #3 Regression coverage exists for the fallback startup path
|
||||||
|
<!-- AC:END -->
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
import { serve } from '@hono/node-server';
|
|
||||||
import type { ImmersionTrackerService } from './immersion-tracker-service.js';
|
import type { ImmersionTrackerService } from './immersion-tracker-service.js';
|
||||||
|
import http, { type IncomingMessage, type ServerResponse } from 'node:http';
|
||||||
import { basename, extname, resolve, sep } from 'node:path';
|
import { basename, extname, resolve, sep } from 'node:path';
|
||||||
import { readFileSync, existsSync, statSync } from 'node:fs';
|
import { readFileSync, existsSync, statSync } from 'node:fs';
|
||||||
|
import { Readable } from 'node:stream';
|
||||||
import { MediaGenerator } from '../../media-generator.js';
|
import { MediaGenerator } from '../../media-generator.js';
|
||||||
import { AnkiConnectClient } from '../../anki-connect.js';
|
import { AnkiConnectClient } from '../../anki-connect.js';
|
||||||
import type { AnkiConnectConfig } from '../../types.js';
|
import type { AnkiConnectConfig } from '../../types.js';
|
||||||
@@ -60,6 +61,71 @@ function resolveStatsNoteFieldName(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toFetchHeaders(headers: IncomingMessage['headers']): Headers {
|
||||||
|
const fetchHeaders = new Headers();
|
||||||
|
for (const [name, value] of Object.entries(headers)) {
|
||||||
|
if (value === undefined) continue;
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
for (const entry of value) {
|
||||||
|
fetchHeaders.append(name, entry);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
fetchHeaders.set(name, value);
|
||||||
|
}
|
||||||
|
return fetchHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toFetchRequest(req: IncomingMessage): Request {
|
||||||
|
const method = req.method ?? 'GET';
|
||||||
|
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? '127.0.0.1'}`);
|
||||||
|
const init: RequestInit & { duplex?: 'half' } = {
|
||||||
|
method,
|
||||||
|
headers: toFetchHeaders(req.headers),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (method !== 'GET' && method !== 'HEAD') {
|
||||||
|
init.body = Readable.toWeb(req) as BodyInit;
|
||||||
|
init.duplex = 'half';
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Request(url, init);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeFetchResponse(res: ServerResponse, response: Response): Promise<void> {
|
||||||
|
res.statusCode = response.status;
|
||||||
|
response.headers.forEach((value, key) => {
|
||||||
|
res.setHeader(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const body = await response.arrayBuffer();
|
||||||
|
res.end(Buffer.from(body));
|
||||||
|
}
|
||||||
|
|
||||||
|
function startNodeHttpServer(
|
||||||
|
app: Hono,
|
||||||
|
config: StatsServerConfig,
|
||||||
|
): { close: () => void } {
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
await writeFetchResponse(res, await app.fetch(toFetchRequest(req)));
|
||||||
|
} catch {
|
||||||
|
res.statusCode = 500;
|
||||||
|
res.end('Internal Server Error');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(config.port, '127.0.0.1');
|
||||||
|
|
||||||
|
return {
|
||||||
|
close: () => {
|
||||||
|
server.close();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** Load known words cache from disk into a Set. Returns null if unavailable. */
|
/** Load known words cache from disk into a Set. Returns null if unavailable. */
|
||||||
function loadKnownWordsSet(cachePath: string | undefined): Set<string> | null {
|
function loadKnownWordsSet(cachePath: string | undefined): Set<string> | null {
|
||||||
if (!cachePath || !existsSync(cachePath)) return null;
|
if (!cachePath || !existsSync(cachePath)) return null;
|
||||||
@@ -1017,25 +1083,19 @@ export function startStatsServer(config: StatsServerConfig): { close: () => void
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const server = bunRuntime.Bun?.serve
|
if (bunRuntime.Bun?.serve) {
|
||||||
? bunRuntime.Bun.serve({
|
const server = bunRuntime.Bun.serve({
|
||||||
fetch: app.fetch,
|
fetch: app.fetch,
|
||||||
port: config.port,
|
port: config.port,
|
||||||
hostname: '127.0.0.1',
|
hostname: '127.0.0.1',
|
||||||
})
|
});
|
||||||
: serve({
|
|
||||||
fetch: app.fetch,
|
|
||||||
port: config.port,
|
|
||||||
hostname: '127.0.0.1',
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
close: () => {
|
close: () => {
|
||||||
if ('stop' in server) {
|
|
||||||
server.stop();
|
server.stop();
|
||||||
} else {
|
},
|
||||||
server.close();
|
};
|
||||||
}
|
}
|
||||||
},
|
|
||||||
};
|
return startNodeHttpServer(app, config);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ function createSetupWindowHandler<TWindow>(
|
|||||||
title: config.title,
|
title: config.title,
|
||||||
show: true,
|
show: true,
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
resizable: config.resizable,
|
...(config.resizable === undefined ? {} : { resizable: config.resizable }),
|
||||||
minimizable: config.minimizable,
|
...(config.minimizable === undefined ? {} : { minimizable: config.minimizable }),
|
||||||
maximizable: config.maximizable,
|
...(config.maximizable === undefined ? {} : { maximizable: config.maximizable }),
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user