SPB Git

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%
6.1 KB · 152 lines typescript
Raw Blame History
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