返回 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 // The page is task-named ("Run a workflow"); the Fleet noun lives in
37 // its description, never the retired Pod noun.
38 expect(fleet?.description.en).toContain("Fleet");
39 expect(`${fleet?.label.en} ${fleet?.description.en}`).not.toContain("Pod");
40 expect(docTopicHref(fleet!, "en")).toBe("/en/docs/fleet");
41 expect(DOC_TOPICS.filter((t) => t.id === "fleet")).toHaveLength(1);
42 expect(DOC_TOPICS.map((t) => t.id)).not.toContain("pod");
43 });
44
45 it("indexes /docs/fleet and keeps /docs/pod out of the sitemap", () => {
46 for (const locale of ["en", "zh"]) {
47 expect(
48 sitemapEntries.some((e) => e.url === `${SITE_URL}/${locale}/docs/fleet`),
49 locale,
50 ).toBe(true);
51 expect(
52 sitemapEntries.some((e) => e.url === `${SITE_URL}/${locale}/docs/pod`),
53 locale,
54 ).toBe(false);
55 }
56 });
57
58 it("serves Fleet at /docs/fleet and permanently redirects /docs/pod", () => {
59 const page = webText("app/[locale]/docs/fleet/page.tsx");
60 const redirect = webText("app/[locale]/docs/pod/page.tsx");
61 expect(page).toContain('path: "/docs/fleet"');
62 expect(page).toContain('import { buildPageMetadata } from "@/lib/page-meta"');
63 expect(redirect).toContain('import { permanentRedirect } from "next/navigation"');
64 expect(redirect).toContain("permanentRedirect(`/${locale}/docs/fleet`)");
65 expect(redirect).not.toContain("buildPageMetadata");
66 expect(existsSync(new URL("app/[locale]/docs/fleet/page.tsx", webRoot))).toBe(true);
67 expect(existsSync(new URL("app/[locale]/docs/pod/page.tsx", webRoot))).toBe(true);
68 });
69
70 it("uses /docs/fleet in the machine-readable index", () => {
71 const llms = buildLlmsTxt();
72 expect(llms).toContain("/docs/fleet");
73 expect(llms).not.toContain("/docs/pod");
74 expect(llms).toContain(getTopic("fleet")!.label.en);
75 });
76
77 it("resolves docs search on both nouns to the one Fleet page", () => {
78 for (const query of ["fleet", "Fleet", "pod", "Pod"]) {
79 const hits = filterDocTopics(DOC_TOPICS, query).map((i) => DOC_TOPICS[i]);
80 expect(hits.filter((t) => t.id === "fleet"), query).toHaveLength(1);
81 }
82 });
83
84 it("uses Fleet in shared vocabulary and the guided path", () => {
85 expect(PRODUCT_TERMS.map((t) => t.term)).toEqual(["Fleet", "Workflow", "Lane", "Runtime"]);
86 const step = GETTING_STARTED_STEPS.find((s) => s.id === "fleet-workflow");
87 expect(step).toBeTruthy();
88 expect(step!.link.href).toBe("/docs/fleet");
89 expect(step!.commands).toContain("/fleet setup");
90 expect(step!.commands).toContain("codewhale fleet status");
91 });
92
93 it("keeps durable Fleet status separate from current-session workers", () => {
94 const page = webText("app/[locale]/docs/fleet/page.tsx");
95 expect(page).toContain("getDocsFleet");
96 for (const locale of ["en", "zh"]) {
97 const source = webText(`lib/i18n/dictionaries/${locale}/docs-fleet.ts`);
98 // Durable run history (shell) and this session's workers (TUI) are
99 // named as two different surfaces in both languages.
100 expect(source, locale).toContain("codewhale fleet status");
101 expect(source, locale).toContain("/fleet workers");
102 expect(source, locale).toContain("/subagents");
103 }
104 });
105
106 it("documents saved fleets separately from members and workers", () => {
107 for (const locale of ["en", "zh"]) {
108 const source = webText(`lib/i18n/dictionaries/${locale}/docs-fleet.ts`);
109 expect(source, locale).toContain("/fleet saved");
110 expect(source, locale).toContain("/fleet setup");
111 expect(source, locale).toContain("codewhale workflow run");
112 expect(source, locale).toContain("codewhale lane");
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