test(FN-4433): complete Step 3 — cover file-scope violation audit emission

Fusion-Task-Id: FN-4433
Fusion-Task-Lineage: 63ee20ca-1e04-4b42-b386-a1e586aac889
This commit is contained in:
Fusion
2026-05-14 04:27:12 -07:00
committed by gsxdsm
parent 2c5d0aba4c
commit 880f755787
2 changed files with 109 additions and 3 deletions

View File

@@ -6,6 +6,7 @@ import {
assertSquashOverlapsFileScope,
attemptWithSideStrategy,
commitOrAmendMergeWithFixes,
enforceSquashFileScopeInvariant,
executeMergeAttempt,
FileScopeViolationError,
} from "../merger.js";
@@ -195,6 +196,108 @@ describe("assertSquashOverlapsFileScope", () => {
});
});
describe("enforceSquashFileScopeInvariant audit emission", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("emits run_audit event on file-scope violation", async () => {
const store = createInvariantStore(["packages/engine/src/merger.ts"]);
const auditor = { git: vi.fn().mockResolvedValue(undefined) };
mockedExecSync.mockImplementation((cmd: any) => {
const cmdStr = String(cmd);
if (cmdStr === "git diff --cached --name-only") return "packages/core/src/store.ts";
if (cmdStr === "git reset --merge") return "";
return "";
});
await expect(enforceSquashFileScopeInvariant({
store: store as never,
taskId: "FN-4073",
rootDir: "/tmp/root",
task: await (store as any).getTask("FN-4073"),
resetLabel: "file-scope invariant violation",
auditor: auditor as any,
})).rejects.toBeInstanceOf(FileScopeViolationError);
expect(auditor.git).toHaveBeenCalledTimes(1);
expect(auditor.git).toHaveBeenCalledWith({
type: "merge:file-scope-violation",
target: "FN-4073",
metadata: {
resetLabel: "file-scope invariant violation",
stagedFiles: ["packages/core/src/store.ts"],
declaredScope: ["packages/engine/src/merger.ts"],
stagedFileCount: 1,
declaredScopeCount: 1,
},
});
});
it("does not emit when scopeOverride bypasses invariant", async () => {
const store = createInvariantStore(["packages/engine/src/merger.ts"], { scopeOverride: true });
const auditor = { git: vi.fn().mockResolvedValue(undefined) };
mockedExecSync.mockImplementation(() => "packages/core/src/store.ts");
await expect(enforceSquashFileScopeInvariant({
store: store as never,
taskId: "FN-4073",
rootDir: "/tmp/root",
task: await (store as any).getTask("FN-4073"),
resetLabel: "file-scope invariant violation",
auditor: auditor as any,
})).resolves.toBeUndefined();
expect(auditor.git).not.toHaveBeenCalled();
});
it("does not mask original violation when audit emission fails", async () => {
const store = createInvariantStore(["packages/engine/src/merger.ts"]);
const auditor = { git: vi.fn().mockRejectedValue(new Error("audit boom")) };
mockedExecSync.mockImplementation((cmd: any) => {
const cmdStr = String(cmd);
if (cmdStr === "git diff --cached --name-only") return "packages/core/src/store.ts";
if (cmdStr === "git reset --merge") return "";
return "";
});
await expect(enforceSquashFileScopeInvariant({
store: store as never,
taskId: "FN-4073",
rootDir: "/tmp/root",
task: await (store as any).getTask("FN-4073"),
resetLabel: "file-scope invariant violation",
auditor: auditor as any,
})).rejects.toBeInstanceOf(FileScopeViolationError);
expect(store.appendAgentLog).toHaveBeenCalledWith(
"FN-4073",
expect.stringContaining("File-scope invariant violation"),
"tool_error",
expect.stringContaining("declaredScope:"),
"merger",
);
});
it("keeps backward compatibility when auditor is omitted", async () => {
const store = createInvariantStore(["packages/engine/src/merger.ts"]);
mockedExecSync.mockImplementation((cmd: any) => {
const cmdStr = String(cmd);
if (cmdStr === "git diff --cached --name-only") return "packages/core/src/store.ts";
if (cmdStr === "git reset --merge") return "";
return "";
});
await expect(enforceSquashFileScopeInvariant({
store: store as never,
taskId: "FN-4073",
rootDir: "/tmp/root",
task: await (store as any).getTask("FN-4073"),
resetLabel: "file-scope invariant violation",
})).rejects.toBeInstanceOf(FileScopeViolationError);
});
});
describe("file-scope invariant wiring", () => {
beforeEach(() => {
vi.clearAllMocks();

View File

@@ -3171,6 +3171,7 @@ export async function commitOrAmendMergeWithFixes(
aiSubject?: string | null,
fixModifiedFiles: ReadonlySet<string> = new Set(),
store?: TaskStore,
auditor?: RunAuditor,
): Promise<MergeFinalizeResult> {
try {
// Build an allowlist of paths we are permitted to stage.
@@ -3517,7 +3518,7 @@ export async function commitOrAmendMergeWithFixes(
rootDir,
task: await store.getTask(taskId),
resetLabel: "file-scope invariant violation",
auditor: audit,
auditor,
});
}
await runDiffVolumeGate({
@@ -3560,7 +3561,7 @@ export async function commitOrAmendMergeWithFixes(
rootDir,
task: await store.getTask(taskId),
resetLabel: "file-scope invariant violation",
auditor: audit,
auditor,
});
}
await runDiffVolumeGate({
@@ -3770,7 +3771,7 @@ export function formatFileScopeViolationAgentLog(error: FileScopeViolationError)
].join("\n");
}
async function enforceSquashFileScopeInvariant(params: {
export async function enforceSquashFileScopeInvariant(params: {
store: TaskStore;
taskId: string;
rootDir: string;
@@ -6669,6 +6670,7 @@ export async function aiMergeTask(
aiMergeSubject,
verificationFixModifiedFiles,
store,
audit,
);
if (!finalized.ok) {
// Phantom-merge guard: refused to fabricate a commit. Reset
@@ -6791,6 +6793,7 @@ export async function aiMergeTask(
aiMergeSubject,
buildFixModifiedFiles,
store,
audit,
);
if (!finalized.ok) {
// Phantom-merge guard: the verification fix passed but no