返回 CodeWhale
check-facts.test.ts
根目录 / web / lib / check-facts.test.ts
1 /**
2 * Tests for check-facts.mjs via its re-importable logic.
3 *
4 * We test the diffFacts helper (ported from check-facts.mjs) against
5 * fixture-like objects to prove the stale-version detection works.
6 * End-to-end `node scripts/check-facts.mjs` exit-code tests are not run
7 * from vitest since they depend on the actual workspace file tree.
8 */
9 import { describe, it, expect } from "vitest";
10
11 // --- inline diffFacts (same logic as check-facts.mjs) ----------------
12
13 interface ProviderFact {
14 id: string;
15 label: string;
16 env: string;
17 }
18
19 interface PublishedReleaseFact {
20 tag: string;
21 version: string;
22 publishedAt: string;
23 url: string;
24 }
25
26 interface ModelFact {
27 id: string;
28 provider: string | null;
29 contextWindow: number | null;
30 maxOutput: number | null;
31 reasoning: boolean;
32 addedAt: string | null;
33 }
34
35 interface RepoFacts {
36 [key: string]: unknown;
37 generatedAt: string;
38 sourceRevision: string | null;
39 sourceCommittedAt: string | null;
40 version: string | null;
41 crates: string[];
42 sandboxBackends: string[];
43 providers: ProviderFact[];
44 models: ModelFact[];
45 defaultModel: string | null;
46 nodeEngines: string | null;
47 toolCount: number | null;
48 license: string | null;
49 latestPublishedRelease: PublishedReleaseFact | null;
50 }
51
52 function diffFacts(
53 committed: Record<string, unknown>,
54 fresh: Record<string, unknown>,
55 ): Array<{ field: string; committed: unknown; fresh: unknown }> {
56 const checkFields = [
57 "version",
58 "crates",
59 "sandboxBackends",
60 "providers",
61 "models",
62 "defaultModel",
63 "nodeEngines",
64 "toolCount",
65 "license",
66 "latestPublishedRelease",
67 ];
68 const diffs: Array<{ field: string; committed: unknown; fresh: unknown }> = [];
69 for (const field of checkFields) {
70 const a = JSON.stringify(committed[field] ?? null);
71 const b = JSON.stringify(fresh[field] ?? null);
72 if (a !== b) {
73 diffs.push({ field, committed: committed[field], fresh: fresh[field] });
74 }
75 }
76 return diffs;
77 }
78
79 // --- helpers ---------------------------------------------------------
80
81 function freshFacts(overrides: Partial<RepoFacts> = {}): RepoFacts {
82 return {
83 generatedAt: new Date().toISOString(),
84 sourceRevision: null,
85 sourceCommittedAt: null,
86 version: "0.8.64",
87 crates: ["cli", "config", "tui"],
88 sandboxBackends: [
89 "seatbelt (macOS, when available)",
90 "bubblewrap (Linux, opt-in when installed)",
91 ],
92 providers: [
93 { id: "deepseek", label: "DeepSeek", env: "DEEPSEEK_API_KEY" },
94 { id: "anthropic", label: "Anthropic", env: "ANTHROPIC_API_KEY" },
95 ],
96 models: [
97 {
98 id: "deepseek-v4-pro",
99 provider: "DeepSeek",
100 contextWindow: 1000000,
101 maxOutput: 128000,
102 reasoning: true,
103 addedAt: "2026-07-01",
104 },
105 ],
106 defaultModel: "deepseek-v4-pro",
107 nodeEngines: ">=18",
108 toolCount: 78,
109 license: "MIT",
110 latestPublishedRelease: {
111 tag: "v0.8.63",
112 version: "0.8.63",
113 publishedAt: "2026-06-01T00:00:00Z",
114 url: "https://github.com/Hmbown/CodeWhale/releases/tag/v0.8.63",
115 },
116 ...overrides,
117 };
118 }
119
120 // --- tests -----------------------------------------------------------
121
122 describe("diffFacts (check-facts parity)", () => {
123 it("returns empty array when facts match", () => {
124 const committed = freshFacts();
125 const fresh = freshFacts();
126 expect(diffFacts(committed, fresh)).toEqual([]);
127 });
128
129 it("detects stale version (0.8.62 vs 0.8.64)", () => {
130 const committed = freshFacts({ version: "0.8.62" });
131 const fresh = freshFacts({ version: "0.8.64" });
132 const diffs = diffFacts(committed, fresh);
133 expect(diffs).toHaveLength(1);
134 expect(diffs[0]).toEqual({
135 field: "version",
136 committed: "0.8.62",
137 fresh: "0.8.64",
138 });
139 });
140
141 it("detects provider list drift (added Anthropic)", () => {
142 const committed = freshFacts({
143 providers: [{ id: "deepseek", label: "DeepSeek", env: "DEEPSEEK_API_KEY" }],
144 });
145 const fresh = freshFacts({
146 providers: [
147 { id: "deepseek", label: "DeepSeek", env: "DEEPSEEK_API_KEY" },
148 { id: "anthropic", label: "Anthropic", env: "ANTHROPIC_API_KEY" },
149 ],
150 });
151 const diffs = diffFacts(committed, fresh);
152 expect(diffs).toHaveLength(1);
153 expect(diffs[0].field).toBe("providers");
154 });
155
156 it("detects stale default model", () => {
157 const committed = freshFacts({ defaultModel: "deepseek-v3" });
158 const fresh = freshFacts({ defaultModel: "deepseek-v4-pro" });
159 const diffs = diffFacts(committed, fresh);
160 expect(diffs).toHaveLength(1);
161 expect(diffs[0].field).toBe("defaultModel");
162 });
163
164 it("detects tool count drift", () => {
165 const committed = freshFacts({ toolCount: 70 });
166 const fresh = freshFacts({ toolCount: 78 });
167 const diffs = diffFacts(committed, fresh);
168 expect(diffs).toHaveLength(1);
169 expect(diffs[0].field).toBe("toolCount");
170 });
171
172 it("detects multiple field drifts at once", () => {
173 const committed = freshFacts({
174 version: "0.8.62",
175 toolCount: 70,
176 });
177 const fresh = freshFacts({
178 version: "0.8.64",
179 toolCount: 78,
180 });
181 const diffs = diffFacts(committed, fresh);
182 expect(diffs).toHaveLength(2);
183 expect(diffs.map((d) => d.field).sort()).toEqual(["toolCount", "version"]);
184 });
185
186 it("ignores generatedAt and exact-build provenance changes", () => {
187 const committed = freshFacts({ generatedAt: "old" });
188 const fresh = freshFacts({
189 generatedAt: "new",
190 sourceRevision: "a".repeat(40),
191 sourceCommittedAt: "2026-07-21T22:00:00Z",
192 });
193 expect(diffFacts(committed, fresh)).toEqual([]);
194 });
195
196 it("detects latest-published-release drift", () => {
197 const committed = freshFacts();
198 const fresh = freshFacts({
199 latestPublishedRelease: {
200 tag: "v0.8.64",
201 version: "0.8.64",
202 publishedAt: "2026-06-02T00:00:00Z",
203 url: "https://github.com/Hmbown/CodeWhale/releases/tag/v0.8.64",
204 },
205 });
206 const diffs = diffFacts(committed, fresh);
207 expect(diffs).toHaveLength(1);
208 expect(diffs[0].field).toBe("latestPublishedRelease");
209 });
210
211 it("handles null-to-value drift for license", () => {
212 const committed = freshFacts({ license: null });
213 const fresh = freshFacts({ license: "MIT" });
214 const diffs = diffFacts(committed, fresh);
215 expect(diffs).toHaveLength(1);
216 expect(diffs[0].field).toBe("license");
217 });
218
219 it("handles empty arrays in committed vs populated arrays in fresh", () => {
220 const committed = freshFacts({ crates: [], providers: [] });
221 const fresh = freshFacts({
222 crates: ["cli"],
223 providers: [{ id: "deepseek", label: "DeepSeek", env: "DEEPSEEK_API_KEY" }],
224 });
225 const diffs = diffFacts(committed, fresh);
226 expect(diffs).toHaveLength(2);
227 expect(diffs.map((d) => d.field).sort()).toEqual(["crates", "providers"]);
228 });
229 });
230
230 lines TYPESCRIPT