c64bridge 0.7.4 → 0.8.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/README.md +14 -4
- package/.github/prompts/assembly-routine-session.prompt.md +9 -9
- package/.github/prompts/basic-program-session.prompt.md +11 -9
- package/.github/prompts/drive-management-session.prompt.md +9 -9
- package/.github/prompts/hello-world-session.prompt.md +17 -0
- package/.github/prompts/petscii-art-session.prompt.md +9 -9
- package/.github/prompts/play-classic-game.prompt.md +9 -9
- package/.github/prompts/pr-converge.prompt.md +13 -0
- package/.github/prompts/sid-music-session.prompt.md +9 -9
- package/AGENTS.md +59 -60
- package/README.md +550 -191
- package/data/assembly/assembly-spec.md +290 -1
- package/data/context/bootstrap.md +26 -2
- package/data/context/fast-paths.md +44 -0
- package/data/graphics/petscii-style-guide.md +6 -2
- package/data/graphics/sprite-charset-best-practices.md +5 -5
- package/dist/.tsbuildinfo +1 -1
- package/dist/c64Client.js +990 -26
- package/dist/config.js +5 -1
- package/dist/device.js +680 -57
- package/dist/diagnostics.js +371 -0
- package/dist/mcp/metadata.js +39 -0
- package/dist/mcp-server.js +170 -20
- package/dist/platform.js +10 -2
- package/dist/prompts/registry.js +166 -309
- package/dist/rag/init.js +37 -1
- package/dist/rag/knowledgeIndex.js +18 -6
- package/dist/rag/rateLimiter.js +2 -2
- package/dist/streamCapture.js +135 -0
- package/dist/tools/audio.js +3 -1
- package/dist/tools/debug.js +470 -0
- package/dist/tools/developer.js +10 -4
- package/dist/tools/graphics.js +183 -17
- package/dist/tools/machineControl.js +11 -5
- package/dist/tools/memory.js +19 -10
- package/dist/tools/meta/audio.js +240 -1
- package/dist/tools/meta/config.js +20 -28
- package/dist/tools/meta/configInventory.js +104 -0
- package/dist/tools/meta/diagnostics.js +45 -2
- package/dist/tools/meta/graphics.js +14 -6
- package/dist/tools/meta/index.js +1 -0
- package/dist/tools/meta/memory.js +20 -6
- package/dist/tools/meta/program.js +473 -0
- package/dist/tools/pollValidator.js +4 -2
- package/dist/tools/programRunners.js +8 -4
- package/dist/tools/registry/config.js +17 -0
- package/dist/tools/registry/disk.js +7 -0
- package/dist/tools/registry/drive.js +3 -0
- package/dist/tools/registry/graphics.js +111 -27
- package/dist/tools/registry/index.js +6 -0
- package/dist/tools/registry/memory.js +1 -0
- package/dist/tools/registry/platform.js +121 -0
- package/dist/tools/registry/program.js +26 -8
- package/dist/tools/registry/sound.js +96 -0
- package/dist/tools/registry/system.js +15 -2
- package/dist/tools/storage.js +1 -0
- package/dist/tools/streaming.js +1 -0
- package/dist/tools/types.js +38 -4
- package/dist/tools/vice.js +160 -0
- package/dist/vicBitmap.js +317 -0
- package/dist/vice/mockServer.js +439 -0
- package/dist/vice/process.js +310 -0
- package/dist/vice/readiness.js +18 -2
- package/dist/vice/viceClient.js +382 -22
- package/doc/developer.md +71 -25
- package/doc/img/backend-switch/hello-c64u.png +0 -0
- package/doc/img/backend-switch/hello-vice-and-c64u.png +0 -0
- package/doc/img/backend-switch/hello-vice.png +0 -0
- package/doc/img/backend-switch/metadata.json +50 -0
- package/doc/plans/vice-backend-switch/plan.md +486 -0
- package/doc/plans/vice-backend-switch-2/plan.md +48 -0
- package/doc/plans/vice-support/notes.md +1 -0
- package/doc/plans/vice-support/plan.md +5 -0
- package/doc/plans/vice-support/rollout.md +23 -17
- package/doc/reviews/vice-support-review.md +185 -0
- package/doc/troubleshooting-mcp.md +35 -6
- package/doc/vice/support-matrix.md +54 -0
- package/mcp/prompts.json +292 -0
- package/mcp/protocol-examples.json +46 -0
- package/mcp/resources.json +940 -0
- package/mcp/schemas/c64_config.schema.json +350 -0
- package/mcp/schemas/c64_debug.schema.json +380 -0
- package/mcp/schemas/c64_disk.schema.json +278 -0
- package/mcp/schemas/c64_drive.schema.json +120 -0
- package/mcp/schemas/c64_extract.schema.json +302 -0
- package/mcp/schemas/c64_graphics.schema.json +333 -0
- package/mcp/schemas/c64_memory.schema.json +155 -0
- package/mcp/schemas/c64_printer.schema.json +218 -0
- package/mcp/schemas/c64_program.schema.json +391 -0
- package/mcp/schemas/c64_rag.schema.json +67 -0
- package/mcp/schemas/c64_select_backend.schema.json +32 -0
- package/mcp/schemas/c64_sound.schema.json +644 -0
- package/mcp/schemas/c64_stream.schema.json +61 -0
- package/mcp/schemas/c64_system.schema.json +227 -0
- package/mcp/schemas/c64_vice.schema.json +63 -0
- package/mcp/server.json +15 -0
- package/mcp/tools.json +4517 -0
- package/mcp.json +90 -15
- package/package.json +11 -4
- package/scripts/capture-backend-switch-screenshots.mjs +276 -0
- package/scripts/generate-mcp-interface.ts +289 -0
- package/scripts/invoke-bun.mjs +67 -21
- package/scripts/mockC64Server.mjs +162 -6
- package/scripts/prepare-release.mjs +59 -2
- package/scripts/run-coverage.mjs +284 -0
- package/scripts/run-tests.mjs +95 -39
- package/scripts/run-tests.ts +378 -73
- package/scripts/update-readme.ts +147 -13
- package/server.json +94 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execSync } from 'node:child_process';
|
|
3
|
-
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { access, readFile, writeFile } from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import process from 'node:process';
|
|
6
6
|
|
|
@@ -16,6 +16,27 @@ const updateJsonFile = async (relativePath, updater) => {
|
|
|
16
16
|
await writeFile(filePath, JSON.stringify(updated, null, 2) + '\n');
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
const fileExists = async (relativePath) => {
|
|
20
|
+
try {
|
|
21
|
+
await access(path.resolve(relativePath));
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const getRepositoryMetadata = (repository) => {
|
|
29
|
+
const url = typeof repository === 'string' ? repository : repository?.url;
|
|
30
|
+
if (!url) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
url,
|
|
36
|
+
source: url.includes('github.com') ? 'github' : undefined,
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
19
40
|
const normaliseVersion = (value) => value.replace(/^v/, '');
|
|
20
41
|
|
|
21
42
|
const isExactVersion = (value) => /^v?\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(value);
|
|
@@ -72,6 +93,42 @@ await updateJsonFile('mcp.json', async (data) => ({
|
|
|
72
93
|
version: newVersion,
|
|
73
94
|
}));
|
|
74
95
|
|
|
96
|
+
if (await fileExists('server.json')) {
|
|
97
|
+
await updateJsonFile('server.json', async (data) => {
|
|
98
|
+
const packages = Array.isArray(data.packages) ? [...data.packages] : [];
|
|
99
|
+
const primaryPackage = packages[0] ?? {
|
|
100
|
+
registryType: 'npm',
|
|
101
|
+
transport: {
|
|
102
|
+
type: 'stdio',
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
packages[0] = {
|
|
107
|
+
...primaryPackage,
|
|
108
|
+
identifier: pkg.name,
|
|
109
|
+
version: newVersion,
|
|
110
|
+
transport: primaryPackage.transport ?? {
|
|
111
|
+
type: 'stdio',
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const repository = getRepositoryMetadata(pkg.repository);
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
...data,
|
|
119
|
+
name: pkg.mcpName ?? data.name,
|
|
120
|
+
version: newVersion,
|
|
121
|
+
repository: repository
|
|
122
|
+
? {
|
|
123
|
+
...(data.repository ?? {}),
|
|
124
|
+
...repository,
|
|
125
|
+
}
|
|
126
|
+
: data.repository,
|
|
127
|
+
packages,
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
75
132
|
// Manifest generation removed (runtime discovery via MCP stdio)
|
|
76
133
|
|
|
77
134
|
// Update CHANGELOG.md from commits since last tag using Conventional Commits subjects.
|
|
@@ -88,4 +145,4 @@ if (skippedVersionBump && hasExistingChangelogEntry) {
|
|
|
88
145
|
}
|
|
89
146
|
|
|
90
147
|
console.log(`Release metadata updated to ${newVersion}.`);
|
|
91
|
-
console.log('Next steps: commit the changes,
|
|
148
|
+
console.log('Next steps: commit and push the changes, then create the release tag from GitHub once main is ready.');
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
const repoRoot = path.resolve(__dirname, "..");
|
|
11
|
+
const coverageDir = path.join(repoRoot, "coverage");
|
|
12
|
+
const runner = path.join(repoRoot, "scripts", "invoke-bun.mjs");
|
|
13
|
+
const configPath = path.join(repoRoot, ".c8rc.json");
|
|
14
|
+
const testRoot = path.join(repoRoot, "test");
|
|
15
|
+
const DEFAULT_COVERAGE_SHARD_SIZE = 12;
|
|
16
|
+
|
|
17
|
+
const legs = [
|
|
18
|
+
{ name: "c64u-mock", args: ["--platform=c64u", "--target=mock"] },
|
|
19
|
+
{ name: "vice-mock", args: ["--platform=vice", "--target=mock"] },
|
|
20
|
+
{ name: "vice-device", args: ["--platform=vice", "--target=device"] },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const supplementalTests = [
|
|
24
|
+
"test/toolsCoverage.test.mjs",
|
|
25
|
+
"test/audioModule.test.mjs",
|
|
26
|
+
"test/updateReadme.test.mjs",
|
|
27
|
+
"test/viceIntegration.test.mjs",
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// Tests that use mock.module() must run in isolation to prevent the module
|
|
31
|
+
// mock from leaking into concurrently-executing test files.
|
|
32
|
+
const isolatedTests = [
|
|
33
|
+
"test/audioRuntime.test.mjs",
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const coverageConfig = JSON.parse(await fs.readFile(configPath, "utf8"));
|
|
37
|
+
const includeMatchers = (coverageConfig.include ?? []).map(globToRegExp);
|
|
38
|
+
const excludeMatchers = (coverageConfig.exclude ?? []).map(globToRegExp);
|
|
39
|
+
|
|
40
|
+
if (import.meta.main) {
|
|
41
|
+
await main();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function main() {
|
|
45
|
+
await fs.mkdir(coverageDir, { recursive: true });
|
|
46
|
+
await fs.rm(path.join(coverageDir, "matrix"), { recursive: true, force: true });
|
|
47
|
+
await fs.mkdir(path.join(coverageDir, "matrix"), { recursive: true });
|
|
48
|
+
|
|
49
|
+
const legOutputs = [];
|
|
50
|
+
const allTestFiles = await listRepoTestFiles(testRoot);
|
|
51
|
+
const isolatedTestSet = new Set(isolatedTests);
|
|
52
|
+
const defaultTestFiles = allTestFiles.filter((f) => !isolatedTestSet.has(f));
|
|
53
|
+
const coverageBatches = buildCoverageBatches(defaultTestFiles, [...supplementalTests, ...isolatedTests], process.env);
|
|
54
|
+
for (const leg of legs) {
|
|
55
|
+
const legDir = path.join(coverageDir, "matrix", leg.name);
|
|
56
|
+
await fs.mkdir(legDir, { recursive: true });
|
|
57
|
+
|
|
58
|
+
const reports = [];
|
|
59
|
+
for (const batch of coverageBatches) {
|
|
60
|
+
reports.push(await runCoverageLeg(leg, legDir, batch.label, batch.files));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const mergedLeg = mergeReports(await Promise.all(reports.map(readReport)));
|
|
64
|
+
const legPath = path.join(coverageDir, `${leg.name}.lcov.info`);
|
|
65
|
+
await fs.writeFile(legPath, serializeReport(mergedLeg), "utf8");
|
|
66
|
+
legOutputs.push(legPath);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const finalReport = mergeReports(await Promise.all(legOutputs.map(readReport)));
|
|
70
|
+
const finalPath = path.join(coverageDir, "lcov.info");
|
|
71
|
+
await fs.writeFile(finalPath, serializeReport(finalReport), "utf8");
|
|
72
|
+
|
|
73
|
+
const summary = summariseReport(finalReport);
|
|
74
|
+
console.log(JSON.stringify({ lines: summary }, null, 2));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function buildCoverageBatches(testFiles, extraTests, env = process.env) {
|
|
78
|
+
const shardSize = resolveCoverageShardSize(env.C64BRIDGE_COVERAGE_SHARD_SIZE);
|
|
79
|
+
const batches = chunkFiles(testFiles, shardSize).map((files, index, all) => ({
|
|
80
|
+
label: all.length === 1 ? "all" : `all-${String(index + 1).padStart(2, "0")}`,
|
|
81
|
+
files,
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
for (const testFile of extraTests) {
|
|
85
|
+
batches.push({
|
|
86
|
+
label: path.basename(testFile, path.extname(testFile)),
|
|
87
|
+
files: [testFile],
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return batches;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function resolveCoverageShardSize(raw) {
|
|
95
|
+
const parsed = Number(raw ?? "");
|
|
96
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_COVERAGE_SHARD_SIZE;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function chunkFiles(files, chunkSize) {
|
|
100
|
+
if (files.length === 0) {
|
|
101
|
+
return [[]];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const chunks = [];
|
|
105
|
+
for (let index = 0; index < files.length; index += chunkSize) {
|
|
106
|
+
chunks.push(files.slice(index, index + chunkSize));
|
|
107
|
+
}
|
|
108
|
+
return chunks;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function listRepoTestFiles(root) {
|
|
112
|
+
const files = [];
|
|
113
|
+
|
|
114
|
+
async function walk(dir) {
|
|
115
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
116
|
+
for (const entry of entries) {
|
|
117
|
+
const fullPath = path.join(dir, entry.name);
|
|
118
|
+
if (entry.isDirectory()) {
|
|
119
|
+
await walk(fullPath);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (entry.isFile() && /\.test\.(mjs|ts)$/i.test(entry.name)) {
|
|
123
|
+
files.push(path.relative(repoRoot, fullPath));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
await walk(root);
|
|
129
|
+
files.sort((a, b) => a.localeCompare(b));
|
|
130
|
+
return files;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function runCoverageLeg(leg, legDir, label, files) {
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
const outputPath = path.join(legDir, `${label}.lcov.info`);
|
|
136
|
+
const args = [runner, "scripts/run-tests.ts", "--coverage", ...leg.args, ...files];
|
|
137
|
+
const child = spawn(process.execPath, args, {
|
|
138
|
+
cwd: repoRoot,
|
|
139
|
+
env: process.env,
|
|
140
|
+
stdio: "inherit",
|
|
141
|
+
});
|
|
142
|
+
child.on("error", reject);
|
|
143
|
+
child.on("exit", async (code) => {
|
|
144
|
+
if (code !== 0) {
|
|
145
|
+
reject(new Error(`Coverage run failed for ${leg.name}/${label} with exit code ${code ?? 1}`));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
await fs.copyFile(path.join(coverageDir, "lcov.info"), outputPath);
|
|
150
|
+
resolve(outputPath);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
reject(error);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function readReport(filePath) {
|
|
159
|
+
const text = await fs.readFile(filePath, "utf8");
|
|
160
|
+
return parseReport(text);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parseReport(text) {
|
|
164
|
+
const report = new Map();
|
|
165
|
+
let current = null;
|
|
166
|
+
for (const line of text.split(/\r?\n/)) {
|
|
167
|
+
if (!line) continue;
|
|
168
|
+
if (line.startsWith("SF:")) {
|
|
169
|
+
const filePath = line.slice(3).trim();
|
|
170
|
+
const relative = toRelativeSourcePath(filePath);
|
|
171
|
+
current = relative ? getOrCreate(report, relative) : null;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (!current) continue;
|
|
175
|
+
if (line.startsWith("DA:")) {
|
|
176
|
+
const [lineNoRaw, hitsRaw] = line.slice(3).split(",", 2);
|
|
177
|
+
const lineNo = Number(lineNoRaw);
|
|
178
|
+
const hits = Number(hitsRaw);
|
|
179
|
+
current.lines.set(lineNo, Math.max(current.lines.get(lineNo) ?? 0, Number.isFinite(hits) ? hits : 0));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return report;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function mergeReports(reports) {
|
|
186
|
+
const merged = new Map();
|
|
187
|
+
for (const report of reports) {
|
|
188
|
+
for (const [filePath, record] of report.entries()) {
|
|
189
|
+
const target = getOrCreate(merged, filePath);
|
|
190
|
+
for (const [lineNo, hits] of record.lines.entries()) {
|
|
191
|
+
target.lines.set(lineNo, Math.max(target.lines.get(lineNo) ?? 0, hits));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return merged;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function serializeReport(report) {
|
|
199
|
+
const lines = [];
|
|
200
|
+
for (const [filePath, record] of [...report.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
201
|
+
let lf = 0;
|
|
202
|
+
let lh = 0;
|
|
203
|
+
lines.push("TN:");
|
|
204
|
+
lines.push(`SF:${filePath}`);
|
|
205
|
+
for (const [lineNo, hits] of [...record.lines.entries()].sort((a, b) => a[0] - b[0])) {
|
|
206
|
+
lines.push(`DA:${lineNo},${hits}`);
|
|
207
|
+
lf += 1;
|
|
208
|
+
if (hits > 0) lh += 1;
|
|
209
|
+
}
|
|
210
|
+
lines.push(`LF:${lf}`);
|
|
211
|
+
lines.push(`LH:${lh}`);
|
|
212
|
+
lines.push("end_of_record");
|
|
213
|
+
}
|
|
214
|
+
return `${lines.join("\n")}\n`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function summariseReport(report) {
|
|
218
|
+
let covered = 0;
|
|
219
|
+
let total = 0;
|
|
220
|
+
for (const record of report.values()) {
|
|
221
|
+
for (const hits of record.lines.values()) {
|
|
222
|
+
total += 1;
|
|
223
|
+
if (hits > 0) covered += 1;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
covered,
|
|
228
|
+
total,
|
|
229
|
+
pct: total === 0 ? "0.00" : ((covered * 100) / total).toFixed(2),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function getOrCreate(report, filePath) {
|
|
234
|
+
let record = report.get(filePath);
|
|
235
|
+
if (!record) {
|
|
236
|
+
record = { lines: new Map() };
|
|
237
|
+
report.set(filePath, record);
|
|
238
|
+
}
|
|
239
|
+
return record;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function toRelativeSourcePath(filePath) {
|
|
243
|
+
const absolute = path.isAbsolute(filePath) ? filePath : path.resolve(repoRoot, filePath);
|
|
244
|
+
const relative = path.relative(repoRoot, absolute).split(path.sep).join("/");
|
|
245
|
+
if (relative.startsWith("..")) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
if (!includeMatchers.some((matcher) => matcher.test(relative))) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
if (excludeMatchers.some((matcher) => matcher.test(relative))) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
return relative;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function globToRegExp(glob) {
|
|
258
|
+
const normalized = glob.split(path.sep).join("/");
|
|
259
|
+
let regex = "^";
|
|
260
|
+
for (let index = 0; index < normalized.length; index += 1) {
|
|
261
|
+
const char = normalized[index];
|
|
262
|
+
const next = normalized[index + 1];
|
|
263
|
+
if (char === "*" && next === "*") {
|
|
264
|
+
regex += ".*";
|
|
265
|
+
index += 1;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (char === "*") {
|
|
269
|
+
regex += "[^/]*";
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (char === "?") {
|
|
273
|
+
regex += ".";
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if ("\\.^$+{}()|[]".includes(char)) {
|
|
277
|
+
regex += `\\${char}`;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
regex += char;
|
|
281
|
+
}
|
|
282
|
+
regex += "$";
|
|
283
|
+
return new RegExp(regex);
|
|
284
|
+
}
|
package/scripts/run-tests.mjs
CHANGED
|
@@ -5,37 +5,11 @@ import os from "node:os";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
|
|
8
|
-
const DEFAULT_TARGET = "mock";
|
|
8
|
+
export const DEFAULT_TARGET = "mock";
|
|
9
9
|
|
|
10
|
-
let target = DEFAULT_TARGET;
|
|
11
|
-
let explicitBaseUrl = null;
|
|
12
10
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
13
11
|
const registerDataUri = "data:text/javascript,import { register } from \"node:module\"; import { pathToFileURL } from \"node:url\"; register(\"ts-node/esm\", pathToFileURL(\"./\"));";
|
|
14
|
-
const nodeArgs
|
|
15
|
-
|
|
16
|
-
for (const arg of process.argv.slice(2)) {
|
|
17
|
-
if (arg === "--mock") {
|
|
18
|
-
target = "mock";
|
|
19
|
-
continue;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (arg === "--real") {
|
|
23
|
-
target = "real";
|
|
24
|
-
continue;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (arg.startsWith("--target=")) {
|
|
28
|
-
target = arg.split("=", 2)[1] ?? DEFAULT_TARGET;
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (arg.startsWith("--base-url=")) {
|
|
33
|
-
explicitBaseUrl = arg.split("=", 2)[1] ?? null;
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
nodeArgs.push(arg);
|
|
38
|
-
}
|
|
12
|
+
const { nodeArgs, target, explicitBaseUrl } = buildNodeTestArgs(process.argv.slice(2));
|
|
39
13
|
const defaultEmbeddingsDir = path.join(repoRoot, "artifacts", "test-embeddings");
|
|
40
14
|
if (!fs.existsSync(defaultEmbeddingsDir)) {
|
|
41
15
|
fs.mkdirSync(defaultEmbeddingsDir, { recursive: true });
|
|
@@ -62,18 +36,20 @@ if (target === "real" && !env.C64_TEST_BASE_URL) {
|
|
|
62
36
|
env.C64_TEST_BASE_URL = resolveBaseUrlFromConfig() ?? "http://c64u";
|
|
63
37
|
}
|
|
64
38
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
39
|
+
if (isMainModule(import.meta.url)) {
|
|
40
|
+
const child = spawn(process.execPath, nodeArgs, {
|
|
41
|
+
stdio: "inherit",
|
|
42
|
+
env,
|
|
43
|
+
});
|
|
69
44
|
|
|
70
|
-
child.on("close", (code, signal) => {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
});
|
|
45
|
+
child.on("close", (code, signal) => {
|
|
46
|
+
if (signal) {
|
|
47
|
+
process.kill(process.pid, signal);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
process.exit(code ?? 1);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
77
53
|
|
|
78
54
|
function resolveBaseUrlFromConfig() {
|
|
79
55
|
const configPathEnv = process.env.C64BRIDGE_CONFIG;
|
|
@@ -193,3 +169,83 @@ function firstDefined(...values) {
|
|
|
193
169
|
}
|
|
194
170
|
return undefined;
|
|
195
171
|
}
|
|
172
|
+
|
|
173
|
+
function isMainModule(moduleUrl) {
|
|
174
|
+
const entry = process.argv[1];
|
|
175
|
+
if (!entry) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
return fileURLToPath(moduleUrl) === path.resolve(entry);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function buildNodeTestArgs(args, root = repoRoot) {
|
|
182
|
+
const defaultTestFiles = listRepoTestFiles(path.join(root, "test"), root);
|
|
183
|
+
const nodeArgs = ["--import", registerDataUri, "--test"];
|
|
184
|
+
let target = DEFAULT_TARGET;
|
|
185
|
+
let explicitBaseUrl = null;
|
|
186
|
+
let hasExplicitTestFiles = false;
|
|
187
|
+
|
|
188
|
+
for (const arg of args) {
|
|
189
|
+
if (arg === "--mock") {
|
|
190
|
+
target = "mock";
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (arg === "--real") {
|
|
195
|
+
target = "real";
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (arg.startsWith("--target=")) {
|
|
200
|
+
target = arg.split("=", 2)[1] ?? DEFAULT_TARGET;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (arg.startsWith("--base-url=")) {
|
|
205
|
+
explicitBaseUrl = arg.split("=", 2)[1] ?? null;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (looksLikeTestFileArg(arg)) {
|
|
210
|
+
hasExplicitTestFiles = true;
|
|
211
|
+
}
|
|
212
|
+
nodeArgs.push(arg);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (!hasExplicitTestFiles) {
|
|
216
|
+
nodeArgs.push(...defaultTestFiles);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return { nodeArgs, target, explicitBaseUrl };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function looksLikeTestFileArg(arg) {
|
|
223
|
+
return typeof arg === "string" && /\.(c|m)?(j|t)sx?$/.test(arg) && !arg.startsWith("--");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function listRepoTestFiles(root, base = repoRoot) {
|
|
227
|
+
if (!fs.existsSync(root)) {
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const files = [];
|
|
232
|
+
walkDirectory(root, files);
|
|
233
|
+
return files
|
|
234
|
+
.filter((file) => looksLikeTestFileArg(file))
|
|
235
|
+
.map((file) => path.relative(base, file).split(path.sep).join("/"))
|
|
236
|
+
.sort();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function walkDirectory(dir, files) {
|
|
240
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
241
|
+
for (const entry of entries) {
|
|
242
|
+
const fullPath = path.join(dir, entry.name);
|
|
243
|
+
if (entry.isDirectory()) {
|
|
244
|
+
walkDirectory(fullPath, files);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (entry.isFile()) {
|
|
248
|
+
files.push(fullPath);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|