返回 CodeWhale
fleet-public-surface.test.ts
根目录 / web / lib / fleet-public-surface.test.ts
1 /**
2 * Fleet is the canonical public noun; Pod remains a documented compatibility
3 * alias for the CLI and TUI surfaces.
4 *
5 * These deterministic source contracts read the real registry, sitemap,
6 * routes, and repository docs so discovery cannot silently split across nouns.
7 */
8 import { existsSync, readFileSync } from "node:fs";
9 import { describe, expect, it } from "vitest";
10 import buildSitemap from "../app/sitemap";
11 import { DOC_TOPICS, docTopicHref, getTopic } from "./docs-map";
12 import { filterDocTopics } from "./search-utils";
13 import { PRODUCT_TERMS } from "./content/vocabulary";
14 import { GETTING_STARTED_STEPS } from "./content/getting-started";
15 import { buildLlmsTxt } from "./llms-txt";
16 import { SITE_URL } from "./page-meta";
17
18 const webRoot = new URL("../", import.meta.url);
19 const repoRoot = new URL("../../", import.meta.url);
20
21 function webText(path: string): string {
22 return readFileSync(new URL(path, webRoot), "utf8");
23 }
24
25 function repoText(path: string): string {
26 return readFileSync(new URL(path, repoRoot), "utf8");
27 }
28
29 const sitemapEntries = buildSitemap();
30
31 describe("Fleet is the canonical public surface", () => {
32 it("registers exactly one roster topic at /docs/fleet", () => {
33 const fleet = getTopic("fleet");
34 expect(fleet?.hasPage).toBe(true);
35 expect(fleet?.slug).toBe("fleet");
36 expect(fleet?.label.en).toContain("Fleet");
37 expect(fleet?.label.en).not.toContain("Pod");
38 expect(docTopicHref(fleet!, "en")).toBe("/en/docs/fleet");
39 expect(DOC_TOPICS.filter((t) => t.id === "fleet")).toHaveLength(1);
40 expect(DOC_TOPICS.map((t) => t.id)).not.toContain("pod");
41 });
42
43 it("indexes /docs/fleet and keeps /docs/pod out of the sitemap", () => {
44 for (const locale of ["en", "zh"]) {
45 expect(
46 sitemapEntries.some((e) => e.url === `${SITE_URL}/${locale}/docs/fleet`),
47 locale,
48 ).toBe(true);
49 expect(
50 sitemapEntries.some((e) => e.url === `${SITE_URL}/${locale}/docs/pod`),
51 locale,
52 ).toBe(false);
53 }
54 });
55
56 it("serves Fleet at /docs/fleet and permanently redirects /docs/pod", () => {
57 const page = webText("app/[locale]/docs/fleet/page.tsx");
58 const redirect = webText("app/[locale]/docs/pod/page.tsx");
59 expect(page).toContain('path: "/docs/fleet"');
60 expect(page).toContain('import { buildPageMetadata } from "@/lib/page-meta"');
61 expect(redirect).toContain('import { permanentRedirect } from "next/navigation"');
62 expect(redirect).toContain("permanentRedirect(`/${locale}/docs/fleet`)");
63 expect(redirect).not.toContain("buildPageMetadata");
64 expect(existsSync(new URL("app/[locale]/docs/fleet/page.tsx", webRoot))).toBe(true);
65 expect(existsSync(new URL("app/[locale]/docs/pod/page.tsx", webRoot))).toBe(true);
66 });
67
68 it("uses /docs/fleet in the machine-readable index", () => {
69 const llms = buildLlmsTxt();
70 expect(llms).toContain("/docs/fleet");
71 expect(llms).not.toContain("/docs/pod");
72 expect(llms).toContain("Fleet / Workflow");
73 });
74
75 it("resolves docs search on both nouns to the one Fleet page", () => {
76 for (const query of ["fleet", "Fleet", "pod", "Pod"]) {
77 const hits = filterDocTopics(DOC_TOPICS, query).map((i) => DOC_TOPICS[i]);
78 expect(hits.filter((t) => t.id === "fleet"), query).toHaveLength(1);
79 }
80 });
81
82 it("uses Fleet in shared vocabulary and the guided path", () => {
83 expect(PRODUCT_TERMS.map((t) => t.term)).toEqual(["Fleet", "Workflow", "Lane", "Runtime"]);
84 const step = GETTING_STARTED_STEPS.find((s) => s.id === "fleet-workflow");
85 expect(step).toBeTruthy();
86 expect(step!.link.href).toBe("/docs/fleet");
87 expect(step!.commands).toContain("/fleet setup");
88 expect(step!.commands).toContain("codewhale fleet status");
89 });
90
91 it("keeps durable Fleet status separate from current-session workers", () => {
92 const en = webText("lib/i18n/dictionaries/en/docs-fleet.ts");
93 const zh = webText("lib/i18n/dictionaries/zh/docs-fleet.ts");
94 const page = webText("app/[locale]/docs/fleet/page.tsx");
95 expect(page).toContain('fleetWorkers: "/fleet workers"');
96 for (const source of [en, zh]) {
97 expect(source).toContain("{fleetStatusTui}");
98 expect(source).toContain("{fleetStatusShell}");
99 expect(source).toContain("{fleetWorkers}");
100 expect(source).toContain("{subagents}");
101 }
102 });
103
104 it("documents saved fleets separately from members and workers", () => {
105 const page = webText("app/[locale]/docs/fleet/page.tsx");
106 const en = webText("lib/i18n/dictionaries/en/docs-fleet.ts");
107 const zh = webText("lib/i18n/dictionaries/zh/docs-fleet.ts");
108 expect(page).toContain('fleetSaved: "/fleet saved"');
109 for (const source of [en, zh]) {
110 expect(source).toContain("{fleetSaved}");
111 expect(source).toContain("/fleet setup");
112 expect(source).toContain("/fleet");
113 }
114 });
115 });
116
117 describe("Fleet compatibility boundary", () => {
118 it("documents the canonical commands, aliases, and shared Fleet artifacts", () => {
119 const doc = repoText("docs/FLEET.md");
120 expect(doc).toContain("`codewhale fleet …`");
121 expect(doc).toContain("`/fleet …`");
122 // Pod was ripped out before ever shipping: no pod aliases documented.
123 expect(doc).not.toContain("`/pod`");
124 expect(doc).not.toContain("codewhale pod");
125 for (const artifact of [
126 ".codewhale/fleet.jsonl",
127 "fleets/<name>.toml",
128 "`[fleet]`",
129 "--fleet",
130 "fleet.status",
131 ]) {
132 expect(doc, artifact).toContain(artifact);
133 }
134 });
135
136 it("pins Fleet to the exact public fact definition", () => {
137 const matrix = JSON.parse(repoText("docs/public-surface-facts.json")) as {
138 product: { terminology: Record<string, string> };
139 };
140 expect(Object.keys(matrix.product.terminology)).toContain("Fleet");
141 expect(Object.keys(matrix.product.terminology)).not.toContain("Pod");
142 const fleet = PRODUCT_TERMS.find((t) => t.term === "Fleet")!;
143 expect(fleet.short.en).toBe(matrix.product.terminology.Fleet);
144 expect(fleet.short.en).toBe(
145 "the user's model inventory: who is in the roster and which member is selected",
146 );
147 expect(repoText("docs/FLEET.md")).toContain(`**Fleet** = ${fleet.short.en}`);
148 });
149 });
150
150 lines TYPESCRIPT