Files
chlova/orchestrator/test/auto-extension.test.ts
T
Kantin-Petit bc61434f7c feat: GitCommitter + AutoExtensionService (v0.26.0)
propose() : écrit artefact+doc → commit ciblé + version → enregistre
l'asset en need-review (privilégié = BLOQUÉ sans sursis) → alerte
asset_created (version+commit+doc). N'exécute jamais l'asset. Commits
ciblés (jamais add -A). 73 tests (dépôt git temp), 0 vuln.

Palier de risque : privilégié (écrit + commit dans le dépôt) — derrière
flag + Phase 2 ; n'exécute aucun asset, tout reste sous review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 06:35:08 +02:00

84 lines
3.0 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { mkdtemp, rm, access } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { AssetRepository } from "../src/gatekeeper/repository.js";
import { GitCommitter } from "../src/autoext/git-committer.js";
import { AutoExtensionService } from "../src/autoext/auto-extension.js";
import { createLogger } from "../src/audit/log.js";
import type { AlertSender, AlertEvent } from "../src/alerts/types.js";
import type { AssetDraft } from "../src/autoext/artifact-writer.js";
const exec = promisify(execFile);
const log = createLogger("silent");
class CapturingSender implements AlertSender {
events: AlertEvent[] = [];
async send(e: AlertEvent): Promise<void> {
this.events.push(e);
}
}
const draft = (over: Partial<AssetDraft> = {}): AssetDraft => ({
type: "workflow-n8n",
name: "Backup NC",
version: "1.0.0",
riskTier: "privileged",
summary: "Sauvegarde.",
content: '{"a":1}',
...over,
});
let root: string;
let repo: AssetRepository;
beforeEach(async () => {
root = await mkdtemp(join(tmpdir(), "chlova-git-"));
await exec("git", ["init", "-q"], { cwd: root });
await exec("git", ["-c", "user.name=t", "-c", "user.email=t@t", "commit", "--allow-empty", "-q", "-m", "init"], { cwd: root });
repo = new AssetRepository(":memory:");
});
afterEach(async () => {
repo.close();
await rm(root, { recursive: true, force: true });
});
function service(alerts: AlertSender): AutoExtensionService {
return new AutoExtensionService(repo, new GitCommitter(root), alerts, log, root);
}
describe("AutoExtensionService.propose", () => {
it("écrit, commit, enregistre BLOQUÉ (privilégié) et alerte", async () => {
const sender = new CapturingSender();
const res = await service(sender).propose(draft());
expect(res.asset.status).toBe("bloqué"); // privilégié → aucun sursis
expect(res.commit).toMatch(/^[0-9a-f]{7,}$/);
expect(res.asset.commitLink).toBe(res.commit);
expect(res.asset.docLink).toBe(res.docPath);
// fichiers présents + commités
await expect(access(join(root, res.artifactPath))).resolves.toBeUndefined();
const { stdout } = await exec("git", ["log", "--oneline"], { cwd: root });
expect(stdout).toContain("need-review");
// asset persistant + alerte
expect(repo.get(res.asset.id)?.status).toBe("bloqué");
expect(sender.events.map((e) => e.kind)).toContain("asset_created");
});
it("réversible → PROVISOIRE", async () => {
const res = await service(new CapturingSender()).propose(draft({ riskTier: "reversible", name: "RO tool", type: "tool" }));
expect(res.asset.status).toBe("provisoire");
expect(res.asset.expiresAt).not.toBeNull();
});
it("refuse un doublon (même id)", async () => {
const svc = service(new CapturingSender());
await svc.propose(draft());
await expect(svc.propose(draft())).rejects.toThrow(/existant/);
});
});