返回 CodeWhale
community-agent-security.test.ts
根目录 / web / lib / community-agent-security.test.ts
1 import { describe, expect, it } from "vitest";
2 import {
3 deleteDraft,
4 draftKey,
5 getDraft,
6 hasFreshDraft,
7 listDrafts,
8 parseDraftKey,
9 saveDraft,
10 type AgentDraft,
11 } from "./community-agent";
12
13 class FakeKv {
14 readonly values = new Map<string, string>();
15 readonly deleted: string[] = [];
16
17 async get(key: string): Promise<string | null> {
18 return this.values.get(key) ?? null;
19 }
20
21 async put(key: string, value: string): Promise<void> {
22 this.values.set(key, value);
23 }
24
25 async list(options?: { prefix?: string; limit?: number }): Promise<{ keys: { name: string }[] }> {
26 const prefix = options?.prefix ?? "";
27 const limit = options?.limit ?? Number.POSITIVE_INFINITY;
28 return {
29 keys: [...this.values.keys()]
30 .filter((key) => key.startsWith(prefix))
31 .slice(0, limit)
32 .map((name) => ({ name })),
33 };
34 }
35
36 async delete(key: string): Promise<void> {
37 this.deleted.push(key);
38 this.values.delete(key);
39 }
40 }
41
42 function draft(overrides: Partial<AgentDraft> = {}): AgentDraft {
43 return {
44 id: "42",
45 type: "triage",
46 bodyEn: "review",
47 bodyZh: "审阅",
48 generatedAt: "2026-07-17T00:00:00.000Z",
49 posted: false,
50 ...overrides,
51 };
52 }
53
54 describe("community-agent draft boundary", () => {
55 it("uses the same canonical key for save and freshness checks", async () => {
56 const kv = new FakeKv();
57 await saveDraft(kv, draft());
58
59 expect([...kv.values.keys()]).toEqual(["draft:triage:42"]);
60 await expect(hasFreshDraft(kv, "triage", "42", "2026-07-16T00:00:00.000Z")).resolves.toBe(true);
61 await expect(hasFreshDraft(kv, "issue", "42", "2026-07-16T00:00:00.000Z")).resolves.toBe(false);
62 });
63
64 it("uses the canonical PR review namespace for freshness checks", async () => {
65 const kv = new FakeKv();
66 await saveDraft(kv, draft({ type: "pr-review" }));
67
68 expect([...kv.values.keys()]).toEqual(["draft:pr-review:42"]);
69 await expect(hasFreshDraft(kv, "pr-review", "42", "2026-07-16T00:00:00.000Z")).resolves.toBe(true);
70 await expect(hasFreshDraft(kv, "pr", "42", "2026-07-16T00:00:00.000Z")).resolves.toBe(false);
71 });
72
73 it("accepts only canonical draft namespaces and bounded ids", () => {
74 expect(parseDraftKey("draft:pr-review:123")).toEqual({ type: "pr-review", id: "123" });
75 expect(parseDraftKey("dispatch:latest")).toBeNull();
76 expect(parseDraftKey("draft:unknown:123")).toBeNull();
77 expect(parseDraftKey("draft:triage:bad:id")).toBeNull();
78 expect(() => draftKey("triage", "bad:id")).toThrow("invalid draft id");
79 });
80
81 it("rejects parseable non-drafts and key/object identity mismatches", async () => {
82 const kv = new FakeKv();
83 kv.values.set("draft:triage:42", JSON.stringify({ generatedAt: "2026-07-17T00:00:00.000Z" }));
84 kv.values.set("draft:triage:43", JSON.stringify(draft()));
85
86 await expect(getDraft(kv, "dispatch:latest")).resolves.toBeNull();
87 await expect(getDraft(kv, "draft:triage:42")).resolves.toBeNull();
88 await expect(getDraft(kv, "draft:triage:43")).resolves.toBeNull();
89 await expect(listDrafts(kv)).resolves.toEqual([]);
90 });
91
92 it("never lets the draft deletion helper cross into another KV namespace", async () => {
93 const kv = new FakeKv();
94 kv.values.set("dispatch:latest", JSON.stringify({ state: "running" }));
95
96 await expect(deleteDraft(kv, "dispatch:latest")).rejects.toThrow("invalid draft key");
97 expect(kv.deleted).toEqual([]);
98 expect(kv.values.has("dispatch:latest")).toBe(true);
99
100 await saveDraft(kv, draft());
101 await expect(deleteDraft(kv, "draft:triage:42")).resolves.toBeUndefined();
102 expect(kv.deleted).toEqual(["draft:triage:42"]);
103 });
104 });
105
105 lines TYPESCRIPT