spb/khaelor Public
KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.
TypeScript 82.9%
HTML 14.9%
CSS 1.1%
JavaScript 0.7%
1/**2 * KHAELOR3 * File: tests/config/persist.test.ts4 * Description: persistPermissionGrant tests — creation, merging, idempotency, atomicity, round-trip into the evaluator, and fail-safe on corrupted config.5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";11import { tmpdir } from "node:os";12import { join } from "node:path";13import { afterEach, beforeEach, describe, expect, it } from "vitest";14import { loadConfig, persistPermissionGrant } from "../../src/config/index.js";15import { evaluate, normalizePermissionsSection } from "../../src/permissions/index.js";16import type { CapabilityRequest } from "../../src/permissions/index.js";17import { unwrap } from "../../src/shared/index.js";1819const GRANT = {20 capability: "process.execute",21 pattern: "npm install *",22 action: "allow" as const,23 source: "project",24};2526describe("persistPermissionGrant", () => {27 let projectDir: string;28 let configPath: string;2930 beforeEach(async () => {31 projectDir = await mkdtemp(join(tmpdir(), "khaelor-persist-"));32 configPath = join(projectDir, ".khaelor", "config.json");33 });3435 afterEach(async () => {36 await rm(projectDir, { recursive: true, force: true });37 });3839 const readJson = async (): Promise<Record<string, unknown>> =>40 JSON.parse(await readFile(configPath, "utf8")) as Record<string, unknown>;4142 it("creates .khaelor/config.json when missing and writes the rule", async () => {43 const result = await persistPermissionGrant(GRANT, { projectDir });44 expect(result.ok).toBe(true);45 expect(await readJson()).toEqual({46 permissions: {47 rules: [{ capability: "process.execute", pattern: "npm install *", action: "allow" }],48 },49 });50 });5152 it("preserves existing config content and appends to existing rules", async () => {53 await mkdir(join(projectDir, ".khaelor"), { recursive: true });54 await writeFile(55 configPath,56 JSON.stringify({57 model: "project-model",58 futureFeature: { keep: true },59 permissions: {60 "file.read": "allow",61 "process.execute": { "git push *": "ask" },62 rules: [{ capability: "network.access", pattern: "curl *", action: "allow" }],63 },64 }),65 );66 unwrap(await persistPermissionGrant(GRANT, { projectDir }));67 expect(await readJson()).toEqual({68 model: "project-model",69 futureFeature: { keep: true },70 permissions: {71 "file.read": "allow",72 "process.execute": { "git push *": "ask" },73 rules: [74 { capability: "network.access", pattern: "curl *", action: "allow" },75 { capability: "process.execute", pattern: "npm install *", action: "allow" },76 ],77 },78 });79 });8081 it("is idempotent — an identical grant is never appended twice", async () => {82 unwrap(await persistPermissionGrant(GRANT, { projectDir }));83 unwrap(await persistPermissionGrant(GRANT, { projectDir }));84 unwrap(await persistPermissionGrant({ ...GRANT, pattern: undefined }, { projectDir }));85 unwrap(await persistPermissionGrant({ ...GRANT, pattern: undefined }, { projectDir }));86 const config = await readJson();87 const permissions = config["permissions"] as { rules: unknown[] };88 expect(permissions.rules).toEqual([89 { capability: "process.execute", pattern: "npm install *", action: "allow" },90 { capability: "process.execute", action: "allow" },91 ]);92 });9394 it("never writes the provenance source field to disk", async () => {95 unwrap(await persistPermissionGrant(GRANT, { projectDir }));96 expect(await readFile(configPath, "utf8")).not.toContain("source");97 });9899 it("leaves no temp files behind after a successful write", async () => {100 unwrap(await persistPermissionGrant(GRANT, { projectDir }));101 expect(await readdir(join(projectDir, ".khaelor"))).toEqual(["config.json"]);102 });103104 it("round-trips: persist → reload → the evaluator now allows the subject", async () => {105 unwrap(await persistPermissionGrant(GRANT, { projectDir }));106 const config = await loadConfig({ cwd: projectDir, env: {}, userConfigDir: join(projectDir, "no-user") });107 const rules = unwrap(normalizePermissionsSection(config.permissions, "project"));108 const request: CapabilityRequest = {109 capability: "process.execute",110 subject: "npm install left-pad",111 display: "Run npm install left-pad",112 alwaysPatterns: ["npm install *"],113 riskNotes: [],114 };115 const decision = evaluate(rules, request);116 expect(decision.action).toBe("allow");117 expect(decision.rule?.source).toBe("project");118 expect(decision.rule?.pattern).toBe("npm install *");119 });120121 it("fails safe on invalid JSON — clear error, file left untouched", async () => {122 await mkdir(join(projectDir, ".khaelor"), { recursive: true });123 await writeFile(configPath, "{ not json");124 const result = await persistPermissionGrant(GRANT, { projectDir });125 expect(result.ok).toBe(false);126 if (!result.ok) {127 expect(result.error.code).toBe("config-invalid");128 expect(result.error.message).toContain(configPath);129 }130 expect(await readFile(configPath, "utf8")).toBe("{ not json");131 });132133 it("fails safe on a config the loader would reject — no rewrite", async () => {134 await mkdir(join(projectDir, ".khaelor"), { recursive: true });135 const original = JSON.stringify({ permissions: { "process.execute": "maybe" } });136 await writeFile(configPath, original);137 const result = await persistPermissionGrant(GRANT, { projectDir });138 expect(result.ok).toBe(false);139 if (!result.ok) expect(result.error.code).toBe("config-invalid");140 expect(await readFile(configPath, "utf8")).toBe(original);141 });142143 it("refuses to rewrite a config carrying a secret field", async () => {144 await mkdir(join(projectDir, ".khaelor"), { recursive: true });145 const original = JSON.stringify({ apiKey: "sk-ant-oops" });146 await writeFile(configPath, original);147 const result = await persistPermissionGrant(GRANT, { projectDir });148 expect(result.ok).toBe(false);149 expect(await readFile(configPath, "utf8")).toBe(original);150 });151});152