From 6ead94c4fe6d1ddbe4d8950879e63912636e0775 Mon Sep 17 00:00:00 2001 From: ScarabSystems Date: Mon, 20 Jul 2026 13:54:07 -0400 Subject: [PATCH 1/3] fix(onboard): require policy presets before resume skip Signed-off-by: ScarabSystems --- .../onboard/machine/handlers/policies.test.ts | 28 +++++++++++++++++++ src/lib/onboard/machine/handlers/policies.ts | 1 + 2 files changed, 29 insertions(+) diff --git a/src/lib/onboard/machine/handlers/policies.test.ts b/src/lib/onboard/machine/handlers/policies.test.ts index 5a916af4f40..38c24a559a8 100644 --- a/src/lib/onboard/machine/handlers/policies.test.ts +++ b/src/lib/onboard/machine/handlers/policies.test.ts @@ -212,6 +212,34 @@ describe("handlePoliciesState", () => { }); }); + it("does not resume policies when recorded presets are empty (#6042)", async () => { + const session = createSession({ policyPresets: [] }); + const { deps, calls, setSession } = createDeps({ + arePolicyPresetsApplied: vi.fn(() => true), + }); + setSession(session); + + const result = await handlePoliciesState({ ...baseOptions(deps), resume: true }); + + expect(calls.skipped).not.toHaveBeenCalled(); + expect(calls.recordSkip).not.toHaveBeenCalled(); + expect(calls.startStep).toHaveBeenCalledWith("policies", { + sandboxName: "my-assistant", + provider: "provider", + model: "model", + policyPresets: [], + }); + expect(calls.setupPolicies).toHaveBeenCalledWith( + "my-assistant", + expect.objectContaining({ selectedPresets: [] }), + ); + expect(calls.complete).toHaveBeenCalledWith( + "policies", + expect.objectContaining({ policyPresets: ["npm"] }), + ); + expect(result.appliedPolicyPresets).toEqual(["npm"]); + }); + it("reconciles unsupported recorded presets before interactive setup", async () => { const session = createSession({ policyPresets: ["npm", "unsupported"] }); const { deps, calls, setSession } = createDeps(); diff --git a/src/lib/onboard/machine/handlers/policies.ts b/src/lib/onboard/machine/handlers/policies.ts index 84f67c2ca8a..d08b0567da8 100644 --- a/src/lib/onboard/machine/handlers/policies.ts +++ b/src/lib/onboard/machine/handlers/policies.ts @@ -187,6 +187,7 @@ export async function handlePoliciesState({ const recordedPolicyPresetsForSupport = policyResumeSelection.policyPresets; const resumePolicies = resume && + recordedPolicyPresetsForSupport.length > 0 && !policyResumeSelection.recordedPolicyPresetsNeedReconcile && !policyResumeSelection.disabledMessagingPolicyPresetApplied && !policyResumeSelection.suppressedAgentRequiredPresetsLive && From 7f8548d02ea9880a6e2df9d59b3b96479c003c81 Mon Sep 17 00:00:00 2001 From: ScarabSystems Date: Mon, 20 Jul 2026 14:33:20 -0400 Subject: [PATCH 2/3] fix(onboard): gate resume on recorded policy presets Signed-off-by: ScarabSystems --- .../onboard/machine/handlers/policies.test.ts | 29 +++++++++++++++++++ src/lib/onboard/machine/handlers/policies.ts | 3 +- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/lib/onboard/machine/handlers/policies.test.ts b/src/lib/onboard/machine/handlers/policies.test.ts index 38c24a559a8..8a5a2d7cb83 100644 --- a/src/lib/onboard/machine/handlers/policies.test.ts +++ b/src/lib/onboard/machine/handlers/policies.test.ts @@ -240,6 +240,35 @@ describe("handlePoliciesState", () => { expect(result.appliedPolicyPresets).toEqual(["npm"]); }); + it("does not resume policies when empty recorded presets expand to required presets (#6042)", async () => { + const session = createSession({ policyPresets: [] }); + const prepareResume = vi.fn((_sandboxName, options) => ({ + policyPresets: [...(options.recordedPolicyPresets ?? []), ...options.hermesToolGateways], + recordedPolicyPresetsNeedReconcile: false, + disabledMessagingPolicyPresetApplied: false, + suppressedAgentRequiredPresetsLive: false, + })); + const { deps, calls, setSession } = createDeps({ + arePolicyPresetsApplied: vi.fn(() => true), + preparePolicyPresetResumeSelection: prepareResume, + }); + setSession(session); + + const result = await handlePoliciesState({ + ...baseOptions(deps), + resume: true, + hermesToolGateways: ["github"], + }); + + expect(calls.skipped).not.toHaveBeenCalled(); + expect(calls.recordSkip).not.toHaveBeenCalled(); + expect(calls.setupPolicies).toHaveBeenCalledWith( + "my-assistant", + expect.objectContaining({ selectedPresets: ["github"] }), + ); + expect(result.appliedPolicyPresets).toEqual(["npm"]); + }); + it("reconciles unsupported recorded presets before interactive setup", async () => { const session = createSession({ policyPresets: ["npm", "unsupported"] }); const { deps, calls, setSession } = createDeps(); diff --git a/src/lib/onboard/machine/handlers/policies.ts b/src/lib/onboard/machine/handlers/policies.ts index d08b0567da8..05c323c5324 100644 --- a/src/lib/onboard/machine/handlers/policies.ts +++ b/src/lib/onboard/machine/handlers/policies.ts @@ -187,7 +187,8 @@ export async function handlePoliciesState({ const recordedPolicyPresetsForSupport = policyResumeSelection.policyPresets; const resumePolicies = resume && - recordedPolicyPresetsForSupport.length > 0 && + recordedPolicyPresets !== null && + recordedPolicyPresets.length > 0 && !policyResumeSelection.recordedPolicyPresetsNeedReconcile && !policyResumeSelection.disabledMessagingPolicyPresetApplied && !policyResumeSelection.suppressedAgentRequiredPresetsLive && From 47d3546ce5448a55b86c809c186175f64826304c Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Sat, 25 Jul 2026 03:19:30 -0700 Subject: [PATCH 3/3] test(onboard): use real policy resume contracts Signed-off-by: Prekshi Vyas --- .../onboard/machine/handlers/policies.test.ts | 57 ------------------- src/lib/onboard/machine/handlers/policies.ts | 2 - .../onboard/policy-resume-selection.test.ts | 21 ++++++- test/onboard.test.ts | 6 ++ 4 files changed, 24 insertions(+), 62 deletions(-) diff --git a/src/lib/onboard/machine/handlers/policies.test.ts b/src/lib/onboard/machine/handlers/policies.test.ts index 8a5a2d7cb83..5a916af4f40 100644 --- a/src/lib/onboard/machine/handlers/policies.test.ts +++ b/src/lib/onboard/machine/handlers/policies.test.ts @@ -212,63 +212,6 @@ describe("handlePoliciesState", () => { }); }); - it("does not resume policies when recorded presets are empty (#6042)", async () => { - const session = createSession({ policyPresets: [] }); - const { deps, calls, setSession } = createDeps({ - arePolicyPresetsApplied: vi.fn(() => true), - }); - setSession(session); - - const result = await handlePoliciesState({ ...baseOptions(deps), resume: true }); - - expect(calls.skipped).not.toHaveBeenCalled(); - expect(calls.recordSkip).not.toHaveBeenCalled(); - expect(calls.startStep).toHaveBeenCalledWith("policies", { - sandboxName: "my-assistant", - provider: "provider", - model: "model", - policyPresets: [], - }); - expect(calls.setupPolicies).toHaveBeenCalledWith( - "my-assistant", - expect.objectContaining({ selectedPresets: [] }), - ); - expect(calls.complete).toHaveBeenCalledWith( - "policies", - expect.objectContaining({ policyPresets: ["npm"] }), - ); - expect(result.appliedPolicyPresets).toEqual(["npm"]); - }); - - it("does not resume policies when empty recorded presets expand to required presets (#6042)", async () => { - const session = createSession({ policyPresets: [] }); - const prepareResume = vi.fn((_sandboxName, options) => ({ - policyPresets: [...(options.recordedPolicyPresets ?? []), ...options.hermesToolGateways], - recordedPolicyPresetsNeedReconcile: false, - disabledMessagingPolicyPresetApplied: false, - suppressedAgentRequiredPresetsLive: false, - })); - const { deps, calls, setSession } = createDeps({ - arePolicyPresetsApplied: vi.fn(() => true), - preparePolicyPresetResumeSelection: prepareResume, - }); - setSession(session); - - const result = await handlePoliciesState({ - ...baseOptions(deps), - resume: true, - hermesToolGateways: ["github"], - }); - - expect(calls.skipped).not.toHaveBeenCalled(); - expect(calls.recordSkip).not.toHaveBeenCalled(); - expect(calls.setupPolicies).toHaveBeenCalledWith( - "my-assistant", - expect.objectContaining({ selectedPresets: ["github"] }), - ); - expect(result.appliedPolicyPresets).toEqual(["npm"]); - }); - it("reconciles unsupported recorded presets before interactive setup", async () => { const session = createSession({ policyPresets: ["npm", "unsupported"] }); const { deps, calls, setSession } = createDeps(); diff --git a/src/lib/onboard/machine/handlers/policies.ts b/src/lib/onboard/machine/handlers/policies.ts index 05c323c5324..84f67c2ca8a 100644 --- a/src/lib/onboard/machine/handlers/policies.ts +++ b/src/lib/onboard/machine/handlers/policies.ts @@ -187,8 +187,6 @@ export async function handlePoliciesState({ const recordedPolicyPresetsForSupport = policyResumeSelection.policyPresets; const resumePolicies = resume && - recordedPolicyPresets !== null && - recordedPolicyPresets.length > 0 && !policyResumeSelection.recordedPolicyPresetsNeedReconcile && !policyResumeSelection.disabledMessagingPolicyPresetApplied && !policyResumeSelection.suppressedAgentRequiredPresetsLive && diff --git a/src/lib/onboard/policy-resume-selection.test.ts b/src/lib/onboard/policy-resume-selection.test.ts index cde2b2f385b..ae7bcf77919 100644 --- a/src/lib/onboard/policy-resume-selection.test.ts +++ b/src/lib/onboard/policy-resume-selection.test.ts @@ -10,9 +10,9 @@ type Preset = { name: string; access?: string }; function policies( options: { applied?: string[]; custom?: string[]; customOwnsObservability?: boolean } = {}, ) { - const setupPresets = ["npm", "brave", "tavily", "observability-otlp-local"].map((name) => ({ - name, - })); + const setupPresets = ["npm", "brave", "tavily", "slack", "observability-otlp-local"].map( + (name) => ({ name }), + ); const customPresets = (options.custom ?? []).map((name) => ({ name })); return { setupPolicyPresetSupported: () => true, @@ -87,6 +87,21 @@ describe("preparePolicyPresetResumeSelection web search reconciliation", () => { }); }); +describe("preparePolicyPresetResumeSelection required preset reconciliation", () => { + it("marks an empty recording for reconciliation when Slack becomes required (#6042)", () => { + const result = preparePolicyPresetResumeSelection({ policies: policies() }, "alpha", { + recordedPolicyPresets: [], + enabledChannels: ["slack"], + agent: "openclaw", + webSearchConfig: null, + webSearchSupported: true, + }); + + expect(result.policyPresets).toEqual(["slack"]); + expect(result.recordedPolicyPresetsNeedReconcile).toBe(true); + }); +}); + describe("preparePolicyPresetResumeSelection tier-default preservation (#6844)", () => { // These exercise the real tiers.yaml through classifyPresetProvenance (no tier // stub): `brave` is a Balanced default, and Restricted lists no such default. diff --git a/test/onboard.test.ts b/test/onboard.test.ts index 669455f9f91..b929f1ef725 100644 --- a/test/onboard.test.ts +++ b/test/onboard.test.ts @@ -53,6 +53,7 @@ type OnboardTestInternals = { session: T, selectedAgentName: string, ) => T; + arePolicyPresetsApplied: (sandboxName: string, selectedPresets?: string[]) => boolean; pullAndResolveBaseImageDigest: () => { digest: string | null; ref: string } | null; createSetupInference: (overrides?: Partial) => SetupInference; SANDBOX_BASE_IMAGE: string; @@ -90,6 +91,7 @@ const { getResumeConfigConflicts, getResumeSandboxConflict, clearAgentScopedResumeState, + arePolicyPresetsApplied, createSetupInference, SANDBOX_BASE_IMAGE, } = onboardTestInternals; @@ -98,6 +100,10 @@ const createDirectSetupInferenceHarness = createDirectSetupInferenceHarnessFactory(createSetupInference); describe("onboard helpers", () => { + it("does not treat an empty policy preset selection as already applied (#6042)", () => { + expect(arePolicyPresetsApplied("unused", [])).toBe(false); + }); + it("adds host proxy variables to sandbox startup env args", () => { const envArgs = ["CHAT_UI_URL=http://127.0.0.1:18789"];