From 0fd3da7cd26ceb8d24842de810000639dc723cad Mon Sep 17 00:00:00 2001 From: James Brunton Date: Fri, 28 Aug 2026 10:58:01 +0100 Subject: [PATCH] Fix classification policy activation --- .../policies/useClassificationPolicy.test.tsx | 24 +++++++++++++++++++ .../policies/useClassificationPolicy.ts | 3 ++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/frontend/editor/src/proprietary/components/policies/useClassificationPolicy.test.tsx b/frontend/editor/src/proprietary/components/policies/useClassificationPolicy.test.tsx index 3e5f175bdb..bc9a688d07 100644 --- a/frontend/editor/src/proprietary/components/policies/useClassificationPolicy.test.tsx +++ b/frontend/editor/src/proprietary/components/policies/useClassificationPolicy.test.tsx @@ -18,6 +18,7 @@ interface TestStub { } const aiEnabled = vi.hoisted(() => ({ value: false })); +const runOn = vi.hoisted(() => ({ value: "upload" as "upload" | "export" })); const mocks = vi.hoisted(() => ({ workspace: [] as Array<{ @@ -57,6 +58,7 @@ vi.mock("@app/hooks/usePolicies", () => ({ runsOnEditor: true, status: "active", backendId: "backend-classification", + runOn: runOn.value, sources: ["editor"], }, }, @@ -119,6 +121,7 @@ describe("useClassificationPolicy delivery", () => { ); mocks.classify.mockReset(); aiEnabled.value = false; + runOn.value = "upload"; mocks.runPolicyOnFile.mockReset(); mocks.runPolicyOnFile.mockResolvedValue(undefined); }); @@ -327,4 +330,25 @@ describe("useClassificationPolicy delivery", () => { ); expect(mocks.runPolicyOnFile).not.toHaveBeenCalled(); }); + + it("does not run on upload when the policy is set to run on export", async () => { + // An export-time policy is enforced by the export path, not this upload engine, so an upload + // must not classify, meter, or escalate. + runOn.value = "export"; + aiEnabled.value = true; + mocks.workspace = [stub("a")]; + mocks.classify.mockResolvedValue({ + labels: ["invoice"], + confidence: "low", + }); + + renderHook(() => useClassificationPolicy()); + + // Give the (immediate) idle path a beat to prove it stays silent. + await new Promise((r) => setTimeout(r, 50)); + expect(mocks.classify).not.toHaveBeenCalled(); + expect(mocks.updateStirlingFileStub).not.toHaveBeenCalled(); + expect(mocks.meter).not.toHaveBeenCalled(); + expect(mocks.runPolicyOnFile).not.toHaveBeenCalled(); + }); }); diff --git a/frontend/editor/src/proprietary/components/policies/useClassificationPolicy.ts b/frontend/editor/src/proprietary/components/policies/useClassificationPolicy.ts index ad5fd8ea46..c0f6cbe874 100644 --- a/frontend/editor/src/proprietary/components/policies/useClassificationPolicy.ts +++ b/frontend/editor/src/proprietary/components/policies/useClassificationPolicy.ts @@ -77,7 +77,8 @@ export function useClassificationPolicy(): void { policy?.configured && policy.status === "active" && backendId && - policy.runsOnEditor, + policy.runsOnEditor && + (policy.runOn ?? "upload") === "upload", ); // The file-producing policies whose chain classification waits behind: it runs on the last one's