@henols/c64-re-tools 0.2.1 → 0.2.2
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/README.md +1 -1
- package/THIRD-PARTY-NOTICES.md +26 -0
- package/bin/cli.mjs +18 -7
- package/package.json +6 -4
- package/skills/acme-build/SKILL.md +51 -17
- package/skills/c64-memory-mapping/SKILL.md +409 -20
- package/skills/c64-program-recon/SKILL.md +437 -86
- package/skills/c64-program-recon/references/reconstruction.md +10 -4
- package/skills/c64-program-recon/references/tool-selection.md +2 -2
- package/skills/c64-program-recon/scripts/packer-finding.mjs +631 -0
- package/skills/c64-program-recon/templates/memory-map.template.md +25 -11
- package/skills/c64-provenance-diff/SKILL.md +3 -3
- package/skills/c64-provenance-diff/scripts/diff-images.mjs +3 -0
- package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +14 -4
- package/skills/c64-ram-capture/RELEASES.json.example +17 -0
- package/skills/c64-ram-capture/SKILL.md +35 -2
- package/skills/c64-ram-capture/scripts/project-paths.mjs +1 -1
- package/skills/c64-ram-capture/scripts/watch-loads.mjs +6 -0
- package/skills/routine-queue-walker/SKILL.md +273 -0
- package/skills/vice-wedge-triage/SKILL.md +8 -8
- package/skills/c64-provenance-diff/scripts/diff-images.test.mjs +0 -665
- package/skills/c64-ram-capture/scripts/d64-parse.test.mjs +0 -243
- package/skills/c64-ram-capture/scripts/dump-artifacts.test.mjs +0 -133
- package/skills/c64-ram-capture/scripts/test-corpus.mjs +0 -75
- package/skills/c64-ram-capture/scripts/watch-loads.test.mjs +0 -339
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
// Coverage for the direct .d64 byte parser: the track/sector offset
|
|
2
|
-
// arithmetic, the directory-chain walk (including its loop guard), and the
|
|
3
|
-
// suspicious-entry detector -- against synthetic images built in-test (so the
|
|
4
|
-
// detector is proven to FIRE on a genuine defect, not merely proven silent),
|
|
5
|
-
// plus an optional pass over whatever real .d64 corpus the host project ships.
|
|
6
|
-
// Portable: with no corpus present the real-image checks skip, never fail.
|
|
7
|
-
import { test } from "node:test";
|
|
8
|
-
import assert from "node:assert/strict";
|
|
9
|
-
import { readFileSync, readdirSync, existsSync } from "node:fs";
|
|
10
|
-
import { join, basename } from "node:path";
|
|
11
|
-
|
|
12
|
-
import { sectorsPerTrack, tsToOffset, parseBam, parseDirectory, readImage } from "./d64-parse.mjs";
|
|
13
|
-
import { projectRoot } from "./project-paths.mjs";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
// ------------------------------------------------------------- tsToOffset
|
|
17
|
-
|
|
18
|
-
test("tsToOffset(1, 0) is byte 0", () => {
|
|
19
|
-
assert.equal(tsToOffset(1, 0), 0);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test("tsToOffset(18, 0) is the BAM's offset -- sum of tracks 1-17's 21 sectors each", () => {
|
|
23
|
-
assert.equal(tsToOffset(18, 0), 17 * 21 * 256);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test("tsToOffset throws for a track below 1", () => {
|
|
27
|
-
assert.throws(() => tsToOffset(0, 0), /track 0 out of range/);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test("tsToOffset throws for a track above 35", () => {
|
|
31
|
-
assert.throws(() => tsToOffset(36, 0), /track 36 out of range/);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("tsToOffset throws for a sector beyond the count for that track's zone", () => {
|
|
35
|
-
// Track 1 is in the 21-sectors-per-track zone (sectors 0-20).
|
|
36
|
-
assert.throws(() => tsToOffset(1, 21), /sector 21 out of range/);
|
|
37
|
-
// Track 30 is in the 18-sectors-per-track zone (sectors 0-17).
|
|
38
|
-
assert.throws(() => tsToOffset(30, 18), /sector 18 out of range/);
|
|
39
|
-
// Track 35 is in the 17-sectors-per-track zone (sectors 0-16).
|
|
40
|
-
assert.throws(() => tsToOffset(35, 17), /sector 17 out of range/);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test("sectorsPerTrack covers all four standard 1541 zones", () => {
|
|
44
|
-
assert.equal(sectorsPerTrack(1), 21);
|
|
45
|
-
assert.equal(sectorsPerTrack(17), 21);
|
|
46
|
-
assert.equal(sectorsPerTrack(18), 19);
|
|
47
|
-
assert.equal(sectorsPerTrack(24), 19);
|
|
48
|
-
assert.equal(sectorsPerTrack(25), 18);
|
|
49
|
-
assert.equal(sectorsPerTrack(30), 18);
|
|
50
|
-
assert.equal(sectorsPerTrack(31), 17);
|
|
51
|
-
assert.equal(sectorsPerTrack(35), 17);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
// -------------------------------------------------------- synthetic image
|
|
55
|
-
|
|
56
|
-
/** A blank, well-formed 35-track image: every BAM track marked fully free. */
|
|
57
|
-
function blankImage() {
|
|
58
|
-
const buf = Buffer.alloc(174848, 0);
|
|
59
|
-
const bamOff = tsToOffset(18, 0);
|
|
60
|
-
buf[bamOff] = 18; // first_dir_track
|
|
61
|
-
buf[bamOff + 1] = 1; // first_dir_sector
|
|
62
|
-
buf[bamOff + 2] = 0x41; // dos version 'A'
|
|
63
|
-
for (let t = 1; t <= 35; t++) {
|
|
64
|
-
const eoff = bamOff + 4 + (t - 1) * 4;
|
|
65
|
-
buf[eoff] = sectorsPerTrack(t); // fully free
|
|
66
|
-
buf[eoff + 1] = 0xff;
|
|
67
|
-
buf[eoff + 2] = 0xff;
|
|
68
|
-
buf[eoff + 3] = 0xff; // top 3 bits unused for <=21-sector zones, harmless
|
|
69
|
-
}
|
|
70
|
-
const nameOff = bamOff + 0x90;
|
|
71
|
-
const nameBuf = Buffer.alloc(16, 0xa0);
|
|
72
|
-
Buffer.from("SYNTHETIC", "latin1").copy(nameBuf);
|
|
73
|
-
nameBuf.copy(buf, nameOff);
|
|
74
|
-
return buf;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function writeDirEntry(buf, dirTrack, dirSector, index, { typeByte, firstTrack, firstSector, name, blocks }) {
|
|
78
|
-
const off = tsToOffset(dirTrack, dirSector) + index * 32;
|
|
79
|
-
buf[off + 2] = typeByte;
|
|
80
|
-
buf[off + 3] = firstTrack;
|
|
81
|
-
buf[off + 4] = firstSector;
|
|
82
|
-
const nameBuf = Buffer.alloc(16, 0xa0);
|
|
83
|
-
Buffer.from(name, "latin1").copy(nameBuf);
|
|
84
|
-
nameBuf.copy(buf, off + 5);
|
|
85
|
-
buf[off + 30] = blocks & 0xff;
|
|
86
|
-
buf[off + 31] = (blocks >> 8) & 0xff;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function markTrackOccupied(buf, track, usedSectors) {
|
|
90
|
-
const bamOff = tsToOffset(18, 0);
|
|
91
|
-
const eoff = bamOff + 4 + (track - 1) * 4;
|
|
92
|
-
buf[eoff] = sectorsPerTrack(track) - usedSectors;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
test("parseDirectory: a genuine defect (0 blocks, first T/S into a BAM-free track) IS flagged suspicious", () => {
|
|
96
|
-
const buf = blankImage();
|
|
97
|
-
// Entry 0: a real, valid PRG that actually occupies track 5.
|
|
98
|
-
markTrackOccupied(buf, 5, 5);
|
|
99
|
-
writeDirEntry(buf, 18, 1, 0, { typeByte: 0x82, firstTrack: 5, firstSector: 0, name: "REAL FILE", blocks: 5 });
|
|
100
|
-
// Chain the real file's own 5 sectors so the terminator is well-formed
|
|
101
|
-
// (not load-bearing for this test, but keeps the fixture honest).
|
|
102
|
-
for (let s = 0; s < 5; s++) {
|
|
103
|
-
const off = tsToOffset(5, s);
|
|
104
|
-
if (s < 4) { buf[off] = 5; buf[off + 1] = s + 1; } else { buf[off] = 0; buf[off + 1] = 0; }
|
|
105
|
-
}
|
|
106
|
-
// Entry 1: the faked entry -- claims track 6, which the BAM still reports
|
|
107
|
-
// entirely free, and a 0 block count.
|
|
108
|
-
writeDirEntry(buf, 18, 1, 1, { typeByte: 0x82, firstTrack: 6, firstSector: 0, name: "FAKE ENTRY", blocks: 0 });
|
|
109
|
-
|
|
110
|
-
const { entries, chain_error } = parseDirectory(buf);
|
|
111
|
-
assert.equal(chain_error, null);
|
|
112
|
-
assert.equal(entries.length, 2);
|
|
113
|
-
const real = entries.find((e) => e.name.startsWith("REAL FILE"));
|
|
114
|
-
const fake = entries.find((e) => e.name.startsWith("FAKE ENTRY"));
|
|
115
|
-
assert.equal(real.suspicious, false, "a genuinely-allocated file must not be flagged");
|
|
116
|
-
assert.equal(fake.suspicious, true, "0 blocks pointing into a BAM-free track must be flagged");
|
|
117
|
-
assert.ok(fake.suspicious_reasons.some((r) => r.includes("block count is 0")));
|
|
118
|
-
assert.ok(fake.suspicious_reasons.some((r) => r.includes("entirely free")));
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test("parseDirectory: first track/sector outside the image is flagged with its own reason", () => {
|
|
122
|
-
const buf = blankImage();
|
|
123
|
-
writeDirEntry(buf, 18, 1, 0, { typeByte: 0x82, firstTrack: 40, firstSector: 0, name: "OUT OF RANGE", blocks: 12 });
|
|
124
|
-
const { entries } = parseDirectory(buf);
|
|
125
|
-
assert.equal(entries.length, 1);
|
|
126
|
-
assert.equal(entries[0].suspicious, true);
|
|
127
|
-
assert.ok(entries[0].suspicious_reasons[0].includes("outside the image"));
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
test("parseDirectory: an unused directory slot (blank name, type 0) is not listed as an entry", () => {
|
|
131
|
-
const buf = blankImage(); // no entries written at all
|
|
132
|
-
const { entries } = parseDirectory(buf);
|
|
133
|
-
assert.equal(entries.length, 0);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test("parseDirectory: terminates on a self-referential next-sector pointer without looping", () => {
|
|
137
|
-
const buf = blankImage();
|
|
138
|
-
const off = tsToOffset(18, 1);
|
|
139
|
-
buf[off] = 18; // next track: itself
|
|
140
|
-
buf[off + 1] = 1; // next sector: itself
|
|
141
|
-
const { entries, chain_error } = parseDirectory(buf);
|
|
142
|
-
assert.equal(entries.length, 0);
|
|
143
|
-
assert.match(chain_error, /revisited 18\/1/);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test("parseDirectory: terminates on a next-sector pointer that cycles back two hops later", () => {
|
|
147
|
-
const buf = blankImage();
|
|
148
|
-
const s1 = tsToOffset(18, 1);
|
|
149
|
-
buf[s1] = 18; buf[s1 + 1] = 2; // 18/1 -> 18/2
|
|
150
|
-
const s2 = tsToOffset(18, 2);
|
|
151
|
-
buf[s2] = 18; buf[s2 + 1] = 1; // 18/2 -> 18/1 (cycle)
|
|
152
|
-
const { chain_error } = parseDirectory(buf);
|
|
153
|
-
assert.match(chain_error, /revisited 18\/1/);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
test("parseBam: reports free-sector counts and derives occupied ranges", () => {
|
|
157
|
-
const buf = blankImage();
|
|
158
|
-
markTrackOccupied(buf, 5, 21);
|
|
159
|
-
markTrackOccupied(buf, 6, 10);
|
|
160
|
-
const bam = parseBam(buf);
|
|
161
|
-
assert.equal(bam.disk_name, "SYNTHETIC");
|
|
162
|
-
assert.deepEqual(bam.occupied_tracks, [5, 6]);
|
|
163
|
-
assert.deepEqual(bam.occupied_ranges, [{ start: 5, end: 6 }]);
|
|
164
|
-
const t5 = bam.per_track.find((t) => t.track === 5);
|
|
165
|
-
assert.equal(t5.free, 0);
|
|
166
|
-
const t7 = bam.per_track.find((t) => t.track === 7);
|
|
167
|
-
assert.equal(t7.free, 21, "an untouched track stays fully free");
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
// ------------------------------------------------------- optional real corpus
|
|
171
|
-
//
|
|
172
|
-
// Everything above runs on synthetic images and passes in any project. What
|
|
173
|
-
// follows exercises the parser against whatever REAL `.d64` images the host
|
|
174
|
-
// project happens to ship, discovered rather than named, and SKIPS when there
|
|
175
|
-
// are none -- so this file never fails just because it was installed somewhere
|
|
176
|
-
// without a disk corpus.
|
|
177
|
-
//
|
|
178
|
-
// These assertions are deliberately properties of the parser, not facts about
|
|
179
|
-
// any particular disk: a specific image's disk name, entry name or block count
|
|
180
|
-
// is that project's evidence and belongs in that project's own records, not
|
|
181
|
-
// hardcoded in a portable test.
|
|
182
|
-
|
|
183
|
-
const CORPUS = (() => {
|
|
184
|
-
const dir = process.env.C64RE_DISKS_DIR ?? join(projectRoot(), "disks");
|
|
185
|
-
if (!existsSync(dir)) return [];
|
|
186
|
-
return readdirSync(dir)
|
|
187
|
-
.filter((f) => f.toLowerCase().endsWith(".d64"))
|
|
188
|
-
.sort()
|
|
189
|
-
.map((f) => join(dir, f));
|
|
190
|
-
})();
|
|
191
|
-
|
|
192
|
-
const noCorpus =
|
|
193
|
-
CORPUS.length === 0
|
|
194
|
-
? "no .d64 images found -- set C64RE_DISKS_DIR to run the real-corpus checks"
|
|
195
|
-
: false;
|
|
196
|
-
|
|
197
|
-
test("real corpus: every image is a standard 174848-byte 35-track image", { skip: noCorpus }, () => {
|
|
198
|
-
for (const path of CORPUS) {
|
|
199
|
-
const buf = readImage(path);
|
|
200
|
-
assert.equal(buf.length, 174848, `${basename(path)} is ${buf.length} bytes, not a plain 35-track image`);
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
test("real corpus: every image's BAM points at a first directory sector and yields occupied ranges", { skip: noCorpus }, () => {
|
|
205
|
-
for (const path of CORPUS) {
|
|
206
|
-
const bam = parseBam(readImage(path));
|
|
207
|
-
assert.equal(bam.first_dir_track, 18, `${basename(path)}: first dir track should be 18 on a 1541 image`);
|
|
208
|
-
assert.ok(bam.first_dir_sector >= 0, `${basename(path)}: first dir sector missing`);
|
|
209
|
-
assert.ok(
|
|
210
|
-
bam.occupied_ranges.length > 0,
|
|
211
|
-
`${basename(path)}: no occupied track ranges derived -- a real image should allocate something`,
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
test("real corpus: every directory chain walks to a clean end, with no loop guard tripped", { skip: noCorpus }, () => {
|
|
217
|
-
for (const path of CORPUS) {
|
|
218
|
-
const { entries, chain_error } = parseDirectory(readImage(path));
|
|
219
|
-
assert.equal(chain_error, null, `${basename(path)}: directory chain failed -- ${chain_error}`);
|
|
220
|
-
assert.ok(entries.length > 0, `${basename(path)}: no directory entries found`);
|
|
221
|
-
}
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
test("real corpus: a suspicious entry always names its reasons, and a clean one never does", { skip: noCorpus }, () => {
|
|
225
|
-
for (const path of CORPUS) {
|
|
226
|
-
const { entries } = parseDirectory(readImage(path));
|
|
227
|
-
for (const e of entries) {
|
|
228
|
-
assert.equal(typeof e.suspicious, "boolean", `${basename(path)}: "${e.name}" has no suspicious flag`);
|
|
229
|
-
if (e.suspicious) {
|
|
230
|
-
assert.ok(
|
|
231
|
-
e.suspicious_reasons.length > 0,
|
|
232
|
-
`${basename(path)}: "${e.name}" is flagged suspicious with no reason given -- a bare boolean is not a finding`,
|
|
233
|
-
);
|
|
234
|
-
} else {
|
|
235
|
-
assert.deepEqual(
|
|
236
|
-
e.suspicious_reasons,
|
|
237
|
-
[],
|
|
238
|
-
`${basename(path)}: "${e.name}" is not suspicious but carries reasons`,
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
});
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
// Coverage for the artifact renderer (01-04 Task 1, Part B): chunk
|
|
2
|
-
// contiguity and the 65536-byte assertion, the gap and overlap refusals,
|
|
3
|
-
// the VIC-bank/screen-base/charset-base derivations against the committed
|
|
4
|
-
// sidecars' own recorded values, and the power-on-pattern run detection
|
|
5
|
-
// that produces `unused` ranges. Runs entirely with no emulator present.
|
|
6
|
-
import { test } from "node:test";
|
|
7
|
-
import assert from "node:assert/strict";
|
|
8
|
-
import { readFileSync } from "node:fs";
|
|
9
|
-
import { fileURLToPath } from "node:url";
|
|
10
|
-
import { dirname, join, resolve } from "node:path";
|
|
11
|
-
|
|
12
|
-
import { assembleImage, sha256Buffer, buildChipState, vicBank, screenBase, buildRangeManifest } from "./dump-artifacts.mjs";
|
|
13
|
-
import { allDumpArtifacts, skipUnless } from "./test-corpus.mjs";
|
|
14
|
-
|
|
15
|
-
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
16
|
-
const REPO_ROOT = resolve(HERE, "..", "..", "..", ".."); // scripts -> skill -> skills -> .claude -> repo root
|
|
17
|
-
|
|
18
|
-
function chunkOf(address, byte, length) {
|
|
19
|
-
// `byte` is a 2-hex-char octet (e.g. "00"); repeating it `length` times
|
|
20
|
-
// yields `length` bytes of hex.
|
|
21
|
-
return { address, hex: byte.repeat(length) };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
test("assembleImage returns a 65536-byte buffer from ordered contiguous chunk records", () => {
|
|
25
|
-
const chunks = [chunkOf(0, "00", 32768), chunkOf(32768, "ff", 32768)];
|
|
26
|
-
const image = assembleImage(chunks);
|
|
27
|
-
assert.equal(image.length, 65536);
|
|
28
|
-
assert.equal(image[0], 0x00);
|
|
29
|
-
assert.equal(image[32768], 0xff);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test("assembleImage accepts chunks out of order and still assembles correctly", () => {
|
|
33
|
-
const chunks = [chunkOf(32768, "ff", 32768), chunkOf(0, "00", 32768)];
|
|
34
|
-
const image = assembleImage(chunks);
|
|
35
|
-
assert.equal(image.length, 65536);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test("assembleImage throws naming the address for a one-byte gap", () => {
|
|
39
|
-
const chunks = [chunkOf(0, "00", 32768), chunkOf(32769, "ff", 32767)];
|
|
40
|
-
assert.throws(() => assembleImage(chunks), /gap before address \$8000/);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test("assembleImage throws naming the address for a one-byte overlap", () => {
|
|
44
|
-
const chunks = [chunkOf(0, "00", 32769), chunkOf(32768, "ff", 32768)];
|
|
45
|
-
assert.throws(() => assembleImage(chunks), /overlap at address \$8000/);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test("assembleImage throws when the total is not exactly 65536 bytes", () => {
|
|
49
|
-
const chunks = [chunkOf(0, "00", 100)];
|
|
50
|
-
assert.throws(() => assembleImage(chunks), /expected exactly 65536/);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test("sha256Buffer hashes with node:crypto and is deterministic", () => {
|
|
54
|
-
const buf = Buffer.from("hello", "utf8");
|
|
55
|
-
const a = sha256Buffer(buf);
|
|
56
|
-
const b = sha256Buffer(buf);
|
|
57
|
-
assert.equal(a, b);
|
|
58
|
-
assert.equal(a.length, 64);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("vicBank derives the bank from the two low bits of $DD00, inverted per the memmap table", () => {
|
|
62
|
-
assert.equal(vicBank(0b00), 3);
|
|
63
|
-
assert.equal(vicBank(0b01), 2);
|
|
64
|
-
assert.equal(vicBank(0b10), 1);
|
|
65
|
-
assert.equal(vicBank(0b11), 0);
|
|
66
|
-
assert.equal(vicBank(193), 2); // real committed dd00_raw
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test("screenBase derives the screen pointer from $D018 bits 4-7 relative to the VIC bank base", () => {
|
|
70
|
-
assert.equal(screenBase(49, 193), 35840); // real committed values
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
// Corpus-driven: re-derive vic_bank / screen_base / charset_base from EVERY
|
|
74
|
-
// committed chip-state sidecar the registry names, and require each to match what
|
|
75
|
-
// that sidecar recorded. Running it over every release is what proves the formula
|
|
76
|
-
// is generic rather than tuned to one machine state -- and with no corpus it
|
|
77
|
-
// skips instead of failing.
|
|
78
|
-
const CHIP_STATES = allDumpArtifacts("chip_state");
|
|
79
|
-
|
|
80
|
-
test("buildChipState reproduces every committed sidecar's recorded vic_bank, screen_base and charset_base",
|
|
81
|
-
{ skip: skipUnless(CHIP_STATES, "committed chip-state sidecars") }, () => {
|
|
82
|
-
for (const { release, label, path } of CHIP_STATES) {
|
|
83
|
-
const committed = JSON.parse(readFileSync(path, "utf8"));
|
|
84
|
-
if (!committed.derived) continue;
|
|
85
|
-
const raw = {
|
|
86
|
-
dd00_raw: committed.derived.dd00_raw,
|
|
87
|
-
d018_raw: committed.derived.d018_raw,
|
|
88
|
-
port01_raw: committed.derived.port01.raw,
|
|
89
|
-
sprite_pointers: committed.derived.sprite_pointers,
|
|
90
|
-
};
|
|
91
|
-
const result = buildChipState(raw);
|
|
92
|
-
const where = `${release}/${label}`;
|
|
93
|
-
assert.equal(result.derived.vic_bank, committed.derived.vic_bank, `${where}: vic_bank`);
|
|
94
|
-
assert.equal(result.derived.screen_base, committed.derived.screen_base, `${where}: screen_base`);
|
|
95
|
-
assert.equal(result.derived.charset_base, committed.derived.charset_base, `${where}: charset_base`);
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
test("buildRangeManifest marks a contiguous power-on-pattern run of at least 16 bytes as kind unused", () => {
|
|
100
|
-
const image = Buffer.alloc(65536, 0xaa);
|
|
101
|
-
// A genuine 20-byte run of $00 in the middle, well clear of the I/O window.
|
|
102
|
-
image.fill(0x00, 4096, 4116);
|
|
103
|
-
const manifest = buildRangeManifest(image, { release: "fake", label: "run1" });
|
|
104
|
-
const hit = manifest.ranges.find((r) => r.start === 4096);
|
|
105
|
-
assert.ok(hit, "expected a range starting at the pattern run");
|
|
106
|
-
assert.equal(hit.kind, "unused");
|
|
107
|
-
assert.equal(hit.end, 4115);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
test("buildRangeManifest marks the I/O window $D000-$DFFF as kind io and sets classification_state to ranges-only", () => {
|
|
111
|
-
const image = Buffer.alloc(65536, 0xaa);
|
|
112
|
-
const manifest = buildRangeManifest(image, { release: "fake", label: "run1" });
|
|
113
|
-
assert.equal(manifest.classification_state, "ranges-only");
|
|
114
|
-
const io = manifest.ranges.find((r) => r.start === 0xd000);
|
|
115
|
-
assert.ok(io);
|
|
116
|
-
assert.equal(io.kind, "io");
|
|
117
|
-
assert.equal(io.end, 0xdfff);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test("buildRangeManifest throws when the image is not exactly 65536 bytes", () => {
|
|
121
|
-
assert.throws(() => buildRangeManifest(Buffer.alloc(100), {}), /exactly 65536 bytes/);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test("buildRangeManifest's ranges union covers $0000-$FFFF with no gap and no overlap on a synthetic image", () => {
|
|
125
|
-
const image = Buffer.alloc(65536, 0x11);
|
|
126
|
-
const manifest = buildRangeManifest(image, { release: "fake", label: "run1" });
|
|
127
|
-
let expected = 0;
|
|
128
|
-
for (const r of manifest.ranges) {
|
|
129
|
-
assert.equal(r.start, expected);
|
|
130
|
-
expected = r.end + 1;
|
|
131
|
-
}
|
|
132
|
-
assert.equal(expected, 65536);
|
|
133
|
-
});
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
// Locates committed artifacts through the registry, so tests exercise whatever
|
|
2
|
-
// corpus the host project actually has instead of naming one project's files.
|
|
3
|
-
//
|
|
4
|
-
// The point is portability without losing coverage: a project with captures gets
|
|
5
|
-
// the real-artifact assertions; a project with none gets them skipped, not
|
|
6
|
-
// failed. Hardcoding `recovery/<some-release>/dumps/<some-label>.state.json`
|
|
7
|
-
// meant this toolkit's tests could only ever pass in the repo they were written
|
|
8
|
-
// in, which is the opposite of shippable.
|
|
9
|
-
//
|
|
10
|
-
// Test-support only. Pure filesystem reads; contacts nothing.
|
|
11
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
12
|
-
import { join } from "node:path";
|
|
13
|
-
|
|
14
|
-
import { projectRoot } from "./project-paths.mjs";
|
|
15
|
-
import { loadRegistry } from "./releases.mjs";
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* First dump in the registry whose `field` names a file that exists, as
|
|
19
|
-
* `{ release, label, path }` -- or null when the registry is absent, empty, or
|
|
20
|
-
* names nothing on disk. Never throws: a missing registry is a skip, not a
|
|
21
|
-
* failure.
|
|
22
|
-
*/
|
|
23
|
-
export function firstDumpArtifact(field) {
|
|
24
|
-
let reg;
|
|
25
|
-
try {
|
|
26
|
-
reg = loadRegistry();
|
|
27
|
-
} catch {
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
for (const r of reg.releases ?? []) {
|
|
31
|
-
for (const d of r.dumps ?? []) {
|
|
32
|
-
const value = d[field];
|
|
33
|
-
if (!value) continue;
|
|
34
|
-
const path = join(projectRoot(), value);
|
|
35
|
-
if (existsSync(path)) return { release: r.id, label: d.label, path };
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Every dump in the registry whose `field` names an existing file. */
|
|
42
|
-
export function allDumpArtifacts(field) {
|
|
43
|
-
let reg;
|
|
44
|
-
try {
|
|
45
|
-
reg = loadRegistry();
|
|
46
|
-
} catch {
|
|
47
|
-
return [];
|
|
48
|
-
}
|
|
49
|
-
const out = [];
|
|
50
|
-
for (const r of reg.releases ?? []) {
|
|
51
|
-
for (const d of r.dumps ?? []) {
|
|
52
|
-
const value = d[field];
|
|
53
|
-
if (!value) continue;
|
|
54
|
-
const path = join(projectRoot(), value);
|
|
55
|
-
if (existsSync(path)) out.push({ release: r.id, label: d.label, path });
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return out;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Parsed JSON for a `firstDumpArtifact` hit, or null. */
|
|
62
|
-
export function readJsonArtifact(field) {
|
|
63
|
-
const hit = firstDumpArtifact(field);
|
|
64
|
-
if (!hit) return null;
|
|
65
|
-
return { ...hit, json: JSON.parse(readFileSync(hit.path, "utf8")) };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* node:test `skip` value: `false` to run, or a human-readable reason string.
|
|
70
|
-
* Pass the thing you looked for so a skipped run explains itself.
|
|
71
|
-
*/
|
|
72
|
-
export function skipUnless(found, what) {
|
|
73
|
-
if (found && (!Array.isArray(found) || found.length > 0)) return false;
|
|
74
|
-
return `no ${what} found via this project's registry -- corpus-dependent check skipped`;
|
|
75
|
-
}
|