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:
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user