返回 CodeWhale
public-copy.test.ts
根目录 / web / lib / public-copy.test.ts
1 import { existsSync, readFileSync } from "node:fs";
2 import { describe, expect, it } from "vitest";
3 import { FACTS } from "./facts.generated";
4 import { RELEASE_CONTRIBUTORS, RELEASE_HELPERS } from "./release-credits";
5 import { EN_CHROME, EN_HOME, getChrome, getHome } from "./i18n/dictionaries";
6
7 function pageSource(path: string): string {
8 return readFileSync(new URL(`../app/[locale]/${path}`, import.meta.url), "utf8");
9 }
10
11 describe("public website copy contracts", () => {
12 it("keeps the docs hub on the compact ocean portal instead of the old almanac treatment", () => {
13 const layout = pageSource("docs/layout.tsx");
14 const search = readFileSync(new URL("../components/docs-search.tsx", import.meta.url), "utf8");
15
16 expect(layout).toContain("docs-portal-hero");
17 expect(layout).toContain("Find the guidance you need.");
18 expect(layout).not.toContain("Section 02");
19 expect(layout).not.toContain("How Codewhale works: ego");
20 expect(layout).not.toContain("<Seal");
21 expect(layout.indexOf('<article className="docs-content')).toBeLessThan(
22 layout.indexOf("<DocsSidebar"),
23 );
24 expect(search).toContain("docs-topic-row");
25 expect(search).not.toContain("40+ Markdown documents");
26 });
27
28 it("keeps unreleased managed-product surfaces out of public copy", () => {
29 const roadmap = pageSource("roadmap/page.tsx");
30 const footer = readFileSync(new URL("../components/footer.tsx", import.meta.url), "utf8");
31
32 expect(roadmap).toContain("Required account for the local runtime");
33 expect(roadmap).not.toContain("Managed app preview");
34 expect(roadmap).not.toContain("Hosted SaaS dashboard");
35 expect(roadmap).not.toContain("Required login / accounts");
36 expect(footer).not.toContain("App preview");
37 expect(footer).not.toContain("app.codewhale.net");
38 expect(footer).not.toMatch(/Create account|Sign up/);
39
40 // Footer copy is dictionary-driven now, so the same ban has to hold
41 // wherever the strings actually live — in every locale, not just the TSX.
42 for (const locale of ["en", "zh", "ja", "vi", "ko", "ru", "uk", "es", "pt-BR", "id"]) {
43 const values = Object.values(getChrome(locale)).join("\n");
44 expect(values, `${locale} chrome`).not.toContain("app.codewhale.net");
45 expect(values, `${locale} chrome`).not.toMatch(/Create account|Sign up|App preview/);
46 }
47 expect(EN_CHROME.footerLicense).toBe("MIT license");
48 });
49
50 it("describes ACP and the VS Code extension at their implemented capability level", () => {
51 const runtime = pageSource("runtime/page.tsx");
52 const sourceDocTargets = [
53 ...new Set(
54 [...runtime.matchAll(/REPO_BLOB_BASE}\/([^`]+)`/g)].map((match) => match[1]),
55 ),
56 ];
57
58 expect(runtime).toContain("ACP (Agent Client Protocol)");
59 expect(runtime).toContain("Baseline JSON-RPC adapter over stdio");
60 expect(runtime).toContain("Phase 0 companion for the local runtime");
61 expect(runtime).not.toContain("Agent Communication Protocol");
62 expect(runtime).not.toContain("IETF-standard");
63 expect(runtime).not.toContain("embeds Codewhale as a side-panel agent");
64 expect(runtime).not.toMatch(/\/(?:en|zh)\/docs#(?:runtime-api|acp|mcp)/);
65 expect(runtime).toContain("docs/RUNTIME_API.md");
66 expect(runtime).toContain("docs/MCP.md");
67 expect(sourceDocTargets).toEqual(["docs/RUNTIME_API.md", "docs/MCP.md"]);
68 for (const target of sourceDocTargets) {
69 expect(existsSync(new URL(`../../${target}`, import.meta.url)), target).toBe(true);
70 }
71 });
72
73 it("uses the current modes, permission postures, and key guidance", () => {
74 const modes = pageSource("docs/modes/page.tsx");
75 const install = pageSource("install/page.tsx");
76 const faq = pageSource("faq/page.tsx");
77 const modeCopy = `${modes}\n${install}\n${faq}`;
78
79 expect(modeCopy).not.toMatch(/\bAgent mode\b|Agent 模式|\bYOLO\b|suggest\s*\/\s*auto\s*\/\s*never|approval_mode|审批模式(建议/);
80 for (const label of ["Plan", "Act", "Operate", "Ask", "Auto-Review", "Full Access"]) {
81 expect(modes).toContain(label);
82 expect(install).toContain(label);
83 }
84 expect(modes).toContain("/mode act");
85 expect(modes).toContain("Shift+Tab");
86 expect(modes).toContain("Plan is always Read Only");
87 expect(modes).toContain("same permission posture, sandbox, and safety rules as Act");
88 expect(faq).toContain("delegation is not mandatory");
89 expect(modeCopy).not.toContain("executable work is dispatched to background Fleet workers");
90 expect(install).toContain("New sessions use your selected default mode (Act unless you changed it)");
91 expect(install).toContain("新会话使用你选择的默认模式(未修改则为 Act)");
92 expect(install).not.toContain("新会话默认以 Act 模式打开");
93 expect(install).toContain("GETTING_STARTED_STEPS");
94 expect(install).not.toContain("① Get an API key");
95 expect(install).not.toContain('href={isZh ? "/zh/faq" : "/faq"}');
96 expect(install).not.toContain('href={isZh ? "/zh/roadmap" : "/roadmap"}');
97 });
98
99 it("keeps source-candidate facts separate from published install facts", () => {
100 const homepage = pageSource("page.tsx");
101 const install = pageSource("install/page.tsx");
102 const community = pageSource("community/page.tsx");
103
104 expect(homepage).toContain("facts.latestPublishedRelease");
105 // The machine-readable source-state attribute stays a literal.
106 expect(homepage).toContain('data-source-state={sourceIsPublished ? "published release" : "source candidate"}');
107 expect(homepage).toContain("publishedRelease.url");
108 // The visible wording moved into the dictionary layer (#4934). Assert the
109 // rendered contract — the EN reference value plus the page's use of it —
110 // instead of a raw TSX string, and hold every locale to a real value.
111 expect(homepage).toContain("d.sourceCandidate");
112 expect(homepage).toContain("d.currentSource");
113 expect(EN_HOME.sourceCandidate).toBe("Source candidate");
114 expect(EN_HOME.currentSource).toBe("Current source");
115 expect(homepage).toContain("fill(d.providerRoutes, { count: providerCount })");
116 expect(EN_HOME.providerRoutes).toBe("{count} provider routes");
117 for (const locale of ["zh", "ja", "ru", "pt-BR"]) {
118 expect(getHome(locale).providerRoutes, `${locale} providerRoutes`).toContain("{count}");
119 expect(getHome(locale).sourceCandidate.trim().length).toBeGreaterThan(0);
120 }
121 expect(homepage).not.toContain("releases/tag/v${version}");
122 expect(homepage).not.toMatch(/Codewhale v0\.9\.1|\"v0\.9\.1 \u00b7/);
123 expect(install).toContain("publishedRelease.tag");
124 expect(install).not.toContain('"v0.8.x"');
125 expect(install).not.toContain("cnbInstall(facts.version");
126 expect(community).toContain("candidate credit");
127 });
128
129 it("presents providers as peers and puts contributor actions near the top", () => {
130 const providerCopy = `${pageSource("models/page.tsx")}\n${pageSource("faq/page.tsx")}`;
131 const community = pageSource("community/page.tsx");
132
133 expect(providerCopy).not.toMatch(/first-class|一级支持|一级模型/);
134 expect(community).toContain("International open-source community");
135 expect(community).toContain("issues/new/choose");
136 expect(community).toContain("docs/LOCALIZATION.md");
137 expect(community).toContain("Hmbown/CodeWhale/pulls");
138 expect(community).toContain("keeps the weekly archive of repository activity");
139 expect(community).not.toContain("latest one sits near the top");
140 expect(community).not.toContain("<Ticker");
141 expect(community).not.toContain("<StatGrid");
142 expect(community).not.toContain("Today's dispatch");
143 });
144
145 it("keeps current-release website credits in exact changelog parity", () => {
146 expect(FACTS.version).toBeTruthy();
147 const changelog = readFileSync(new URL("../../CHANGELOG.md", import.meta.url), "utf8");
148 const release = changelog
149 .split(`## [${FACTS.version}]`)[1]
150 ?.split("\n## ")[0];
151 const contributorSection = release
152 ?.split("### Contributors")[1]
153 ?.split("\n### ")[0];
154 expect(contributorSection, `missing ${FACTS.version} contributor ledger`).toBeTruthy();
155
156 const changelogHandles = [
157 ...new Set(contributorSection?.match(/@[A-Za-z0-9_-]+/g) ?? []),
158 ].sort();
159 const websiteHandles = [...RELEASE_CONTRIBUTORS, ...RELEASE_HELPERS];
160 const contributorDoc = readFileSync(
161 new URL("../../docs/CONTRIBUTORS.md", import.meta.url),
162 "utf8",
163 );
164 const currentDocBand = contributorDoc
165 .split(`<summary><strong>v${FACTS.version} `)[1]
166 ?.split("</details>")[0];
167 expect(currentDocBand, `missing ${FACTS.version} contributor-doc band`).toBeTruthy();
168 const docHandles = [
169 ...new Set(
170 [...(currentDocBand?.matchAll(/github\.com\/([A-Za-z0-9_-]+)\)/g) ?? [])].map(
171 (match) => `@${match[1]}`,
172 ),
173 ),
174 ].sort();
175
176 expect(new Set(websiteHandles).size, "credit arrays must not overlap or repeat").toBe(
177 websiteHandles.length,
178 );
179 expect([...websiteHandles].sort()).toEqual(changelogHandles);
180 expect(docHandles).toEqual(changelogHandles);
181 });
182 });
183
183 lines TYPESCRIPT