c64bridge 0.8.1 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/prompts/steer.prompt.md +55 -0
- package/AGENTS.md +9 -7
- package/README.md +192 -154
- package/data/embeddings_asm.json +1 -1
- package/data/embeddings_basic.json +1 -1
- package/data/embeddings_hardware.json +1 -1
- package/data/embeddings_mixed.json +1 -1
- package/data/embeddings_other.json +1 -1
- package/data/graphics/petscii-style-guide.md +4 -4
- package/data/graphics/sprite-charset-best-practices.md +9 -9
- package/data/vice/vice-binary-monitor-spec.md +938 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/c64Client.js +76 -2
- package/dist/device.js +129 -13
- package/dist/mcp-server.js +1 -1
- package/dist/prompts/registry.js +27 -25
- package/dist/rag/indexer.js +18 -8
- package/dist/rag/knowledgeIndex.js +112 -79
- package/dist/rag/resourceUris.js +93 -0
- package/dist/sidwaveCompiler.js +1 -1
- package/dist/{assemblyConverter.js → tools/assember.js} +10 -1
- package/dist/tools/audio.js +11 -11
- package/dist/{basicConverter.js → tools/basicTokenizer.js} +1 -1
- package/dist/tools/debug.js +159 -6
- package/dist/tools/developer.js +13 -13
- package/dist/tools/disassembler.js +478 -0
- package/dist/tools/graphics.js +10 -10
- package/dist/tools/input.js +338 -0
- package/dist/tools/machineControl.js +5 -5
- package/dist/tools/memory.js +336 -12
- package/dist/tools/meta/index.js +1 -1
- package/dist/tools/printer.js +11 -12
- package/dist/tools/programRunners.js +61 -22
- package/dist/tools/rag.js +9 -9
- package/dist/tools/registry/batch.js +167 -0
- package/dist/tools/registry/config.js +1 -1
- package/dist/tools/registry/disk.js +1 -1
- package/dist/tools/registry/drive.js +1 -1
- package/dist/tools/registry/extract.js +1 -1
- package/dist/tools/registry/graphics.js +102 -2
- package/dist/tools/registry/index.js +4 -0
- package/dist/tools/registry/input.js +2 -0
- package/dist/tools/registry/memory.js +62 -2
- package/dist/tools/registry/platform.js +1 -1
- package/dist/tools/registry/printer.js +1 -1
- package/dist/tools/registry/program.js +1 -1
- package/dist/tools/registry/rag.js +1 -1
- package/dist/tools/registry/sound.js +1 -1
- package/dist/tools/registry/stream.js +1 -1
- package/dist/tools/registry/system.js +1 -1
- package/dist/tools/schema.js +4 -5
- package/dist/tools/storage.js +15 -15
- package/dist/tools/streaming.js +3 -3
- package/dist/tools/symbolRegistry.js +49 -0
- package/dist/tools/types.js +56 -4
- package/dist/tools/vice.js +2 -5
- package/dist/vice/process.js +120 -2
- package/dist/vice/readiness.js +35 -0
- package/dist/vice/smokeOptions.js +44 -0
- package/dist/vice/viceClient.js +5 -1
- package/dist/viceRunner.js +2 -0
- package/doc/plans/prompts/README.md +7 -7
- package/mcp/prompts.json +42 -59
- package/mcp/protocol-examples.json +2 -2
- package/mcp/resources.json +179 -166
- package/mcp/schemas/c64_batch.schema.json +53 -0
- package/mcp/schemas/c64_config.schema.json +525 -283
- package/mcp/schemas/c64_debug.schema.json +763 -304
- package/mcp/schemas/c64_disk.schema.json +499 -243
- package/mcp/schemas/c64_drive.schema.json +165 -90
- package/mcp/schemas/c64_extract.schema.json +533 -274
- package/mcp/schemas/c64_graphics.schema.json +607 -295
- package/mcp/schemas/c64_input.schema.json +136 -0
- package/mcp/schemas/c64_memory.schema.json +637 -128
- package/mcp/schemas/c64_printer.schema.json +393 -196
- package/mcp/schemas/c64_program.schema.json +699 -340
- package/mcp/schemas/c64_rag.schema.json +100 -54
- package/mcp/schemas/c64_select_backend.schema.json +49 -23
- package/mcp/schemas/c64_sound.schema.json +1092 -577
- package/mcp/schemas/c64_stream.schema.json +89 -47
- package/mcp/schemas/c64_system.schema.json +351 -185
- package/mcp/schemas/c64_vice.schema.json +85 -47
- package/mcp/server.json +1 -1
- package/mcp/tools.json +7299 -3346
- package/mcp.json +3 -3
- package/package.json +17 -14
- package/scripts/fetch-external-sources.mjs +1 -2
- package/scripts/run-coverage.mjs +33 -1
- package/scripts/run-tests.mjs +2 -2
- package/scripts/run-tests.ts +275 -52
- package/scripts/update-readme.ts +132 -3
- package/server.json +2 -2
package/mcp.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c64bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Stdio-based Model Context Protocol (MCP) server that bridges Commodore 64 Ultimate hardware with AI agents.",
|
|
5
5
|
"entry": "dist/index.js",
|
|
6
6
|
"type": "stdio",
|
|
@@ -113,11 +113,11 @@
|
|
|
113
113
|
"default": "120000000"
|
|
114
114
|
},
|
|
115
115
|
"VICE_BINARY": {
|
|
116
|
-
"description": "VICE binary to launch for managed emulator sessions and audio capture",
|
|
116
|
+
"description": "VICE binary to launch for managed emulator sessions and audio capture; automatic search is used only when this override is missing or invalid",
|
|
117
117
|
"default": "x64sc"
|
|
118
118
|
},
|
|
119
119
|
"VICE_DIRECTORY": {
|
|
120
|
-
"description": "Override the VICE resource directory used for ROM and UI asset discovery",
|
|
120
|
+
"description": "Override the VICE resource directory used for ROM and UI asset discovery; automatic search is used only when this override is missing or invalid",
|
|
121
121
|
"default": "auto-detect"
|
|
122
122
|
},
|
|
123
123
|
"VICE_HOST": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c64bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"mcpName": "io.github.chrisgleissner/c64bridge",
|
|
5
5
|
"description": "C64 Bridge MCP server for Commodore 64 Ultimate hardware and VICE emulator control",
|
|
6
6
|
"type": "module",
|
|
@@ -22,9 +22,11 @@
|
|
|
22
22
|
"lint": "npm run check",
|
|
23
23
|
"test": "node scripts/invoke-bun.mjs scripts/run-tests.ts",
|
|
24
24
|
"test:node": "node scripts/run-tests.mjs",
|
|
25
|
+
"test:vice:mock": "npm run test -- --platform=vice --target=mock",
|
|
26
|
+
"test:vice:device": "FORCE_XVFB=1 VICE_VISIBLE=false VICE_WARP=true npm run test -- --platform=vice --target=device",
|
|
25
27
|
"coverage": "node scripts/run-coverage.mjs",
|
|
26
28
|
"coverage:single": "node scripts/invoke-bun.mjs scripts/run-tests.ts --coverage",
|
|
27
|
-
"test:matrix": "npm run test -- --platform=c64u --target=mock && npm run test
|
|
29
|
+
"test:matrix": "npm run test -- --platform=c64u --target=mock && npm run test:vice:mock && npm run test:vice:device",
|
|
28
30
|
"mcp:generate": "node scripts/invoke-bun.mjs scripts/generate-mcp-interface.ts",
|
|
29
31
|
"check:package": "node scripts/check-package.mjs",
|
|
30
32
|
"check:run-local": "bash scripts/run-mcp-check.sh local 5",
|
|
@@ -40,7 +42,8 @@
|
|
|
40
42
|
"changelog:generate": "node scripts/generate-changelog.mjs",
|
|
41
43
|
"release:prepare": "node scripts/prepare-release.mjs",
|
|
42
44
|
"compare:rest": "node scripts/compare-rest-apis.mjs",
|
|
43
|
-
"vice:smoke": "node scripts/invoke-bun.mjs test/vice/viceSmokeTest.ts"
|
|
45
|
+
"vice:smoke": "node scripts/invoke-bun.mjs test/vice/viceSmokeTest.ts",
|
|
46
|
+
"vice:smoke:visible": "node scripts/invoke-bun.mjs test/vice/viceSmokeTest.ts --visible-demo"
|
|
44
47
|
},
|
|
45
48
|
"keywords": [
|
|
46
49
|
"mcp",
|
|
@@ -59,24 +62,24 @@
|
|
|
59
62
|
"node": ">=24"
|
|
60
63
|
},
|
|
61
64
|
"dependencies": {
|
|
62
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
63
|
-
"axios": "^1.
|
|
65
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
66
|
+
"axios": "^1.16.0",
|
|
64
67
|
"date-fns": "^4.1.0",
|
|
65
|
-
"fastify": "^5.
|
|
66
|
-
"jimp": "^1.6.
|
|
67
|
-
"meyda": "^5.6.
|
|
68
|
-
"octokit": "^5.0.
|
|
69
|
-
"pitchfinder": "^2.
|
|
68
|
+
"fastify": "^5.8.5",
|
|
69
|
+
"jimp": "^1.6.1",
|
|
70
|
+
"meyda": "^5.6.3",
|
|
71
|
+
"octokit": "^5.0.5",
|
|
72
|
+
"pitchfinder": "^2.3.4",
|
|
70
73
|
"reflect-metadata": "^0.2.2",
|
|
71
|
-
"yaml": "^2.
|
|
74
|
+
"yaml": "^2.8.4"
|
|
72
75
|
},
|
|
73
76
|
"optionalDependencies": {
|
|
74
77
|
"naudiodon": "^2.3.6"
|
|
75
78
|
},
|
|
76
79
|
"devDependencies": {
|
|
77
|
-
"@types/node": "^
|
|
78
|
-
"
|
|
79
|
-
"typescript": "^
|
|
80
|
+
"@types/node": "^25.6.0",
|
|
81
|
+
"tsx": "^4.21.0",
|
|
82
|
+
"typescript": "^6.0.3"
|
|
80
83
|
},
|
|
81
84
|
"files": [
|
|
82
85
|
"mcp.json",
|
|
@@ -7,8 +7,7 @@ import { register } from 'node:module';
|
|
|
7
7
|
async function main() {
|
|
8
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
const projectRoot = path.resolve(__dirname, '..');
|
|
10
|
-
|
|
11
|
-
register(tsNodeLoader, pathToFileURL(projectRoot));
|
|
10
|
+
register("tsx/esm", pathToFileURL(projectRoot));
|
|
12
11
|
const { fetchFromCsv } = await import('../src/rag/externalFetcher.ts');
|
|
13
12
|
// Resolve sources.csv from env, dist/rag, or src/rag (in that order)
|
|
14
13
|
let csvPath = process.env.RAG_SOURCES_CSV;
|
package/scripts/run-coverage.mjs
CHANGED
|
@@ -13,6 +13,24 @@ const runner = path.join(repoRoot, "scripts", "invoke-bun.mjs");
|
|
|
13
13
|
const configPath = path.join(repoRoot, ".c8rc.json");
|
|
14
14
|
const testRoot = path.join(repoRoot, "test");
|
|
15
15
|
const DEFAULT_COVERAGE_SHARD_SIZE = 12;
|
|
16
|
+
const DEFAULT_VICE_MOCK_TEST_FILES = [
|
|
17
|
+
"test/device.test.mjs",
|
|
18
|
+
"test/viceIntegration.test.mjs",
|
|
19
|
+
"test/viceModule.test.mjs",
|
|
20
|
+
"test/groupedToolsShims.test.mjs",
|
|
21
|
+
"test/inputModule.test.mjs",
|
|
22
|
+
"test/batchAndDisplayState.test.mjs",
|
|
23
|
+
"test/toolsTypes.test.mjs",
|
|
24
|
+
"test/platformRegistry.test.mjs",
|
|
25
|
+
"test/meta/program.test.mjs",
|
|
26
|
+
"test/mcpServerIntegration.test.mjs",
|
|
27
|
+
"test/c64Client.test.mjs",
|
|
28
|
+
"test/vice/viceSmokeTest.ts",
|
|
29
|
+
];
|
|
30
|
+
const DEFAULT_VICE_DEVICE_TEST_FILES = [
|
|
31
|
+
"test/device.test.mjs",
|
|
32
|
+
"test/vice/viceSmokeTest.ts",
|
|
33
|
+
];
|
|
16
34
|
|
|
17
35
|
const legs = [
|
|
18
36
|
{ name: "c64u-mock", args: ["--platform=c64u", "--target=mock"] },
|
|
@@ -24,6 +42,11 @@ const supplementalTests = [
|
|
|
24
42
|
"test/toolsCoverage.test.mjs",
|
|
25
43
|
"test/audioModule.test.mjs",
|
|
26
44
|
"test/updateReadme.test.mjs",
|
|
45
|
+
"test/toolsSchema.test.mjs",
|
|
46
|
+
"test/symbolRegistry.test.mjs",
|
|
47
|
+
"test/programRunnersModule.test.mjs",
|
|
48
|
+
"test/batchAndDisplayState.test.mjs",
|
|
49
|
+
"test/inputModule.test.mjs",
|
|
27
50
|
"test/viceIntegration.test.mjs",
|
|
28
51
|
];
|
|
29
52
|
|
|
@@ -50,11 +73,20 @@ export async function main() {
|
|
|
50
73
|
const allTestFiles = await listRepoTestFiles(testRoot);
|
|
51
74
|
const isolatedTestSet = new Set(isolatedTests);
|
|
52
75
|
const defaultTestFiles = allTestFiles.filter((f) => !isolatedTestSet.has(f));
|
|
53
|
-
const coverageBatches = buildCoverageBatches(defaultTestFiles, [...supplementalTests, ...isolatedTests], process.env);
|
|
54
76
|
for (const leg of legs) {
|
|
55
77
|
const legDir = path.join(coverageDir, "matrix", leg.name);
|
|
56
78
|
await fs.mkdir(legDir, { recursive: true });
|
|
57
79
|
|
|
80
|
+
const legDefaultFiles = leg.name === "vice-mock"
|
|
81
|
+
? DEFAULT_VICE_MOCK_TEST_FILES
|
|
82
|
+
: leg.name === "vice-device"
|
|
83
|
+
? DEFAULT_VICE_DEVICE_TEST_FILES
|
|
84
|
+
: defaultTestFiles;
|
|
85
|
+
const legSupplementalTests = leg.name.startsWith("vice")
|
|
86
|
+
? []
|
|
87
|
+
: [...supplementalTests, ...isolatedTests];
|
|
88
|
+
const coverageBatches = buildCoverageBatches(legDefaultFiles, legSupplementalTests, process.env);
|
|
89
|
+
|
|
58
90
|
const reports = [];
|
|
59
91
|
for (const batch of coverageBatches) {
|
|
60
92
|
reports.push(await runCoverageLeg(leg, legDir, batch.label, batch.files));
|
package/scripts/run-tests.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { fileURLToPath } from "node:url";
|
|
|
8
8
|
export const DEFAULT_TARGET = "mock";
|
|
9
9
|
|
|
10
10
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
|
-
const
|
|
11
|
+
const tsLoader = "tsx/esm";
|
|
12
12
|
const { nodeArgs, target, explicitBaseUrl } = buildNodeTestArgs(process.argv.slice(2));
|
|
13
13
|
const defaultEmbeddingsDir = path.join(repoRoot, "artifacts", "test-embeddings");
|
|
14
14
|
if (!fs.existsSync(defaultEmbeddingsDir)) {
|
|
@@ -180,7 +180,7 @@ function isMainModule(moduleUrl) {
|
|
|
180
180
|
|
|
181
181
|
export function buildNodeTestArgs(args, root = repoRoot) {
|
|
182
182
|
const defaultTestFiles = listRepoTestFiles(path.join(root, "test"), root);
|
|
183
|
-
const nodeArgs = ["--import",
|
|
183
|
+
const nodeArgs = ["--import", tsLoader, "--test"];
|
|
184
184
|
let target = DEFAULT_TARGET;
|
|
185
185
|
let explicitBaseUrl = null;
|
|
186
186
|
let hasExplicitTestFiles = false;
|
package/scripts/run-tests.ts
CHANGED
|
@@ -9,14 +9,79 @@ import { fileURLToPath } from "node:url";
|
|
|
9
9
|
const DEFAULT_TARGET = "mock";
|
|
10
10
|
const DEFAULT_PLATFORM = "c64u";
|
|
11
11
|
const DEFAULT_BUN_FILE_LIMIT = 4;
|
|
12
|
-
|
|
12
|
+
// Keep default Bun shards small enough to avoid descriptor exhaustion on broad suites.
|
|
13
|
+
const DEFAULT_BUN_BATCH_SIZE = DEFAULT_BUN_FILE_LIMIT;
|
|
14
|
+
const DEFAULT_VICE_MOCK_TEST_FILES = [
|
|
15
|
+
"test/device.test.mjs",
|
|
16
|
+
"test/viceIntegration.test.mjs",
|
|
17
|
+
"test/viceModule.test.mjs",
|
|
18
|
+
"test/groupedToolsShims.test.mjs",
|
|
19
|
+
"test/inputModule.test.mjs",
|
|
20
|
+
"test/batchAndDisplayState.test.mjs",
|
|
21
|
+
"test/toolsTypes.test.mjs",
|
|
22
|
+
"test/platformRegistry.test.mjs",
|
|
23
|
+
"test/meta/program.test.mjs",
|
|
24
|
+
"test/mcpServerIntegration.test.mjs",
|
|
25
|
+
"test/c64Client.test.mjs",
|
|
26
|
+
"test/vice/viceSmokeTest.ts",
|
|
27
|
+
];
|
|
28
|
+
const DEFAULT_VICE_DEVICE_TEST_FILES = [
|
|
29
|
+
"test/device.test.mjs",
|
|
30
|
+
"test/vice/viceSmokeTest.ts",
|
|
31
|
+
];
|
|
32
|
+
const BUN_ONLY_TEST_IMPORT_RE = /from\s+["']bun:test["']/;
|
|
33
|
+
const BUN_RUNTIME_REQUIRED_TEST_FILES = new Set([
|
|
34
|
+
"test/generateMcpInterface.test.mjs",
|
|
35
|
+
"test/scripts/start.test.mjs",
|
|
36
|
+
]);
|
|
37
|
+
const ISOLATED_NODE_TEST_FILES = new Set([
|
|
38
|
+
"test/mcpServerIntegration.test.mjs",
|
|
39
|
+
"test/mcpServerPlatformInit.test.mjs",
|
|
40
|
+
"test/meta/background.test.mjs",
|
|
41
|
+
"test/pollIntegration.test.mjs",
|
|
42
|
+
"test/pollValidator.test.mjs",
|
|
43
|
+
"test/programRunnersModule.test.mjs",
|
|
44
|
+
"test/toolsCoverage.test.mjs",
|
|
45
|
+
]);
|
|
13
46
|
const ISOLATED_BUN_TEST_FILES = new Set([
|
|
14
47
|
"test/audioRuntime.test.mjs",
|
|
48
|
+
"test/mcpServerIntegration.test.mjs",
|
|
49
|
+
"test/mcpServerPlatformInit.test.mjs",
|
|
15
50
|
]);
|
|
16
51
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
17
52
|
const defaultEmbeddingsDir = path.join(repoRoot, "artifacts", "test-embeddings");
|
|
18
53
|
const defaultTestFiles = listRepoTestFiles(path.join(repoRoot, "test"));
|
|
19
54
|
|
|
55
|
+
function findExecutableOnPath(binary: string, envPath: string | undefined = process.env.PATH): string | null {
|
|
56
|
+
const normalized = binary.trim();
|
|
57
|
+
if (!normalized) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
if (path.isAbsolute(normalized) || normalized.includes(path.sep)) {
|
|
61
|
+
try {
|
|
62
|
+
fs.accessSync(normalized, fs.constants.X_OK);
|
|
63
|
+
return normalized;
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const entry of (envPath ?? "").split(path.delimiter)) {
|
|
70
|
+
if (!entry) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const candidate = path.join(entry, normalized);
|
|
74
|
+
try {
|
|
75
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
76
|
+
return candidate;
|
|
77
|
+
} catch {
|
|
78
|
+
// Continue searching PATH.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
20
85
|
function resolveNodeExecutable(): string {
|
|
21
86
|
const candidates = [
|
|
22
87
|
process.env.C64BRIDGE_TEST_NODE_BIN,
|
|
@@ -35,6 +100,31 @@ function resolveNodeExecutable(): string {
|
|
|
35
100
|
return "node";
|
|
36
101
|
}
|
|
37
102
|
|
|
103
|
+
function resolveBunExecutable(env: NodeJS.ProcessEnv = process.env): string | null {
|
|
104
|
+
if (typeof (globalThis as { Bun?: unknown }).Bun !== "undefined") {
|
|
105
|
+
return process.execPath;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const candidates = [
|
|
109
|
+
env.C64BRIDGE_TEST_BUN_BIN,
|
|
110
|
+
env.C64BRIDGE_BUN_BIN,
|
|
111
|
+
process.env.C64BRIDGE_TEST_BUN_BIN,
|
|
112
|
+
process.env.C64BRIDGE_BUN_BIN,
|
|
113
|
+
"bun",
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
for (const candidate of candidates) {
|
|
117
|
+
if (candidate && typeof candidate === "string" && candidate.trim()) {
|
|
118
|
+
const resolved = findExecutableOnPath(candidate.trim(), env.PATH ?? process.env.PATH);
|
|
119
|
+
if (resolved) {
|
|
120
|
+
return resolved;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
38
128
|
export type RunTestsArgs = {
|
|
39
129
|
target: string;
|
|
40
130
|
platform: "c64u" | "vice";
|
|
@@ -43,6 +133,53 @@ export type RunTestsArgs = {
|
|
|
43
133
|
passthrough: string[];
|
|
44
134
|
};
|
|
45
135
|
|
|
136
|
+
export function buildMatrixEnv(
|
|
137
|
+
platform: "c64u" | "vice",
|
|
138
|
+
target: "mock" | "device",
|
|
139
|
+
explicitBaseUrl: string | null,
|
|
140
|
+
envSource: NodeJS.ProcessEnv = process.env,
|
|
141
|
+
): Record<string, string> {
|
|
142
|
+
const env: Record<string, string> = { ...envSource } as Record<string, string>;
|
|
143
|
+
env.C64_MODE = platform;
|
|
144
|
+
env.C64_TEST_TARGET = target === "device" ? "real" : "mock";
|
|
145
|
+
|
|
146
|
+
if (platform === "vice") {
|
|
147
|
+
const useViceMock = target !== "device";
|
|
148
|
+
env.VICE_TEST_TARGET = useViceMock ? "mock" : "vice";
|
|
149
|
+
env.C64_TEST_ENABLE_VICE_MOCK = useViceMock ? "1" : "0";
|
|
150
|
+
if (!useViceMock) {
|
|
151
|
+
env.VICE_AVAILABLE = "1";
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
delete env.VICE_TEST_TARGET;
|
|
155
|
+
delete env.C64_TEST_ENABLE_VICE_MOCK;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!env.RAG_EMBEDDINGS_DIR) {
|
|
159
|
+
env.RAG_EMBEDDINGS_DIR = defaultEmbeddingsDir;
|
|
160
|
+
}
|
|
161
|
+
if (explicitBaseUrl) {
|
|
162
|
+
env.C64_TEST_BASE_URL = explicitBaseUrl;
|
|
163
|
+
}
|
|
164
|
+
if (platform === "c64u" && target === "device" && !env.C64_TEST_BASE_URL) {
|
|
165
|
+
env.C64_TEST_BASE_URL = resolveBaseUrlFromConfig(env) ?? "http://c64u";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return env;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function resolveDefaultMatrixTestFiles(
|
|
172
|
+
platform: "c64u" | "vice",
|
|
173
|
+
target: "mock" | "device",
|
|
174
|
+
): string[] {
|
|
175
|
+
if (platform !== "vice") {
|
|
176
|
+
return [...defaultTestFiles];
|
|
177
|
+
}
|
|
178
|
+
return target === "device"
|
|
179
|
+
? [...DEFAULT_VICE_DEVICE_TEST_FILES]
|
|
180
|
+
: [...DEFAULT_VICE_MOCK_TEST_FILES];
|
|
181
|
+
}
|
|
182
|
+
|
|
46
183
|
export function parseRunTestsArgs(args: string[]): RunTestsArgs {
|
|
47
184
|
let target = DEFAULT_TARGET;
|
|
48
185
|
let platform: "c64u" | "vice" = DEFAULT_PLATFORM;
|
|
@@ -106,6 +243,10 @@ export function shouldUseNodeFallback(runCoverage: boolean, passthrough: string[
|
|
|
106
243
|
if (runCoverage) {
|
|
107
244
|
return false;
|
|
108
245
|
}
|
|
246
|
+
if (String(env.C64_MODE ?? "").trim().toLowerCase() === "vice"
|
|
247
|
+
&& String(env.VICE_TEST_TARGET ?? "").trim().toLowerCase() === "vice") {
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
109
250
|
|
|
110
251
|
const explicitFiles = passthrough.filter(looksLikeTestFileArg);
|
|
111
252
|
if (passthrough.length === 0) {
|
|
@@ -139,11 +280,72 @@ export function buildBunTestBatches(passthrough: string[], env: NodeJS.ProcessEn
|
|
|
139
280
|
];
|
|
140
281
|
}
|
|
141
282
|
|
|
283
|
+
export function splitPassthroughByRuntime(
|
|
284
|
+
passthrough: string[],
|
|
285
|
+
root: string = repoRoot,
|
|
286
|
+
): { nodePassthrough: string[]; bunPassthrough: string[] } {
|
|
287
|
+
const explicitFiles = passthrough.filter(looksLikeTestFileArg);
|
|
288
|
+
if (explicitFiles.length === 0) {
|
|
289
|
+
return { nodePassthrough: [...passthrough], bunPassthrough: [] };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const sharedArgs = passthrough.filter((arg) => !looksLikeTestFileArg(arg));
|
|
293
|
+
const bunFiles = explicitFiles.filter((file) => isBunOnlyTestFile(path.join(root, file)));
|
|
294
|
+
const nodeFiles = explicitFiles.filter((file) => !bunFiles.includes(file));
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
nodePassthrough: nodeFiles.length > 0 ? [...nodeFiles, ...sharedArgs] : [],
|
|
298
|
+
bunPassthrough: bunFiles.length > 0 ? [...bunFiles, ...sharedArgs] : [],
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function buildNodeFallbackBatches(
|
|
303
|
+
passthrough: string[],
|
|
304
|
+
root: string = repoRoot,
|
|
305
|
+
): string[][] {
|
|
306
|
+
const explicitFiles = passthrough.filter(looksLikeTestFileArg);
|
|
307
|
+
if (explicitFiles.length === 0) {
|
|
308
|
+
return [passthrough];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const sharedArgs = passthrough.filter((arg) => !looksLikeTestFileArg(arg));
|
|
312
|
+
const isolatedFiles = explicitFiles.filter((file) => isNodeIsolatedTestFile(path.join(root, file)));
|
|
313
|
+
const sharedFiles = explicitFiles.filter((file) => !isolatedFiles.includes(file));
|
|
314
|
+
const batches: string[][] = [];
|
|
315
|
+
|
|
316
|
+
if (sharedFiles.length > 0) {
|
|
317
|
+
batches.push([...sharedFiles, ...sharedArgs]);
|
|
318
|
+
}
|
|
319
|
+
for (const file of isolatedFiles) {
|
|
320
|
+
batches.push([file, ...sharedArgs]);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return batches.length > 0 ? batches : [passthrough];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function isBunOnlyTestFile(filePath: string): boolean {
|
|
327
|
+
const relativePath = path.relative(repoRoot, filePath).split(path.sep).join("/");
|
|
328
|
+
if (BUN_RUNTIME_REQUIRED_TEST_FILES.has(relativePath)) {
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
return BUN_ONLY_TEST_IMPORT_RE.test(fs.readFileSync(filePath, "utf8"));
|
|
333
|
+
} catch {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function isNodeIsolatedTestFile(filePath: string): boolean {
|
|
339
|
+
const relativePath = path.relative(repoRoot, filePath).split(path.sep).join("/");
|
|
340
|
+
return ISOLATED_NODE_TEST_FILES.has(relativePath);
|
|
341
|
+
}
|
|
342
|
+
|
|
142
343
|
async function runNodeFallback(target: string, explicitBaseUrl: string | null, passthrough: string[], env: Record<string, string>, runCoverage: boolean): Promise<number> {
|
|
143
344
|
console.warn("[run-tests] Using Node runner for this test set to avoid Bun memory growth on broad suites");
|
|
144
345
|
if (runCoverage) {
|
|
145
346
|
console.warn("[run-tests] Coverage reporting is unavailable in Node fallback mode");
|
|
146
347
|
}
|
|
348
|
+
const { nodePassthrough, bunPassthrough } = splitPassthroughByRuntime(passthrough);
|
|
147
349
|
const nodeExecutable = resolveNodeExecutable();
|
|
148
350
|
const nodeScript = path.join(repoRoot, "scripts", "run-tests.mjs");
|
|
149
351
|
const fallbackArgs = [] as string[];
|
|
@@ -153,23 +355,53 @@ async function runNodeFallback(target: string, explicitBaseUrl: string | null, p
|
|
|
153
355
|
if (explicitBaseUrl) {
|
|
154
356
|
fallbackArgs.push(`--base-url=${explicitBaseUrl}`);
|
|
155
357
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
358
|
+
if (nodePassthrough.length > 0 || bunPassthrough.length === 0) {
|
|
359
|
+
const nodeBatches = buildNodeFallbackBatches(nodePassthrough);
|
|
360
|
+
for (let index = 0; index < nodeBatches.length; index += 1) {
|
|
361
|
+
const batch = nodeBatches[index] ?? [];
|
|
362
|
+
if (nodeBatches.length > 1) {
|
|
363
|
+
console.warn(`[run-tests] Node fallback batch ${index + 1}/${nodeBatches.length} (${batch.filter(looksLikeTestFileArg).length} files)`);
|
|
364
|
+
}
|
|
365
|
+
const nodeExitCode = await new Promise<number>((resolve) => {
|
|
366
|
+
const childProcess = spawn(nodeExecutable, [nodeScript, ...fallbackArgs, ...batch], {
|
|
367
|
+
cwd: repoRoot,
|
|
368
|
+
env,
|
|
369
|
+
stdio: "inherit",
|
|
370
|
+
}) as unknown as {
|
|
371
|
+
on(event: "error", listener: (error: Error) => void): void;
|
|
372
|
+
on(event: "exit", listener: (code: number | null) => void): void;
|
|
373
|
+
};
|
|
374
|
+
childProcess.on("error", (error) => {
|
|
375
|
+
console.error("[run-tests] Failed to launch Node fallback:", error);
|
|
376
|
+
resolve(1);
|
|
377
|
+
});
|
|
378
|
+
childProcess.on("exit", (code) => {
|
|
379
|
+
resolve(typeof code === "number" ? code : 1);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
if (nodeExitCode !== 0) {
|
|
384
|
+
return nodeExitCode;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (bunPassthrough.length === 0) {
|
|
390
|
+
return 0;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const bunExecutable = resolveBunExecutable(env);
|
|
394
|
+
if (!bunExecutable) {
|
|
395
|
+
console.error("[run-tests] Bun-only tests were selected but no Bun executable is available");
|
|
396
|
+
return 1;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
console.warn("[run-tests] Running Bun-only tests under Bun to preserve runtime coverage");
|
|
400
|
+
const bunBatches = buildBunTestBatches(bunPassthrough, env);
|
|
401
|
+
return await runBunBatches(env, bunBatches, {
|
|
402
|
+
coverage: false,
|
|
403
|
+
bunExecutable,
|
|
404
|
+
labelPrefix: "bun-only-suite",
|
|
173
405
|
});
|
|
174
406
|
}
|
|
175
407
|
|
|
@@ -200,24 +432,11 @@ async function main(): Promise<number> {
|
|
|
200
432
|
fs.mkdirSync(defaultEmbeddingsDir, { recursive: true });
|
|
201
433
|
}
|
|
202
434
|
|
|
203
|
-
const env: Record<string, string> = { ...process.env } as Record<string, string>;
|
|
204
435
|
const normalizedTarget = normalizeTarget(target);
|
|
205
|
-
env
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
} else {
|
|
210
|
-
delete env.VICE_TEST_TARGET;
|
|
211
|
-
}
|
|
212
|
-
if (!env.RAG_EMBEDDINGS_DIR) {
|
|
213
|
-
env.RAG_EMBEDDINGS_DIR = defaultEmbeddingsDir;
|
|
214
|
-
}
|
|
215
|
-
if (explicitBaseUrl) {
|
|
216
|
-
env.C64_TEST_BASE_URL = explicitBaseUrl;
|
|
217
|
-
}
|
|
218
|
-
if (platform === "c64u" && normalizedTarget === "device" && !env.C64_TEST_BASE_URL) {
|
|
219
|
-
env.C64_TEST_BASE_URL = resolveBaseUrlFromConfig(env) ?? "http://c64u";
|
|
220
|
-
}
|
|
436
|
+
const env = buildMatrixEnv(platform, normalizedTarget, explicitBaseUrl);
|
|
437
|
+
const effectivePassthrough = passthrough.length > 0
|
|
438
|
+
? passthrough
|
|
439
|
+
: resolveDefaultMatrixTestFiles(platform, normalizedTarget);
|
|
221
440
|
|
|
222
441
|
printMatrixHeading({
|
|
223
442
|
platform,
|
|
@@ -230,24 +449,20 @@ async function main(): Promise<number> {
|
|
|
230
449
|
const bunRuntime = (globalThis as { Bun?: unknown }).Bun;
|
|
231
450
|
if (!bunRuntime) {
|
|
232
451
|
console.warn("[run-tests] Bun runtime not detected; falling back to Node runner");
|
|
233
|
-
return await runNodeFallback(target, explicitBaseUrl,
|
|
452
|
+
return await runNodeFallback(target, explicitBaseUrl, effectivePassthrough, env, runCoverage);
|
|
234
453
|
}
|
|
235
454
|
|
|
236
|
-
if (
|
|
237
|
-
return await
|
|
238
|
-
coverage: false,
|
|
239
|
-
labelPrefix: "default-suite",
|
|
240
|
-
});
|
|
455
|
+
if (shouldUseNodeFallback(runCoverage, effectivePassthrough, env)) {
|
|
456
|
+
return await runNodeFallback(target, explicitBaseUrl, effectivePassthrough, env, runCoverage);
|
|
241
457
|
}
|
|
242
458
|
|
|
243
|
-
if (
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
return await runNodeFallback(target, explicitBaseUrl, passthrough, env, runCoverage);
|
|
459
|
+
if (!runCoverage) {
|
|
460
|
+
const batches = buildBunTestBatches(effectivePassthrough, env);
|
|
461
|
+
return await runBunBatches(env, batches, {
|
|
462
|
+
bunExecutable: process.execPath,
|
|
463
|
+
coverage: false,
|
|
464
|
+
labelPrefix: passthrough.length === 0 ? "default-suite" : "sharded-suite",
|
|
465
|
+
});
|
|
251
466
|
}
|
|
252
467
|
|
|
253
468
|
const bunArgs = [
|
|
@@ -259,7 +474,7 @@ async function main(): Promise<number> {
|
|
|
259
474
|
"--coverage-reporter=text",
|
|
260
475
|
]
|
|
261
476
|
: []),
|
|
262
|
-
...(
|
|
477
|
+
...effectivePassthrough.map((arg) => normalizeBunBatchArg(arg)),
|
|
263
478
|
];
|
|
264
479
|
return await runExternalCommand(process.execPath, bunArgs, env);
|
|
265
480
|
}
|
|
@@ -345,7 +560,7 @@ function chunkFiles(files: string[], chunkSize: number): string[][] {
|
|
|
345
560
|
async function runBunBatches(
|
|
346
561
|
env: Record<string, string>,
|
|
347
562
|
batches: string[][],
|
|
348
|
-
options: { coverage: boolean; labelPrefix: string },
|
|
563
|
+
options: { coverage: boolean; labelPrefix: string; bunExecutable: string },
|
|
349
564
|
): Promise<number> {
|
|
350
565
|
const coverageArgs = options.coverage
|
|
351
566
|
? ["--coverage", "--coverage-reporter=lcov", "--coverage-reporter=text"]
|
|
@@ -354,7 +569,8 @@ async function runBunBatches(
|
|
|
354
569
|
for (let index = 0; index < batches.length; index += 1) {
|
|
355
570
|
const batch = batches[index] ?? [];
|
|
356
571
|
console.log(`[run-tests] ${options.labelPrefix} batch ${index + 1}/${batches.length} (${batch.length} entries)`);
|
|
357
|
-
const
|
|
572
|
+
const bunArgs = batch.map((arg) => normalizeBunBatchArg(arg));
|
|
573
|
+
const exitCode = await runExternalCommand(options.bunExecutable, ["test", ...coverageArgs, ...bunArgs], env);
|
|
358
574
|
if (exitCode !== 0) {
|
|
359
575
|
return exitCode;
|
|
360
576
|
}
|
|
@@ -363,6 +579,13 @@ async function runBunBatches(
|
|
|
363
579
|
return 0;
|
|
364
580
|
}
|
|
365
581
|
|
|
582
|
+
export function normalizeBunBatchArg(arg: string): string {
|
|
583
|
+
if (looksLikeTestFileArg(arg) && !arg.startsWith("./") && !arg.startsWith("../") && !path.isAbsolute(arg)) {
|
|
584
|
+
return `./${arg}`;
|
|
585
|
+
}
|
|
586
|
+
return arg;
|
|
587
|
+
}
|
|
588
|
+
|
|
366
589
|
function listRepoTestFiles(testRoot: string): string[] {
|
|
367
590
|
const files: string[] = [];
|
|
368
591
|
|