返回 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 RepoFacts {
27 [key: string]: unknown;
28 generatedAt: string;
29 sourceRevision: string | null;
30 sourceCommittedAt: string | null;
31 version: string | null;
32 crates: string[];
33 sandboxBackends: string[];
34 providers: ProviderFact[];
35 defaultModel: string | null;
36 nodeEngines: string | null;
37 toolCount: number | null;
38 license: string | null;
39 latestPublishedRelease: PublishedReleaseFact | null;
40 }
41
42 function diffFacts(
43 committed: Record<string, unknown>,
44 fresh: Record<string, unknown>,
45 ): Array<{ field: string; committed: unknown; fresh: unknown }> {
46 const checkFields = [
47 "version",
48 "crates",
49 "sandboxBackends",
50 "providers",
51 "defaultModel",
52 "nodeEngines",
53 "toolCount",
54 "license",
55 "latestPublishedRelease",
56 ];
57 const diffs: Array<{ field: string; committed: unknown; fresh: unknown }> = [];
58 for (const field of checkFields) {
59 const a = JSON.stringify(committed[field] ?? null);
60 const b = JSON.stringify(fresh[field] ?? null);
61 if (a !== b) {
62 diffs.push({ field, committed: committed[field], fresh: fresh[field] });
63 }
64 }
65 return diffs;
66 }
67
68 // --- helpers ---------------------------------------------------------
69
70 function freshFacts(overrides: Partial<RepoFacts> = {}): RepoFacts {
71 return {
72 generatedAt: new Date().toISOString(),
73 sourceRevision: null,
74 sourceCommittedAt: null,
75 version: "0.8.64",
76 crates: ["cli", "config", "tui"],
77 sandboxBackends: [
78 "seatbelt (macOS, when available)",
79 "bubblewrap (Linux, opt-in when installed)",
80 ],
81 providers: [
82 { id: "deepseek", label: "DeepSeek", env: "DEEPSEEK_API_KEY" },
83 { id: "anthropic", label: "Anthropic", env: "ANTHROPIC_API_KEY" },
84 ],
85 defaultModel: "deepseek-v4-pro",
86 nodeEngines: ">=18",
87 toolCount: 78,
88 license: "MIT",
89 latestPublishedRelease: {
90 tag: "v0.8.63",
91 version: "0.8.63",
92 publishedAt: "2026-06-01T00:00:00Z",
93 url: "https://github.com/Hmbown/CodeWhale/releases/tag/v0.8.63",
94 },
95 ...overrides,
96 };
97 }
98
99 // --- tests -----------------------------------------------------------
100
101 describe("diffFacts (check-facts parity)", () => {
102 it("returns empty array when facts match", () => {
103 const committed = freshFacts();
104 const fresh = freshFacts();
105 expect(diffFacts(committed, fresh)).toEqual([]);
106 });
107
108 it("detects stale version (0.8.62 vs 0.8.64)", () => {
109 const committed = freshFacts({ version: "0.8.62" });
110 const fresh = freshFacts({ version: "0.8.64" });
111 const diffs = diffFacts(committed, fresh);
112 expect(diffs).toHaveLength(1);
113 expect(diffs[0]).toEqual({
114 field: "version",
115 committed: "0.8.62",
116 fresh: "0.8.64",
117 });
118 });
119
120 it("detects provider list drift (added Anthropic)", () => {
121 const committed = freshFacts({
122 providers: [{ id: "deepseek", label: "DeepSeek", env: "DEEPSEEK_API_KEY" }],
123 });
124 const fresh = freshFacts({
125 providers: [
126 { id: "deepseek", label: "DeepSeek", env: "DEEPSEEK_API_KEY" },
127 { id: "anthropic", label: "Anthropic", env: "ANTHROPIC_API_KEY" },
128 ],
129 });
130 const diffs = diffFacts(committed, fresh);
131 expect(diffs).toHaveLength(1);
132 expect(diffs[0].field).toBe("providers");
133 });
134
135 it("detects stale default model", () => {
136 const committed = freshFacts({ defaultModel: "deepseek-v3" });
137 const fresh = freshFacts({ defaultModel: "deepseek-v4-pro" });
138 const diffs = diffFacts(committed, fresh);
139 expect(diffs).toHaveLength(1);
140 expect(diffs[0].field).toBe("defaultModel");
141 });
142
143 it("detects tool count drift", () => {
144 const committed = freshFacts({ toolCount: 70 });
145 const fresh = freshFacts({ toolCount: 78 });
146 const diffs = diffFacts(committed, fresh);
147 expect(diffs).toHaveLength(1);
148 expect(diffs[0].field).toBe("toolCount");
149 });
150
151 it("detects multiple field drifts at once", () => {
152 const committed = freshFacts({
153 version: "0.8.62",
154 toolCount: 70,
155 });
156 const fresh = freshFacts({
157 version: "0.8.64",
158 toolCount: 78,
159 });
160 const diffs = diffFacts(committed, fresh);
161 expect(diffs).toHaveLength(2);
162 expect(diffs.map((d) => d.field).sort()).toEqual(["toolCount", "version"]);
163 });
164
165 it("ignores generatedAt and exact-build provenance changes", () => {
166 const committed = freshFacts({ generatedAt: "old" });
167 const fresh = freshFacts({
168 generatedAt: "new",
169 sourceRevision: "a".repeat(40),
170 sourceCommittedAt: "2026-07-21T22:00:00Z",
171 });
172 expect(diffFacts(committed, fresh)).toEqual([]);
173 });
174
175 it("detects latest-published-release drift", () => {
176 const committed = freshFacts();
177 const fresh = freshFacts({
178 latestPublishedRelease: {
179 tag: "v0.8.64",
180 version: "0.8.64",
181 publishedAt: "2026-06-02T00:00:00Z",
182 url: "https://github.com/Hmbown/CodeWhale/releases/tag/v0.8.64",
183 },
184 });
185 const diffs = diffFacts(committed, fresh);
186 expect(diffs).toHaveLength(1);
187 expect(diffs[0].field).toBe("latestPublishedRelease");
188 });
189
190 it("handles null-to-value drift for license", () => {
191 const committed = freshFacts({ license: null });
192 const fresh = freshFacts({ license: "MIT" });
193 const diffs = diffFacts(committed, fresh);
194 expect(diffs).toHaveLength(1);
195 expect(diffs[0].field).toBe("license");
196 });
197
198 it("handles empty arrays in committed vs populated arrays in fresh", () => {
199 const committed = freshFacts({ crates: [], providers: [] });
200 const fresh = freshFacts({
201 crates: ["cli"],
202 providers: [{ id: "deepseek", label: "DeepSeek", env: "DEEPSEEK_API_KEY" }],
203 });
204 const diffs = diffFacts(committed, fresh);
205 expect(diffs).toHaveLength(2);
206 expect(diffs.map((d) => d.field).sort()).toEqual(["crates", "providers"]);
207 });
208 });
209
209 lines TYPESCRIPT