fix: carry the retry work onto the new editor pipeline

Rebased onto main after 5a squash-merged, which also brought #7581's
rewrite of the upload chain. Where that moved first, this follows it:

- The AI escalation decision belongs to the local-pass engine now, so
  the localPassFailed gate and its store helper go. A pass that produces
  no verdict leaves the file eligible and a reload retries it, which is
  #7581's answer to what that gate was for.
- uploadChain.ts went the same way: main already orders the chain, as
  orderedRewritingCategories, and keeps its own nextUploadCategory. The
  policy retry resumes off main's ordering instead. An annotating policy
  is no longer in the chain, so a retry of one re-runs just that policy.
- aiEnabled was threaded through the retry path only to reach the chain
  ordering, which no longer takes it, so it goes from all three hooks.
- Test fixtures move from sources: ["editor"] to the runsOnEditor flag
  that replaced it.

The unlock hold-back is ported onto main's dispatch loop unchanged.
This commit is contained in:
EthanHealy01
2026-09-01 19:07:19 +01:00
parent 83256a0af4
commit 1021307d5a
9 changed files with 29 additions and 56 deletions
@@ -811,8 +811,6 @@ describe("retrying an attended policy run", () => {
{ policyId: "pol-1", fileId: "f-1" },
expect.any(File),
"f-unlocked",
// No app-config above this render, so the engine reads as off and Classification drops out.
false,
);
// And with the prefixed notification id, never a raw failure id.
expect(reportNotificationResolved).toHaveBeenCalledWith("failure:evt-1");
@@ -841,7 +839,6 @@ describe("retrying an attended policy run", () => {
{ policyId: "pol-1", fileId: "f-1" },
expect.any(File),
null,
false,
);
});
@@ -459,12 +459,5 @@ export function useNotificationActions(): ClientActionRegistry {
VIEW_FILE: viewFile,
VIEW_IN_PROCESSOR: viewInProcessor,
};
}, [
canOpenHere,
openInWorkbench,
fileContext,
fileStore,
navigate,
t,
]);
}, [canOpenHere, openInWorkbench, fileContext, fileStore, navigate, t]);
}
@@ -84,24 +84,6 @@ describe("policyRunStore", () => {
expect(isDispatched("security", "f1")).toBe(true);
});
it("a browser-local run does not claim the (policy, file) dispatch key", () => {
// The local classification heuristic records a run for the same (classification, file) pair
// the server escalation is keyed on. If that claimed the key, the auto-run would read
// "already dispatched" and never ask the AI - which killed escalation entirely.
recordRunStart(
rec({
runId: "local-classification-f1-1",
categoryId: "classification",
fileId: "f1",
target: "local",
browserLocal: true,
status: "RUNNING",
}),
);
expect(getRun("local-classification-f1-1")).toBeDefined();
expect(isDispatched("classification", "f1")).toBe(false);
});
it("a real backend run still claims the dispatch key", () => {
recordRunStart(rec({ runId: "srv-1", categoryId: "classification" }));
expect(isDispatched("classification", "f1")).toBe(true);
@@ -23,6 +23,7 @@ vi.mock("@app/hooks/usePolicies", () => ({
status: "active",
backendId: "backend-sec",
runOn: "upload",
runsOnEditor: true,
order: 0,
},
},
@@ -438,6 +438,16 @@ function applyOutputName(
: `${base}_${outputName}${ext}`;
}
/** Next upload policy in the chain, or undefined if last or no longer eligible. */
function nextUploadCategory(
orderedUploadCategories: string[],
categoryId: string,
): string | undefined {
const index = orderedUploadCategories.indexOf(categoryId);
if (index < 0) return undefined;
return orderedUploadCategories[index + 1];
}
async function reconcileServerRuns(
policies: PoliciesByCategory,
): Promise<void> {
@@ -151,7 +151,6 @@ describe("useResolutionContinuation", () => {
{ policyId: "pol-1", fileId: "f-locked" },
expect.any(File),
"f-unlocked",
true,
);
expect(refreshNotificationsNow).toHaveBeenCalled();
});
@@ -25,18 +25,13 @@ const RESOLUTION_TOOLS: Record<string, string> = {
};
export function useResolutionContinuation(): (run: SucceededToolRun) => void {
return useCallback(
(run: SucceededToolRun) => {
void continueResolutions(run);
},
[],
);
return useCallback((run: SucceededToolRun) => {
void continueResolutions(run);
}, []);
}
/** Best-effort: a continuation that cannot happen leaves the row where it already was. */
async function continueResolutions(
run: SucceededToolRun,
): Promise<void> {
async function continueResolutions(run: SucceededToolRun): Promise<void> {
try {
if (!(await couldResolveAnything(run))) return;
@@ -134,7 +134,7 @@ describe("rechainPolicyOnDocument", () => {
it("submits bytes the caller already holds, still under the original reference", async () => {
await expect(
rechainPolicyOnDocument(target, unlocked, "f-unlocked", false),
rechainPolicyOnDocument(target, unlocked, "f-unlocked"),
).resolves.toEqual({
ok: true,
tracked: true,
@@ -146,7 +146,7 @@ describe("rechainPolicyOnDocument", () => {
it("attributes the run to the adopted document, not the one the failure named", async () => {
// Two references: the failure's to the server so a repeat folds on, the adopted one to the store.
await rechainPolicyOnDocument(target, unlocked, "f-unlocked", false);
await rechainPolicyOnDocument(target, unlocked, "f-unlocked");
expect(runStoredPolicy).toHaveBeenCalledWith("pol-1", [unlocked], "f-1");
expect(getRun("run-1")).toMatchObject({ fileId: "f-unlocked" });
@@ -156,7 +156,7 @@ describe("rechainPolicyOnDocument", () => {
it("runs untracked rather than filing the output against the wrong document, and admits it", async () => {
// No workspace id, so recording it would version the encrypted original instead.
await expect(
rechainPolicyOnDocument(target, unlocked, null, false),
rechainPolicyOnDocument(target, unlocked, null),
).resolves.toEqual({ ok: true, tracked: false });
expect(runStoredPolicy).toHaveBeenCalled();
@@ -169,7 +169,7 @@ describe("rechainPolicyOnDocument", () => {
});
await expect(
rechainPolicyOnDocument(target, unlocked, "f-unlocked", false),
rechainPolicyOnDocument(target, unlocked, "f-unlocked"),
).resolves.toEqual({
ok: false,
reason: "rejected",
@@ -183,7 +183,7 @@ describe("rechainPolicyOnDocument", () => {
});
await expect(
rechainPolicyOnDocument(target, unlocked, "f-unlocked", false),
rechainPolicyOnDocument(target, unlocked, "f-unlocked"),
).resolves.toEqual({
ok: false,
reason: "rejected",
@@ -195,7 +195,7 @@ describe("rechainPolicyOnDocument", () => {
runStoredPolicy.mockRejectedValue(new Error("Network Error"));
await expect(
rechainPolicyOnDocument(target, unlocked, "f-unlocked", false),
rechainPolicyOnDocument(target, unlocked, "f-unlocked"),
).resolves.toEqual({
ok: false,
reason: "rejected",
@@ -215,7 +215,8 @@ describe("rechainPolicyOnDocument rejoining the chain", () => {
configured: true,
status: "active",
runOn: "upload",
sources: ["editor"],
sources: [],
runsOnEditor: true,
order,
};
}
@@ -254,7 +255,7 @@ describe("rechainPolicyOnDocument rejoining the chain", () => {
};
completedRun("run-w", "watermark", "f-upload", "f-1");
await rechainPolicyOnDocument(target, unlocked, "f-unlocked", false);
await rechainPolicyOnDocument(target, unlocked, "f-unlocked");
expect(runStoredPolicy).toHaveBeenCalledWith("pol-1", [unlocked], "f-1");
});
@@ -266,7 +267,7 @@ describe("rechainPolicyOnDocument rejoining the chain", () => {
security: uploadPolicy("pol-1", 1),
};
await rechainPolicyOnDocument(target, unlocked, "f-unlocked", false);
await rechainPolicyOnDocument(target, unlocked, "f-unlocked");
// Filed under the resumed policy's category, so security still runs on watermark's output.
expect(runStoredPolicy).toHaveBeenCalledWith("pol-w", [unlocked], "f-1");
@@ -283,7 +284,7 @@ describe("rechainPolicyOnDocument rejoining the chain", () => {
security: uploadPolicy("pol-1", 1),
};
await rechainPolicyOnDocument(target, unlocked, "f-unlocked", false);
await rechainPolicyOnDocument(target, unlocked, "f-unlocked");
expect(runStoredPolicy).toHaveBeenCalledWith("pol-1", [unlocked], "f-1");
});
@@ -295,7 +296,7 @@ describe("rechainPolicyOnDocument rejoining the chain", () => {
security: { ...uploadPolicy("pol-1", 1), runOn: "export" },
};
await rechainPolicyOnDocument(target, unlocked, "f-unlocked", false);
await rechainPolicyOnDocument(target, unlocked, "f-unlocked");
expect(runStoredPolicy).toHaveBeenCalledWith("pol-1", [unlocked], "f-1");
});
@@ -53,12 +53,7 @@ export async function rechainPolicyOnDocument(
document: File,
workspaceFileId: string | null,
): Promise<PolicyRerunOutcome> {
return submit(
target,
document,
workspaceFileId,
resumePointFor(target),
);
return submit(target, document, workspaceFileId, resumePointFor(target));
}
/** Which policy a retry should actually run, and the category to file its run under. */