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