Skip to content

Commit e76af57

Browse files
committed
[red] require five-board tutorial captures
Signed-off-by: Viwat Vchirawongkwin <viwat.v@chula.ac.th>
1 parent aac251d commit e76af57

3 files changed

Lines changed: 133 additions & 29 deletions

File tree

‎tools/web/scripts/test-process-tutorial-captures.sh‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,17 @@ trap cleanup EXIT HUP INT TERM
2626

2727
command -v magick >/dev/null 2>&1 || fail "ImageMagick's magick is required"
2828
[ -x "$PROCESSOR" ] || fail "processor is not executable: $PROCESSOR"
29+
30+
CAPTURE_TABLE="$($PROCESSOR --list)"
31+
[ "$(printf '%s\n' "$CAPTURE_TABLE" | wc -l | tr -d ' ')" = 18 ] ||
32+
fail "capture table must expose eighteen reviewed derivatives"
33+
[ "$(printf '%s\n' "$CAPTURE_TABLE" | awk -F '\t' 'NF != 6 { bad = 1 } END { print bad + 0 }')" = 0 ] ||
34+
fail "capture rows must name source, key, width, height, crop x, and crop y"
35+
[ "$(printf '%s\n' "$CAPTURE_TABLE" | cut -f1 | sort -u | wc -l | tr -d ' ')" = 13 ] ||
36+
fail "capture table must bind thirteen distinct raw frames"
37+
[ "$(printf '%s\n' "$CAPTURE_TABLE" | cut -f2 | sort -u | wc -l | tr -d ' ')" = 18 ] ||
38+
fail "capture keys must be unique"
39+
2940
mkdir -p "$SCREENSHOTS_ROOT"
3041
TEST_ROOT=$(mktemp -d "$SCREENSHOTS_ROOT/.capture-processor-test.XXXXXX")
3142
INPUT_DIR="$TEST_ROOT/raw"

‎tools/web/src/test/learn-content-contract.test.tsx‎

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ const lessonPages: Array<{
144144
},
145145
];
146146

147-
const physicalTabletLessonPages = lessonPages.slice(0, 6);
147+
const physicalTabletLessonPages = lessonPages;
148148

149149
function mainText(Page: ComponentType): string {
150150
render(<Page />);
@@ -344,16 +344,53 @@ describe("instructional visual contract", () => {
344344

345345
expect(physicalFigures.length).toBeGreaterThan(0);
346346
for (const figure of physicalFigures) {
347-
const image = within(figure).getByRole("img");
348-
expect(image.getAttribute("src")).toMatch(
349-
/^\/learn\/app\/[a-z0-9.-]+\.png$/,
350-
);
351-
expect(image.getAttribute("alt")).toMatch(/PyBLE 0\.2\.0 beta/i);
347+
const images = within(figure).getAllByRole("img");
348+
expect(images.length).toBeGreaterThan(0);
349+
for (const image of images) {
350+
expect(image.getAttribute("src")).toMatch(
351+
/^\/learn\/app\/[a-z0-9.-]+\.png$/,
352+
);
353+
expect(image.getAttribute("alt")).toMatch(/PyBLE 0\.2\.0 beta/i);
354+
}
352355
expect(figure).not.toHaveTextContent(/integration test|debug|golden/i);
353356
}
354357
expect(main).not.toHaveTextContent(/PyBLE Integration Test/i);
355358
},
356359
);
360+
361+
it.each([SetupTutorial, HardwareTutorial])(
362+
"renders the complete five-board observed identity set",
363+
(Page) => {
364+
render(<Page />);
365+
const main = screen.getByRole("main");
366+
const text = main.textContent ?? "";
367+
368+
for (const [boardId, runtimeChip] of [
369+
["5646", "esp32-s3"],
370+
["8C9E", "esp32"],
371+
["C81A", "esp32-c3"],
372+
["DA86", "esp32-s3"],
373+
["3DCB", "rpi-pico2-w"],
374+
] as const) {
375+
expect(text).toContain(boardId);
376+
expect(text).toContain(runtimeChip);
377+
}
378+
},
379+
);
380+
381+
it("uses identity subsets as hardware boundaries, not profile proof", () => {
382+
const configuredText = mainText(ConfiguredHardwareTutorial);
383+
expect(configuredText).toMatch(/C81A.*esp32-c3/is);
384+
385+
const picoText = mainText(Pico2WTutorial);
386+
expect(picoText).toMatch(/3DCB.*rpi-pico2-w/is);
387+
388+
const waveshareText = mainText(WaveshareTutorial);
389+
expect(waveshareText).toMatch(/5646.*DA86/is);
390+
expect(waveshareText).toMatch(
391+
/esp32-s3.*(?:cannot|does not).*distinguish.*profile/is,
392+
);
393+
});
357394
});
358395

359396
describe("tutorial truth and safety content", () => {

‎tools/web/src/test/tutorial-app-capture-assets.test.ts‎

Lines changed: 79 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,58 @@ import { describe, expect, it } from "vitest";
1010
import { tutorialAppCaptures } from "@/lib/tutorial-app-captures";
1111

1212
const expectedCaptureIds = [
13-
"setupScanResults",
14-
"setupConnectedIdentity",
13+
"setupFiveBoardScan",
14+
"identity5646Ready",
15+
"identity5646Chip",
16+
"identity8c9eReady",
17+
"identity8c9eChip",
18+
"identityc81aReady",
19+
"identityc81aChip",
20+
"identityda86Ready",
21+
"identityda86Chip",
22+
"identity3dcbReady",
23+
"identity3dcbChip",
1524
"firstProgramEditorConsole",
1625
"filesMultiDeleteReview",
1726
"githubBranchChooser",
18-
"githubPrewriteReview",
27+
"githubPinnedSource",
28+
"githubTargetReview",
1929
"examplesImportComplete",
2030
"blocksHelloWorkspace",
2131
] as const;
2232

33+
const identityDimensions = {
34+
identity5646Ready: { width: 400, height: 84 },
35+
identity5646Chip: { width: 512, height: 390 },
36+
identity8c9eReady: { width: 400, height: 84 },
37+
identity8c9eChip: { width: 512, height: 390 },
38+
identityc81aReady: { width: 400, height: 84 },
39+
identityc81aChip: { width: 512, height: 390 },
40+
identityda86Ready: { width: 400, height: 84 },
41+
identityda86Chip: { width: 512, height: 390 },
42+
identity3dcbReady: { width: 400, height: 84 },
43+
identity3dcbChip: { width: 512, height: 390 },
44+
} as const;
45+
46+
type CaptureRecord = {
47+
src: string;
48+
alt: string;
49+
width: number;
50+
height: number;
51+
provenance: {
52+
rawCapture: string;
53+
processing: string;
54+
depictedState: string;
55+
review:
56+
| { status: "pending-derivative"; sha256: null }
57+
| { status: "reviewed"; sha256: string };
58+
};
59+
};
60+
61+
const captures = tutorialAppCaptures as unknown as Record<
62+
string,
63+
CaptureRecord | undefined
64+
>;
2365
const pngSignature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
2466
const forbiddenMetadataChunks = new Set([
2567
"eXIf",
@@ -37,21 +79,17 @@ function pngChunkTypes(bytes: Buffer): string[] {
3779
const dataLength = bytes.readUInt32BE(offset);
3880
const chunkEnd = offset + 12 + dataLength;
3981
expect(chunkEnd).toBeLessThanOrEqual(bytes.length);
40-
4182
const chunkType = bytes.toString("ascii", offset + 4, offset + 8);
4283
chunkTypes.push(chunkType);
4384
offset = chunkEnd;
44-
45-
if (chunkType === "IEND") {
46-
break;
47-
}
85+
if (chunkType === "IEND") break;
4886
}
4987

5088
return chunkTypes;
5189
}
5290

5391
describe("reviewed physical tutorial capture assets", () => {
54-
it("keeps the reviewed capture registry complete and unique", () => {
92+
it("keeps the five-board capture registry complete and unique", () => {
5593
expect(Object.keys(tutorialAppCaptures)).toEqual(expectedCaptureIds);
5694
expect(
5795
new Set(Object.values(tutorialAppCaptures).map(({ src }) => src)).size,
@@ -61,24 +99,31 @@ describe("reviewed physical tutorial capture assets", () => {
6199
it.each(expectedCaptureIds)(
62100
"%s binds content-versioned, stripped PNG bytes to reviewed provenance",
63101
async (captureId) => {
64-
const capture = tutorialAppCaptures[captureId];
65-
const { review } = capture.provenance;
102+
const capture = captures[captureId];
103+
expect(capture).toBeDefined();
104+
if (!capture) return;
66105

67-
if (review.status !== "reviewed") {
68-
throw new Error(`${captureId} has not completed derivative review`);
69-
}
106+
const { review } = capture.provenance;
107+
expect(review.status).toBe("reviewed");
108+
if (review.status !== "reviewed") return;
70109

71110
expect(review.sha256).toMatch(/^[0-9a-f]{64}$/);
72111
expect(basename(capture.src)).toContain(review.sha256.slice(0, 12));
73112
expect(capture.src).not.toContain("pending");
74-
expect(capture.provenance.rawCapture).toMatch(
75-
/production-release\.raw\.png$/,
76-
);
113+
expect(capture.provenance.rawCapture).toMatch(/\.raw\.png$/);
77114
expect(capture.provenance.rawCapture).not.toMatch(
78-
/debug|golden|integration/i,
115+
/debug|golden|integration|ipad/i,
79116
);
80117
expect(capture.provenance.processing).toMatch(
81-
/crop.*status.*navigation.*strip metadata.*not retouch/is,
118+
/crop.*strip metadata.*not retouch/is,
119+
);
120+
121+
const expectedDimensions =
122+
captureId in identityDimensions
123+
? identityDimensions[captureId as keyof typeof identityDimensions]
124+
: { width: 2000, height: 1092 };
125+
expect({ width: capture.width, height: capture.height }).toEqual(
126+
expectedDimensions,
82127
);
83128

84129
const asset = await readFile(
@@ -87,10 +132,6 @@ describe("reviewed physical tutorial capture assets", () => {
87132
expect(asset.subarray(0, pngSignature.length)).toEqual(pngSignature);
88133
expect(asset.readUInt32BE(16)).toBe(capture.width);
89134
expect(asset.readUInt32BE(20)).toBe(capture.height);
90-
expect({ width: capture.width, height: capture.height }).toEqual({
91-
width: 2000,
92-
height: 1092,
93-
});
94135
expect(createHash("sha256").update(asset).digest("hex")).toBe(
95136
review.sha256,
96137
);
@@ -105,4 +146,19 @@ describe("reviewed physical tutorial capture assets", () => {
105146
).toEqual([]);
106147
},
107148
);
149+
150+
it("keeps both S3 observations truthful and non-identifying", () => {
151+
expect(captures.identity5646Ready?.provenance.depictedState).toMatch(
152+
/5646.*0\.6\.0/is,
153+
);
154+
expect(captures.identityda86Ready?.provenance.depictedState).toMatch(
155+
/DA86.*0\.6\.0/is,
156+
);
157+
expect(captures.identity5646Chip?.provenance.depictedState).toMatch(
158+
/esp32-s3.*not.*profile/is,
159+
);
160+
expect(captures.identityda86Chip?.provenance.depictedState).toMatch(
161+
/esp32-s3.*not.*profile/is,
162+
);
163+
});
108164
});

0 commit comments

Comments
 (0)