Skip to content

Commit b6045a8

Browse files
committed
test(permissions): pin update_plan purge migration for approval stores
1 parent 1a6c171 commit b6045a8

1 file changed

Lines changed: 194 additions & 0 deletions

File tree

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3+
import { tmpdir } from "node:os";
4+
import { join } from "node:path";
5+
import { generateSessionId, sessionDir } from "../session/index.js";
6+
import { loadSeededApprovals } from "../session/runtime-assembly.js";
7+
import { normalizeSeededApprovals } from "./authz-grants.js";
8+
import { migratePersistedApprovalStores } from "./approval-store-migration.js";
9+
10+
let cwd = "";
11+
let home = "";
12+
let sessionId = "";
13+
14+
const sessionStorePath = (): string =>
15+
join(sessionDir(cwd, sessionId, home), "permissions.json");
16+
const projectStorePath = (): string => join(cwd, ".corbits", "permissions.json");
17+
const globalStorePath = (): string => join(home, ".corbits", "permissions.json");
18+
const backupPath = (path: string): string => `${path}.bak`;
19+
20+
async function readJson(path: string): Promise<unknown> {
21+
return JSON.parse(await readFile(path, "utf-8")) as unknown;
22+
}
23+
24+
beforeEach(async () => {
25+
cwd = await mkdtemp(join(tmpdir(), "approval-migration-"));
26+
home = await mkdtemp(join(tmpdir(), "approval-migration-home-"));
27+
sessionId = generateSessionId();
28+
await mkdir(sessionDir(cwd, sessionId, home), { recursive: true });
29+
});
30+
31+
afterEach(async () => {
32+
await rm(cwd, { recursive: true, force: true });
33+
await rm(home, { recursive: true, force: true });
34+
});
35+
36+
describe("migratePersistedApprovalStores", () => {
37+
test("purges update_plan keys from every store, backs up, and re-runs as a no-op", async () => {
38+
const sessionOriginal = {
39+
approvals: [
40+
{ tool: "update_plan", pattern: "plan *" },
41+
{ tool: "run_shell", pattern: "npm *" },
42+
{ tool: "bash", pattern: "git *" },
43+
],
44+
};
45+
const projectOriginal = {
46+
approvals: [
47+
{ tool: "update_plan", pattern: "plan *" },
48+
{ tool: "manage_tasks", pattern: "tasks *" },
49+
],
50+
};
51+
const globalOriginal = {
52+
approvals: [
53+
{ tool: "update_plan", pattern: "plan *" },
54+
{ tool: "run_shell", pattern: "git *" },
55+
],
56+
providerModels: {
57+
"openai:gpt-5": [
58+
{ tool: "update_plan", pattern: "plan *" },
59+
{ tool: "run_shell", pattern: "npm *" },
60+
],
61+
"anthropic:opus": [{ tool: "run_shell", pattern: "ls *" }],
62+
},
63+
};
64+
await writeFile(sessionStorePath(), JSON.stringify(sessionOriginal));
65+
await mkdir(join(cwd, ".corbits"), { recursive: true });
66+
await writeFile(projectStorePath(), JSON.stringify(projectOriginal));
67+
await mkdir(join(home, ".corbits"), { recursive: true });
68+
await writeFile(globalStorePath(), JSON.stringify(globalOriginal));
69+
70+
const first = await migratePersistedApprovalStores(cwd, sessionId, home);
71+
72+
expect(first.purged).toBe(4);
73+
expect(first.backups).toHaveLength(3);
74+
75+
expect(await readJson(sessionStorePath())).toEqual({
76+
approvals: [
77+
{ tool: "run_shell", pattern: "npm *" },
78+
{ tool: "bash", pattern: "git *" },
79+
],
80+
});
81+
expect(await readJson(projectStorePath())).toEqual({
82+
approvals: [{ tool: "manage_tasks", pattern: "tasks *" }],
83+
});
84+
expect(await readJson(globalStorePath())).toEqual({
85+
approvals: [{ tool: "run_shell", pattern: "git *" }],
86+
providerModels: {
87+
"openai:gpt-5": [{ tool: "run_shell", pattern: "npm *" }],
88+
"anthropic:opus": [{ tool: "run_shell", pattern: "ls *" }],
89+
},
90+
});
91+
92+
for (const [path, original] of [
93+
[sessionStorePath(), sessionOriginal],
94+
[projectStorePath(), projectOriginal],
95+
[globalStorePath(), globalOriginal],
96+
] as const) {
97+
expect(await readJson(backupPath(path))).toEqual(original);
98+
}
99+
100+
const sessionAfterFirst = await readFile(sessionStorePath(), "utf-8");
101+
const second = await migratePersistedApprovalStores(cwd, sessionId, home);
102+
expect(second.purged).toBe(0);
103+
expect(second.backups).toEqual([]);
104+
expect(await readFile(sessionStorePath(), "utf-8")).toBe(sessionAfterFirst);
105+
});
106+
107+
test("leaves clean stores untouched with no backup written", async () => {
108+
const sessionOriginal = {
109+
approvals: [{ tool: "run_shell", pattern: "npm *" }],
110+
};
111+
await writeFile(sessionStorePath(), JSON.stringify(sessionOriginal));
112+
113+
const result = await migratePersistedApprovalStores(cwd, sessionId, home);
114+
115+
expect(result.purged).toBe(0);
116+
expect(result.backups).toEqual([]);
117+
expect(await readJson(sessionStorePath())).toEqual(sessionOriginal);
118+
await expect(
119+
readFile(backupPath(sessionStorePath()), "utf-8"),
120+
).rejects.toThrow();
121+
});
122+
123+
test("treats missing and corrupt stores as no-ops", async () => {
124+
await mkdir(join(cwd, ".corbits"), { recursive: true });
125+
await writeFile(projectStorePath(), "not json{{{");
126+
127+
const result = await migratePersistedApprovalStores(cwd, sessionId, home);
128+
129+
expect(result.purged).toBe(0);
130+
expect(result.backups).toEqual([]);
131+
expect(await readFile(projectStorePath(), "utf-8")).toBe("not json{{{");
132+
});
133+
134+
test("purges exactly the keys the load-time normalizer drops", async () => {
135+
const tools = [
136+
"update_plan",
137+
"Update_Plan",
138+
"default.update_plan",
139+
"manage_tasks",
140+
"run_shell",
141+
];
142+
await writeFile(
143+
sessionStorePath(),
144+
JSON.stringify({
145+
approvals: tools.map((tool) => ({ tool, pattern: "x *" })),
146+
}),
147+
);
148+
149+
const result = await migratePersistedApprovalStores(cwd, sessionId, home);
150+
151+
const seeded = tools.map((tool) => ({ tool, pattern: "x *" }));
152+
const droppedByNormalizer = seeded.filter(
153+
(approval) =>
154+
!normalizeSeededApprovals([approval]).some(
155+
(kept: { tool: string }) => kept.tool === approval.tool,
156+
),
157+
);
158+
expect(result.purged).toBe(droppedByNormalizer.length);
159+
expect(result.purged).toBe(3);
160+
const remaining = (
161+
(await readJson(sessionStorePath())) as {
162+
approvals: { tool: string }[];
163+
}
164+
).approvals.map((approval) => approval.tool);
165+
expect(remaining).toEqual(["manage_tasks", "run_shell"]);
166+
});
167+
168+
test("seed loading purges on-disk update_plan keys while the normalizer still drops them in memory", async () => {
169+
await writeFile(
170+
sessionStorePath(),
171+
JSON.stringify({
172+
approvals: [
173+
{ tool: "update_plan", pattern: "plan *" },
174+
{ tool: "run_shell", pattern: "npm *" },
175+
],
176+
}),
177+
);
178+
await mkdir(join(cwd, ".corbits"), { recursive: true });
179+
await writeFile(
180+
projectStorePath(),
181+
JSON.stringify({
182+
approvals: [{ tool: "update_plan", pattern: "plan *" }],
183+
}),
184+
);
185+
186+
const seeded = await loadSeededApprovals(cwd, sessionId, home);
187+
188+
expect(seeded).toEqual([{ tool: "run_shell", pattern: "npm *" }]);
189+
expect(await readJson(sessionStorePath())).toEqual({
190+
approvals: [{ tool: "run_shell", pattern: "npm *" }],
191+
});
192+
expect(await readJson(projectStorePath())).toEqual({ approvals: [] });
193+
});
194+
});

0 commit comments

Comments
 (0)