bc61434f7c
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>
84 lines
3.0 KiB
TypeScript
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/);
|
|
});
|
|
});
|