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