返回 CodeWhale
public-surface-contract.test.ts
根目录 / web / lib / public-surface-contract.test.ts
1 import {
2 chmodSync,
3 existsSync,
4 mkdtempSync,
5 readFileSync,
6 rmSync,
7 statSync,
8 writeFileSync,
9 } from "node:fs";
10 import { tmpdir } from "node:os";
11 import { join } from "node:path";
12 import { spawnSync } from "node:child_process";
13 import { describe, expect, it } from "vitest";
14 import { FACTS } from "./facts.generated";
15 import { SNIPPETS, VERIFY } from "./install-binary-snippets";
16 import { getChrome, getHome } from "./i18n/dictionaries";
17 import { footerProjectLinks } from "./i18n/links";
18 import { TERMINAL_SCREENSHOT } from "./media-manifest";
19
20 const root = new URL("../../", import.meta.url);
21
22 type PublicSurfaceMatrix = {
23 schemaVersion: number;
24 product: {
25 name: string;
26 description: string;
27 license: string;
28 terminology: Record<string, string>;
29 };
30 sourceCandidate: {
31 version: string;
32 providerCount: number;
33 toolCount: number;
34 sandboxBackends: string[];
35 };
36 latestPublishedRelease: {
37 tag: string;
38 version: string;
39 publishedAt: string;
40 url: string;
41 };
42 install: {
43 recommended: string;
44 binaries: string[];
45 channels: Record<string, string>;
46 androidTermux: {
47 status: string;
48 npm: string;
49 requiresMatchingPublishedAssets: boolean;
50 sourceBuild: boolean;
51 };
52 };
53 control: {
54 modes: string[];
55 permissionPostures: string[];
56 shortcuts: {
57 mode: { chord: string; when: string };
58 permissionPosture: { chord: string; when: string };
59 };
60 };
61 toolSurface: {
62 defaultActive: string[];
63 schemas: Record<string, string[]>;
64 deferred: Record<string, string[]>;
65 compatibility: {
66 legacyAliases: string;
67 modelVisible: boolean;
68 toolSearchDiscoverable: boolean;
69 };
70 activationCache: {
71 maximumNames: number;
72 maximumSchemaBytes: number;
73 scope: string;
74 };
75 agentConcurrency: {
76 defaultConfigured: number;
77 maximumConfigured: number;
78 maximumAdmitted: number;
79 };
80 };
81 surfaces: { availableInSourceCandidate: string[] };
82 trust: Record<string, string>;
83 repository: {
84 canonical: string;
85 mirrors: string[];
86 creditSources: string[];
87 requiredCandidateCredits: string[];
88 };
89 screenshot: {
90 readme: string;
91 website: string;
92 sourceVersion: string | null;
93 sourceCommit: string | null;
94 terminal: string;
95 capture: string;
96 sources: string[];
97 };
98 [key: string]: unknown;
99 };
100
101 const matrix = JSON.parse(text("docs/public-surface-facts.json")) as PublicSurfaceMatrix;
102
103 function text(path: string): string {
104 return readFileSync(new URL(path, root), "utf8");
105 }
106
107 function bytes(path: string): Buffer {
108 return readFileSync(new URL(path, root));
109 }
110
111 function pngDimensions(image: Buffer): [number, number] {
112 expect(image.subarray(1, 4).toString("ascii")).toBe("PNG");
113 return [image.readUInt32BE(16), image.readUInt32BE(20)];
114 }
115
116 /**
117 * Dimensions of the canonical product screenshot. It is a lossless VP8L WebP,
118 * so the size lives in the 14-bit-packed VP8L header rather than a PNG IHDR;
119 * PNG is still accepted so this keeps working if the asset is ever swapped back.
120 */
121 function imageDimensions(image: Buffer): [number, number] {
122 if (image.subarray(1, 4).toString("ascii") === "PNG") return pngDimensions(image);
123 expect(image.subarray(0, 4).toString("ascii")).toBe("RIFF");
124 expect(image.subarray(8, 12).toString("ascii")).toBe("WEBP");
125 const chunk = image.subarray(12, 16).toString("ascii");
126 expect(chunk, "expected a lossless VP8L screenshot").toBe("VP8L");
127 // VP8L: 1 signature byte, then width-1 and height-1 as 14-bit LE fields.
128 const bits = image.readUInt32LE(21);
129 return [(bits & 0x3fff) + 1, ((bits >> 14) & 0x3fff) + 1];
130 }
131
132 function comparableVersion(value: string): number {
133 const [major = 0, minor = 0, patch = 0] = value.split(".").map((n) => Number.parseInt(n, 10) || 0);
134 return major * 1_000_000 + minor * 1_000 + patch;
135 }
136
137 describe("public surface contracts", () => {
138 it("keeps source-candidate and published-release facts distinct and aligned", () => {
139 expect(matrix.schemaVersion).toBe(2);
140 expect(matrix.sourceCandidate.version).toBe(FACTS.version);
141 expect(matrix.sourceCandidate.providerCount).toBe(FACTS.providers.length);
142 expect(matrix.sourceCandidate.toolCount).toBe(FACTS.toolCount);
143 expect(matrix.sourceCandidate.sandboxBackends).toEqual(FACTS.sandboxBackends);
144 expect({
145 tag: matrix.latestPublishedRelease.tag,
146 version: matrix.latestPublishedRelease.version,
147 publishedAt: matrix.latestPublishedRelease.publishedAt,
148 url: matrix.latestPublishedRelease.url,
149 }).toEqual(FACTS.latestPublishedRelease);
150 // The published release may equal the source candidate: that is the normal
151 // state in the window between shipping vX.Y.Z and opening the next lane.
152 // What must never happen is the site advertising a version that is not
153 // published yet, so assert published <= source candidate rather than
154 // asserting they differ.
155 expect(comparableVersion(matrix.latestPublishedRelease.version)).toBeLessThanOrEqual(
156 comparableVersion(matrix.sourceCandidate.version),
157 );
158 expect(matrix.latestPublishedRelease).not.toHaveProperty("providerCount");
159 expect(matrix.latestPublishedRelease).not.toHaveProperty("toolCount");
160 expect(matrix.surfaces).not.toHaveProperty("stable");
161 expect(matrix.surfaces.availableInSourceCandidate).toContain("Web client");
162 // A capture may identify an exact build or explicitly remain an unversioned
163 // current session. Never infer release provenance from pixels alone.
164 const screenshotSourceVersion = matrix.screenshot.sourceVersion;
165 if (screenshotSourceVersion !== null) {
166 expect(comparableVersion(screenshotSourceVersion)).toBeLessThanOrEqual(
167 comparableVersion(matrix.sourceCandidate.version),
168 );
169 expect(matrix.screenshot.sourceCommit).toMatch(/^[0-9a-f]{40}$/);
170 } else {
171 expect(matrix.screenshot.sourceCommit).toBeNull();
172 expect(matrix.screenshot.capture).toContain("no published-release or exact-candidate claim");
173 }
174 });
175
176 it("backs product and install claims with package and documentation content", () => {
177 const readme = text("README.md");
178 const npmReadme = text("npm/codewhale/README.md");
179 const install = text("docs/INSTALL.md");
180 const changelog = text("CHANGELOG.md");
181 const license = text("LICENSE");
182 const npmArtifacts = text("npm/codewhale/scripts/artifacts.js");
183 const npmPackage = JSON.parse(text("npm/codewhale/package.json")) as {
184 description: string;
185 bin: Record<string, string>;
186 };
187
188 expect(matrix.product.name).toBe("Codewhale");
189 expect(matrix.product.license).toBe("MIT");
190 expect(matrix.product.description).toBe(npmPackage.description);
191 expect(license).toContain("MIT License");
192 expect(matrix.install.recommended).toBe("curl -fsSL https://codewhale.net/install.sh | sh");
193 expect(readme).toContain(matrix.install.recommended);
194 expect(Object.keys(npmPackage.bin)).toEqual(matrix.install.binaries);
195 expect(matrix.install.channels).toEqual({
196 npm: "published releases only",
197 cargo: "published crates only",
198 prebuiltArchives: "published GitHub Releases only",
199 cnbMirror: "documented targets only",
200 });
201 expect(matrix.install.androidTermux).toEqual({
202 status: "preview",
203 npm: "preview",
204 requiresMatchingPublishedAssets: true,
205 sourceBuild: true,
206 });
207 // Pinned to FACTS.version, not a literal: these assertions used to carry
208 // the version number by hand, so every release bump broke them and the
209 // failure looked like a copy defect rather than a stale test.
210 expect(install).toContain(`v${FACTS.version} source candidate`);
211 expect(install).toContain("unpublished source candidate");
212 const androidRow = install.split("\n").find((line) => line.startsWith("| Android / Termux |"));
213 expect(androidRow).toContain("arm64 (aarch64)");
214 expect(androidRow).toContain("`codewhale-android-arm64.tar.gz`");
215 expect(androidRow).toContain("⚠️⁴ preview");
216 expect(install).not.toContain(`wrapper is published at\nv${FACTS.version}`);
217 expect(npmReadme).toMatch(/^- Android arm64 \/ Termux \(preview;/m);
218 expect(npmReadme).toContain("requires matching Android assets");
219 expect(npmArtifacts).toContain("android: {");
220 for (const binary of ["codewhale-android-arm64", "codew-android-arm64"]) {
221 expect(npmArtifacts).toContain(binary);
222 }
223 // Matched by string prefix rather than by building a RegExp from
224 // FACTS.version: escaping only `.` left backslashes unescaped, which
225 // CodeQL flagged as incomplete escaping. The version needs no regex.
226 const heading = `## [${FACTS.version}] - `;
227 const headingLine = changelog
228 .split("\n")
229 .find((line) => line.startsWith(heading));
230 expect(headingLine, `missing "${heading}" changelog heading`).toBeTruthy();
231 const headingSuffix = headingLine?.slice(heading.length) ?? "";
232 expect(headingSuffix).toMatch(/^(?:Unreleased candidate|\d{4}-\d{2}-\d{2})$/);
233 if (headingSuffix === "Unreleased candidate") {
234 // Pre-tag candidate: notes still call it a source candidate, and the
235 // version compare link must not claim a tagged endpoint yet.
236 expect(changelog).toContain(`v${FACTS.version} source candidate`);
237 expect(changelog).not.toContain(`compare/v${FACTS.version}...HEAD`);
238 } else {
239 // Dated release: intro names the version, and the version's compare
240 // link points at a tag range (Unreleased may still use ...HEAD).
241 expect(changelog).toContain(`Codewhale v${FACTS.version}`);
242 const versionCompare = changelog
243 .split("\n")
244 .find((line) => line.startsWith(`[${FACTS.version}]: `));
245 expect(versionCompare, `missing [${FACTS.version}] compare link`).toBeTruthy();
246 expect(versionCompare).toContain(`...v${FACTS.version}`);
247 expect(versionCompare).not.toContain("...HEAD");
248 }
249 });
250
251 it("keeps one runtime and channel-specific command names exact", () => {
252 const installDoc = text("docs/INSTALL.md");
253 const installPage = text("web/app/[locale]/install/page.tsx");
254 const npmReadme = text("npm/codewhale/README.md");
255 const cliCargo = text("crates/cli/Cargo.toml");
256 const cargoBinaryNames = Array.from(
257 cliCargo.matchAll(/\[\[bin\]\]\s+name = "([^"]+)"/g),
258 (match) => match[1],
259 );
260
261 expect(matrix.install.binaries).toEqual(["codewhale", "codew"]);
262 expect(cargoBinaryNames).toEqual(["codewhale"]);
263 expect(installDoc).toContain("One Cargo package is required");
264 expect(installDoc).toContain("`codewhale-cli` installs the `codewhale` command");
265 expect(installDoc).toContain("Cargo does\nnot create that alias");
266 expect(installPage).toContain("# Install the compiled runtime as codewhale");
267 expect(installPage).not.toContain("codewhale-tui");
268 expect(npmReadme).toContain("installs `codewhale` plus the `codew` convenience name");
269 expect(npmReadme).not.toContain("codewhale-tui");
270 for (const platform of [
271 "macos-arm64",
272 "macos-x64",
273 "linux-arm64",
274 "linux-x64",
275 ] as const) {
276 expect(SNIPPETS[platform], platform).toContain(`codew-${platform}`);
277 expect(SNIPPETS[platform], platform).toContain(
278 `sudo mv codew-${platform} /usr/local/bin/codew`,
279 );
280 expect(SNIPPETS[platform], platform).not.toContain("codewhale-tui");
281 expect(VERIFY[platform], platform).not.toContain("codewhale-tui");
282 }
283 for (const arch of ["x64", "arm64"] as const) {
284 expect(SNIPPETS[`windows-${arch}`], arch).toContain(`codew-windows-${arch}.exe`);
285 expect(SNIPPETS[`windows-${arch}`], arch).toContain(
286 'Get-FileHash "$dest\\codew.exe"',
287 );
288 expect(SNIPPETS[`windows-${arch}`], arch).not.toContain("codewhale-tui");
289 expect(VERIFY[`windows-${arch}`], arch).not.toContain("codewhale-tui");
290 }
291 });
292
293 it("checks Unix release assets under their manifest filenames before renaming", () => {
294 const scratch = mkdtempSync(join(tmpdir(), "codewhale-install-checksum-"));
295 const mockBin = join(scratch, "bin");
296 const curlPath = join(mockBin, "curl");
297 const checksumPath = join(mockBin, "checksum");
298
299 try {
300 const mkdir = spawnSync("/bin/mkdir", ["-p", mockBin]);
301 expect(mkdir.status, mkdir.stderr.toString()).toBe(0);
302
303 writeFileSync(
304 curlPath,
305 `#!/bin/sh
306 output=""
307 url=""
308 while [ "$#" -gt 0 ]; do
309 case "$1" in
310 -o) shift; output="$1" ;;
311 http*) url="$1" ;;
312 esac
313 shift
314 done
315 [ -n "$output" ] || output=$(basename "$url")
316 if [ "$output" = codewhale-artifacts-sha256.txt ]; then
317 for platform in macos-arm64 macos-x64 linux-arm64 linux-x64; do
318 for binary in codewhale codew; do
319 printf 'fixture-hash %s-%s\\n' "$binary" "$platform"
320 done
321 done > "$output"
322 else
323 printf 'fixture payload for %s\\n' "$output" > "$output"
324 fi
325 `,
326 );
327 writeFileSync(
328 checksumPath,
329 `#!/bin/sh
330 while [ "$#" -gt 0 ]; do shift; done
331 while read -r _hash filename; do
332 if [ ! -f "$filename" ]; then
333 echo "manifest target missing: $filename" >&2
334 exit 1
335 fi
336 done
337 `,
338 );
339 chmodSync(curlPath, 0o755);
340 chmodSync(checksumPath, 0o755);
341 for (const command of ["shasum", "sha256sum"]) {
342 const link = spawnSync("/bin/ln", ["-s", checksumPath, join(mockBin, command)]);
343 expect(link.status, link.stderr.toString()).toBe(0);
344 }
345
346 for (const platform of [
347 "macos-arm64",
348 "macos-x64",
349 "linux-arm64",
350 "linux-x64",
351 ] as const) {
352 const lines = SNIPPETS[platform].split("\n");
353 const checksumLine = lines.findIndex((line) => line.includes(" -c -"));
354 expect(checksumLine, platform).toBeGreaterThan(-1);
355 const result = spawnSync(
356 "/bin/bash",
357 ["-o", "pipefail", "-eu", "-c", lines.slice(0, checksumLine + 1).join("\n")],
358 {
359 cwd: scratch,
360 env: { ...process.env, PATH: `${mockBin}:${process.env.PATH ?? ""}` },
361 },
362 );
363 expect(result.status, `${platform}: ${result.stderr.toString()}`).toBe(0);
364 }
365 } finally {
366 rmSync(scratch, { recursive: true, force: true });
367 }
368 });
369
370 it("qualifies the resolved audit path and best-effort persistence", () => {
371 const installPage = text("web/app/[locale]/install/page.tsx");
372
373 expect(matrix.trust.audit).toContain("best-effort");
374 expect(matrix.trust.audit).toContain("$CODEWHALE_HOME");
375 // Install links to configuration instead of duplicating its storage map.
376 expect(installPage).toContain("/docs/configuration");
377 expect(installPage).not.toContain(
378 "audit.log credential / approval / elevation audit trail",
379 );
380 });
381
382 it("keeps modes, permission postures, and idle shortcuts exact", () => {
383 const modes = text("docs/MODES.md");
384 const keys = text("docs/KEYBINDINGS.md");
385 const homepage = text("web/app/[locale]/page.tsx");
386 const docsMap = text("web/lib/docs-map.ts");
387 const matrixText = text("docs/public-surface-facts.json");
388
389 expect(matrix.control.modes).toEqual(["Plan", "Work", "Operate"]);
390 expect(matrix.control.permissionPostures).toEqual(["Ask", "Auto-Review", "Full Access"]);
391 // Tab is gated on the composer being EMPTY, not idle
392 // (crates/tui/src/tui/ui.rs:6978 `if !app.input.is_empty() { continue; }`
393 // immediately before `app.cycle_mode()`); Shift+Tab has no composer
394 // precondition at all (ui.rs:6363-6367 gates only on the modal stack).
395 expect(matrix.control.shortcuts).toEqual({
396 mode: { chord: "Tab", when: "composer empty" },
397 permissionPosture: {
398 chord: "Shift+Tab",
399 when: "always (suppressed only under a non-Config modal)",
400 },
401 });
402 for (const label of [...matrix.control.modes, ...matrix.control.permissionPostures]) {
403 expect(modes).toContain(label);
404 expect(homepage).toContain(label);
405 }
406 expect(modes).toContain("when the composer is empty");
407 expect(keys).toContain("When the composer is empty, cycle TUI mode");
408 expect(keys).toContain("`Shift+Tab`");
409 // Detailed key semantics belong to the canonical keybinding/mode docs, not
410 // the concise README or every translation.
411 expect(keys).not.toContain("When the composer is idle");
412 expect(`${modes}\n${homepage}\n${docsMap}`).not.toContain("approval posture");
413 expect(matrixText).not.toContain('"approvalPostures"');
414 });
415
416 it("keeps public tool facts aligned with the native core and discovery boundaries", () => {
417 const toolDoc = text("docs/TOOL_SURFACE.md");
418 const toolsPage = text("web/app/[locale]/docs/tools/page.tsx") + text("web/lib/content/tools.ts");
419 const registry = text("crates/tui/src/tools/registry.rs");
420 const limits = text("crates/tui/src/config/subagent_limits.rs");
421 const roadmap = text("web/app/[locale]/roadmap/page.tsx");
422
423 const catalog = text("crates/tui/src/core/engine/tool_catalog.rs");
424 const nativeCore = catalog.match(/const DEFAULT_ACTIVE_NATIVE_TOOLS: &[\s\S]*?= &\[([\s\S]*?)\];/);
425 expect(nativeCore, "native core declaration must be found").not.toBeNull();
426 const nativeNames = [...nativeCore![1].matchAll(/"([^"\n]+)"/g)].map((match) => match[1]);
427 expect(nativeNames.length).toBeGreaterThan(0);
428 expect(matrix.toolSurface.defaultActive).toEqual(nativeNames);
429 expect(matrix.toolSurface.schemas).toEqual({
430 read: ["path", "offset?", "limit?"],
431 write: ["path", "content"],
432 edit: ["path", "edits"],
433 bash: ["command", "timeout?"],
434 });
435 expect(matrix.toolSurface.deferred).toEqual({ Web: ["search", "fetch", "wait"] });
436 expect(matrix.toolSurface.compatibility).toEqual({
437 legacyAliases: "hidden-exact",
438 modelVisible: false,
439 toolSearchDiscoverable: false,
440 });
441 expect(matrix.toolSurface.activationCache).toEqual({
442 maximumNames: 8,
443 maximumSchemaBytes: 16_384,
444 scope: "per conversation; each subagent owns an independent policy-filtered cache",
445 });
446 expect(matrix.toolSurface.agentConcurrency).toEqual({
447 defaultConfigured: 64,
448 maximumConfigured: 128,
449 maximumAdmitted: 1024,
450 });
451 expect(limits).toContain("DEFAULT_MAX_SUBAGENTS: usize = 64");
452 expect(limits).toContain("MAX_SUBAGENTS: usize = 128");
453 expect(limits).toContain("MAX_SUBAGENT_ADMISSION: usize = 1024");
454 expect(roadmap).toContain("64 concurrent sessions by default, configurable to 128");
455 expect(roadmap.indexOf('{ title: "Local web client"')).toBeLessThan(
456 roadmap.indexOf('title: "Underway"'),
457 );
458 expect(roadmap).toContain("Implemented in the v0.9.1 source candidate");
459 for (const name of matrix.toolSurface.defaultActive) {
460 expect(toolDoc, name).toContain(`\`${name}\``);
461 expect(toolsPage, name).toContain(name);
462 }
463 expect(toolDoc).toContain("`Web` is conditional and deferred");
464 expect(toolDoc).toContain("Each subagent gets its own policy-filtered deferred catalog");
465 expect(toolDoc).toContain("Compatibility is execution compatibility, not fuzzy aliasing");
466 expect(registry).toContain("Arc::new(ReadTool)");
467 expect(registry).toContain("Arc::new(WriteTool)");
468 expect(registry).toContain("Arc::new(EditTool)");
469 expect(registry).toContain("Arc::new(LowercaseBashTool)");
470 expect(registry).toContain('FileTool::new("File")');
471 expect(registry).toContain('BashTool::new("Bash")');
472 expect(toolsPage).toContain("8 names / 16 KiB");
473 expect(toolsPage).toContain("Web search/fetch");
474 expect(toolsPage).not.toContain("docs/TOOL_LIFECYCLE.md");
475 });
476
477 it("states the hosted-provider privacy boundary without a false local-only promise", () => {
478 const faq = text("web/app/[locale]/faq/page.tsx");
479 const roadmap = text("web/app/[locale]/roadmap/page.tsx");
480 const providers = text("docs/PROVIDERS.md");
481 const runtime = text("docs/RUNTIME_API.md");
482
483 expect(matrix.trust.hostedProviderBoundary).toContain("selected hosted provider");
484 expect(matrix.trust.localInference).toContain("loopback local-model route");
485 // The source candidate counts by default and says so. Disclosure alone
486 // is never acceptance, every opt-out stays authoritative, and the
487 // published release's earlier opt-in behavior is named, not blurred.
488 expect(matrix.trust.telemetry).toContain(`Codewhale ${matrix.sourceCandidate.version} counts anonymous usage by default`);
489 expect(matrix.trust.telemetry).toContain("discloses it at first launch");
490 expect(matrix.trust.telemetry).toContain("policy notice version 5");
491 expect(matrix.trust.telemetry).toContain("Codewhale and PostHog");
492 expect(matrix.trust.telemetry).toContain("earlier 0.9.11 release asked first");
493 expect(matrix.trust.telemetry).toContain("never records any acceptance");
494 expect(matrix.trust.telemetry).toContain("opt-out recorded under the earlier opt-in policy stays off");
495 expect(matrix.trust.telemetry).not.toContain("requires explicit consent");
496 expect(matrix.trust.telemetry).toContain("does not collect conversations");
497 expect(matrix.trust.telemetry).toContain("per-turn/per-tool timelines");
498 // The destination is now named, and named exactly — a trust claim that says
499 // "an endpoint" without saying which one is not a trust claim.
500 expect(matrix.trust.telemetry).toContain(
501 "https://telemetry.codewhale.net/v1/telemetry",
502 );
503 expect(matrix.trust.telemetry).toContain("no IP, country, or geo column");
504 expect(matrix.trust.telemetry).toContain("three-month retention");
505 // The local dry-run path stays documented, because it is what lets a user
506 // audit the schema against their own traffic.
507 expect(matrix.trust.telemetry).toContain("local dry-run file");
508 expect(matrix.trust.telemetry).toContain("no mandatory hosted relay");
509 expect(faq).toContain("The hosted");
510 expect(faq).toContain("provider you select receives the prompt");
511 expect(faq).toContain("keep model inference local");
512 expect(faq).toContain("你选择的托管 provider 会收到");
513 expect(faq).not.toContain("No telemetry, no cloud processing of your code");
514 expect(faq).not.toContain("不会将你的代码上传到云端处理");
515 expect(roadmap).not.toContain("what happens there stays there");
516 expect(roadmap).not.toContain("你的数据不会离开");
517 expect(providers).toMatch(/Hosted\s+routes/);
518 expect(runtime).toContain("No hosted relay");
519 });
520
521 it("backs product vocabulary, contributor credit, and the exact MIT footer", () => {
522 const fleet = text("docs/FLEET.md");
523 const changelog = text("CHANGELOG.md");
524 const contributors = text("docs/CONTRIBUTORS.md");
525 const releaseCredits = text("web/lib/release-credits.ts");
526 const footer = text("web/components/footer.tsx");
527
528 expect(matrix.product.terminology).toEqual({
529 Fleet: "the user's model inventory: who is in the roster and which member is selected",
530 Workflow: "what order the work follows",
531 Lane: "one running Workflow instance",
532 Runtime: "where, how, and with what authority selected work executes",
533 });
534 for (const [term, definition] of Object.entries(matrix.product.terminology)) {
535 expect(fleet).toContain(`**${term}** = ${definition}`);
536 }
537 expect(matrix.repository.requiredCandidateCredits).not.toHaveLength(0);
538 expect(matrix.repository.mirrors.some((mirror) => mirror.includes("gitee"))).toBe(false);
539 for (const handle of matrix.repository.requiredCandidateCredits) {
540 expect(changelog).toContain(handle);
541 expect(contributors).toContain(`github.com/${handle.slice(1)}`);
542 expect(releaseCredits).toContain(`"${handle}"`);
543 }
544 // The footer link sets are generated from the locale dictionaries
545 // (lib/i18n/links.ts) rather than hardcoded per-locale arrays, so the
546 // exact MIT pairing is asserted on the rendered contract for both the
547 // English and Chinese editions — same guarantee, one source.
548 expect(footerProjectLinks("en", getChrome("en")).at(-1)).toEqual({
549 label: "MIT license",
550 href: "https://github.com/Hmbown/CodeWhale/blob/main/LICENSE",
551 });
552 expect(footerProjectLinks("zh", getChrome("zh")).at(-1)).toEqual({
553 label: "MIT 许可证",
554 href: "https://github.com/Hmbown/CodeWhale/blob/main/LICENSE",
555 });
556 expect(footer).toContain("href={REPO_RELEASES_URL}");
557 expect(text("web/lib/i18n/links.ts")).toContain(
558 'export const REPO_RELEASES_URL = `${REPO_URL}/releases`',
559 );
560 expect(text("web/lib/i18n/links.ts")).toContain(
561 'export const REPO_URL = "https://github.com/Hmbown/CodeWhale"',
562 );
563 expect(footer).toContain("GITEE_ENABLED &&");
564 });
565
566 it("keeps supplied terminal screenshots and website dimensions truthful", () => {
567 // Website and README share the same exact-build terminal-cell capture.
568 const readmeImage = bytes(matrix.screenshot.readme);
569 const websiteImage = bytes(matrix.screenshot.website);
570
571 expect(imageDimensions(websiteImage)).toEqual([TERMINAL_SCREENSHOT.width, TERMINAL_SCREENSHOT.height]);
572 expect(readmeImage).toEqual(websiteImage);
573 expect(statSync(new URL(matrix.screenshot.readme, root)).size).toBeLessThan(500_000);
574 expect(statSync(new URL(matrix.screenshot.website, root)).size).toBeLessThan(500_000);
575 expect(matrix.screenshot.terminal).toContain("real PTY cell capture");
576 // A development-build capture, never a release claim.
577 expect(matrix.screenshot.capture).toContain("development build");
578 expect(matrix.screenshot.capture).toContain("not a default");
579
580 const readme = text("README.md");
581 const homepage = text("web/app/[locale]/page.tsx");
582 expect(readme).toContain(matrix.screenshot.readme);
583 expect(`web/public${TERMINAL_SCREENSHOT.src}`).toBe(matrix.screenshot.website);
584 expect(imageDimensions(websiteImage)).toEqual([TERMINAL_SCREENSHOT.width, TERMINAL_SCREENSHOT.height]);
585 expect(homepage).toContain("src={TERMINAL_SCREENSHOT.src}");
586 // Every locale describes the actual capture; build identity comes from
587 // the media manifest instead of a stale version embedded in translations.
588 expect(homepage).toContain("alt={fill(d.screenshotAlt, { version: TERMINAL_SCREENSHOT.version })}");
589 expect(homepage).toContain("fill(d.shotBuild, { version: TERMINAL_SCREENSHOT.version })");
590 expect(getHome("en").shotBuild).toBe("v{version} development build");
591 for (const locale of ["en", "zh", "ja", "vi", "ko", "ru", "uk", "es", "pt-BR", "id", "fr", "de", "ca", "hi", "tr", "it", "pl", "ar"]) {
592 const home = getHome(locale);
593 expect(home.shotBuild, `${locale} shotBuild`).toContain("{version}");
594 expect(home.screenshotAlt, `${locale} alt`).toContain("{version}");
595 expect(home.screenshotAlt, `${locale} alt`).toContain("Ask");
596 expect(home.screenshotAlt, `${locale} alt`).toContain("Work");
597 expect(home.screenshotAlt, `${locale} alt`).not.toMatch(/171acee|0\.9\.12|Full Access/);
598 }
599 });
600
601 it("keeps the standalone wire strip a record of GitHub, not a summary of it", () => {
602 const ticker = text("web/components/ticker.tsx");
603 const github = text("web/lib/github.ts");
604
605 // An empty or unreachable feed removes the strip. No skeleton, no
606 // placeholder row, no invented item.
607 expect(ticker).toContain("if (!ordered.length) return null;");
608
609 // Drafts are the author's own not-ready marker, not an event.
610 expect(ticker).toContain("EVENT_STATES.includes(item.state)");
611 expect(ticker).not.toContain('"draft"');
612
613 // Every verb resolves through the caller's dictionary — the strip never
614 // hardcodes an English event word next to a translated page.
615 for (const key of [
616 "tickerMerged",
617 "tickerOpened",
618 "tickerClosed",
619 "tickerReleased",
620 "tickerFirstContribution",
621 "tickerBy",
622 "tickerAria",
623 ] as const) {
624 for (const locale of ["en", "zh", "ja", "vi", "ko", "ru", "uk", "es", "pt-BR", "id"]) {
625 expect(getChrome(locale)[key].trim().length, `${locale} ${key}`).toBeGreaterThan(0);
626 }
627 }
628 expect(getChrome("en").tickerBy).toContain("{handle}");
629
630 // The first-contribution mark is GitHub's verdict, copied, never ours.
631 expect(github).toContain('association === "FIRST_TIME_CONTRIBUTOR"');
632 expect(ticker).toContain("item.firstTimeContributor");
633
634 // A verb is dated by its own event, so a merge is never dated by a later
635 // comment on the thread.
636 expect(github).toContain("eventAt");
637 expect(ticker).toContain("item.eventAt ?? item.updatedAt");
638
639 // Merged pull requests, issues, and releases — the whole life of the repo,
640 // within the existing three-call budget.
641 expect(github).toContain("/releases?per_page=");
642 expect(github).toContain('kind: "release"');
643 });
644
645 it("keeps reduced motion static without hiding the reasoning trace", () => {
646 const css = text("web/app/globals.css");
647
648 expect(css).toMatch(
649 /@media \(prefers-reduced-motion: reduce\)\s*\{[\s\S]*?\.ticker-track\s*\{\s*animation:\s*none;\s*\}[\s\S]*?\}/,
650 );
651 // Freezing the track must not also hide the entries it stopped scrolling.
652 expect(css).toMatch(/\.ticker-viewport\s*\{\s*overflow-x:\s*auto;\s*\}/);
653 });
654
655 it("keeps the homepage free of fabricated demo panels", () => {
656 const homepage = text("web/app/[locale]/page.tsx");
657
658 expect(homepage).not.toContain("TerminalPlayer");
659 expect(homepage).not.toContain("paper-decides");
660 expect(homepage).not.toContain("product-receipt");
661 });
662
663 it("keeps every fact-matrix source resolvable in the repository", () => {
664 const sources = new Set<string>();
665 const visit = (value: unknown) => {
666 if (Array.isArray(value)) {
667 value.forEach(visit);
668 } else if (value && typeof value === "object") {
669 for (const [key, child] of Object.entries(value)) {
670 if (key === "sources" || key === "creditSources") {
671 (child as string[]).forEach((source) => sources.add(source));
672 } else {
673 visit(child);
674 }
675 }
676 }
677 };
678 visit(matrix);
679
680 for (const source of sources) {
681 expect(existsSync(new URL(source, root)), source).toBe(true);
682 }
683 });
684 });
685
685 lines TYPESCRIPT