返回 CodeWhale
facts.test.ts
根目录 / web / lib / facts.test.ts
1 import { describe, expect, it } from "vitest";
2 import { resolveFacts, type RepoFacts } from "./facts";
3
4 const release090 = {
5 tag: "v0.9.0",
6 version: "0.9.0",
7 publishedAt: "2026-07-16T20:05:39Z",
8 url: "https://github.com/Hmbown/CodeWhale/releases/tag/v0.9.0",
9 };
10
11 function snapshot(overrides: Partial<RepoFacts> = {}): RepoFacts {
12 return {
13 generatedAt: "2026-07-21T22:00:00Z",
14 sourceRevision: "a".repeat(40),
15 sourceCommittedAt: "2026-07-21T22:00:00Z",
16 version: "0.9.1",
17 crates: ["cli", "tui"],
18 sandboxBackends: ["seatbelt (macOS)"],
19 providers: [{ id: "deepseek", label: "DeepSeek", env: "DEEPSEEK_API_KEY" }],
20 models: [
21 {
22 id: "deepseek-v4-pro",
23 provider: "DeepSeek",
24 contextWindow: 1000000,
25 maxOutput: 128000,
26 reasoning: true,
27 addedAt: "2026-07-01",
28 },
29 ],
30 defaultModel: "deepseek-v4-pro",
31 nodeEngines: ">=18",
32 toolCount: 66,
33 license: "MIT",
34 latestPublishedRelease: release090,
35 ...overrides,
36 };
37 }
38
39 describe("resolveFacts", () => {
40 it("keeps newer build facts over an older KV snapshot", () => {
41 const build = snapshot();
42 const cached = snapshot({
43 sourceRevision: "b".repeat(40),
44 sourceCommittedAt: "2026-07-21T21:00:00Z",
45 version: "0.8.67",
46 toolCount: 60,
47 });
48
49 const result = resolveFacts(build, cached);
50
51 expect(result.source).toBe("build");
52 expect(result.reason).toBe("build-source-newer-or-ambiguous");
53 expect(result.facts.version).toBe("0.9.1");
54 expect(result.facts.toolCount).toBe(66);
55 });
56
57 it("rejects a legacy KV snapshot with no source provenance", () => {
58 const build = snapshot();
59 const cached = snapshot({
60 sourceRevision: null,
61 sourceCommittedAt: null,
62 version: "0.8.67",
63 });
64
65 const result = resolveFacts(build, cached);
66
67 expect(result.source).toBe("build");
68 expect(result.reason).toBe("kv-missing-source-provenance");
69 expect(result.facts.version).toBe("0.9.1");
70 });
71
72 it("accepts KV facts derived from the exact build revision", () => {
73 const build = snapshot();
74 const cached = snapshot({ generatedAt: "2026-07-21T23:00:00Z", toolCount: 67 });
75
76 const result = resolveFacts(build, cached);
77
78 expect(result.source).toBe("kv");
79 expect(result.reason).toBe("same-source-revision");
80 expect(result.facts.toolCount).toBe(67);
81 });
82
83 it("accepts a source-proven newer KV snapshot", () => {
84 const build = snapshot();
85 const cached = snapshot({
86 sourceRevision: "c".repeat(40),
87 sourceCommittedAt: "2026-07-21T23:00:00Z",
88 version: "0.9.2",
89 });
90
91 const result = resolveFacts(build, cached);
92
93 expect(result.source).toBe("kv");
94 expect(result.reason).toBe("kv-source-newer");
95 expect(result.facts.version).toBe("0.9.2");
96 });
97
98 it("tracks the latest published release independently of stale source facts", () => {
99 const build = snapshot();
100 const cached = snapshot({
101 sourceRevision: "b".repeat(40),
102 sourceCommittedAt: "2026-07-21T21:00:00Z",
103 version: "0.8.67",
104 latestPublishedRelease: {
105 tag: "v0.9.1",
106 version: "0.9.1",
107 publishedAt: "2026-07-22T01:00:00Z",
108 url: "https://github.com/Hmbown/CodeWhale/releases/tag/v0.9.1",
109 },
110 });
111
112 const result = resolveFacts(build, cached);
113
114 expect(result.source).toBe("build");
115 expect(result.facts.version).toBe("0.9.1");
116 expect(result.facts.latestPublishedRelease?.tag).toBe("v0.9.1");
117 });
118
119 it("falls back to build facts for malformed cache data", () => {
120 const result = resolveFacts(snapshot(), { version: "0.8.67" });
121
122 expect(result.source).toBe("build");
123 expect(result.reason).toBe("invalid-kv-snapshot");
124 expect(result.facts.toolCount).toBe(66);
125 });
126
127 it.each([
128 [
129 "a malformed source revision",
130 snapshot({
131 sourceRevision: "not-a-git-sha",
132 sourceCommittedAt: "2026-07-21T23:00:00Z",
133 toolCount: 67,
134 }),
135 ],
136 [
137 "an unparseable source timestamp",
138 snapshot({
139 sourceRevision: "a".repeat(40),
140 sourceCommittedAt: "not-a-timestamp",
141 toolCount: 67,
142 }),
143 ],
144 [
145 "a source revision without its timestamp",
146 snapshot({
147 sourceRevision: "a".repeat(40),
148 sourceCommittedAt: null,
149 toolCount: 67,
150 }),
151 ],
152 [
153 "a source timestamp without its revision",
154 snapshot({
155 sourceRevision: null,
156 sourceCommittedAt: "2026-07-21T23:00:00Z",
157 toolCount: 67,
158 }),
159 ],
160 ])("rejects %s before source precedence is evaluated", (_description, cached) => {
161 const result = resolveFacts(snapshot(), cached);
162
163 expect(result.source).toBe("build");
164 expect(result.reason).toBe("invalid-kv-snapshot");
165 expect(result.facts.toolCount).toBe(66);
166 });
167
168 it("rejects a published release whose URL is not canonical for its tag", () => {
169 const cached = snapshot({
170 latestPublishedRelease: {
171 ...release090,
172 url: "https://example.com/releases/tag/v0.9.0",
173 },
174 toolCount: 67,
175 });
176
177 const result = resolveFacts(snapshot(), cached);
178
179 expect(result.source).toBe("build");
180 expect(result.reason).toBe("invalid-kv-snapshot");
181 expect(result.facts.toolCount).toBe(66);
182 });
183 });
184
184 lines TYPESCRIPT