Skip to content

Commit 83d59f3

Browse files
committed
fix(onboard): prune channels from reused sandbox selection
Fixes #9283 Signed-off-by: Deepak Jain <deepujain@gmail.com>
1 parent 588bb6d commit 83d59f3

3 files changed

Lines changed: 78 additions & 2 deletions

File tree

src/lib/onboard/machine/handlers/sandbox-messaging.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,7 @@ describe("reconcileReusedSandboxMessaging", () => {
359359
it("does not clear an equal recorded plan from a different authority", () => {
360360
const plan = telegramPlan(hashCredential("123456:registry-token") ?? "");
361361
const clearPlanEnv = vi.fn();
362+
vi.stubEnv("TELEGRAM_BOT_TOKEN", "123456:registry-token");
362363

363364
const result = reconcileReusedSandboxMessaging(
364365
structuredClone(plan),
@@ -371,7 +372,38 @@ describe("reconcileReusedSandboxMessaging", () => {
371372
expect(clearPlanEnv).not.toHaveBeenCalled();
372373
});
373374

375+
it("omits a removed host-backed channel when reusing an existing sandbox (#9283)", () => {
376+
const plan = discordPlan(hashCredential("previous-discord-token") ?? "");
377+
vi.stubEnv("DISCORD_BOT_TOKEN", "");
378+
379+
const result = reconcileReusedSandboxMessaging(
380+
plan,
381+
{ name: "openclaw" },
382+
{ clearPlanEnv() {} },
383+
);
384+
385+
expect(result).toEqual({ plan, selectedChannels: [], changed: false });
386+
});
387+
388+
it("keeps a lifecycle-selected channel when reusing an existing sandbox (#9283)", () => {
389+
const plan = {
390+
...discordPlan(hashCredential("previous-discord-token") ?? ""),
391+
workflow: "add-channel" as const,
392+
};
393+
vi.stubEnv("DISCORD_BOT_TOKEN", "");
394+
395+
const result = reconcileReusedSandboxMessaging(
396+
plan,
397+
{ name: "openclaw" },
398+
{ clearPlanEnv() {} },
399+
);
400+
401+
expect(result).toEqual({ plan, selectedChannels: ["discord"], changed: false });
402+
});
403+
374404
it("removes every unsupported channel artifact from a reused plan", () => {
405+
vi.stubEnv("TELEGRAM_BOT_TOKEN", "123456:registry-token");
406+
375407
const result = reconcileReusedSandboxMessaging(
376408
mixedChannelPlan(),
377409
{ name: "openclaw" },

src/lib/onboard/machine/handlers/sandbox-messaging.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,9 +434,16 @@ export function reconcileReusedSandboxMessaging<Agent>(
434434
const filtered = plan ? filterMessagingPlanForCurrentAgent(plan, agent) : null;
435435
const changed = !isDeepStrictEqual(filtered, recordedPlan);
436436
if (changed) deps.clearPlanEnv();
437-
return {
437+
const selection = {
438438
plan: filtered,
439439
selectedChannels: getActiveChannelsFromPlan(filtered),
440+
};
441+
const currentSelection =
442+
filtered && registryPlanRecordsLifecycleSelection(filtered)
443+
? selection
444+
: filterUnconfiguredHostChannelsFromSelection(selection, agent);
445+
return {
446+
...currentSelection,
440447
changed,
441448
};
442449
}

src/lib/onboard/machine/handlers/sandbox.test.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ import {
1111
} from "../../../state/onboard-checkpoint-decision";
1212
import { CHECKPOINT_SCHEMA_VERSION } from "../../../state/onboard-checkpoint-types";
1313
import { createSession, type Session } from "../../../state/onboard-session";
14-
import { detectMessagingChannelsFromEnv } from "../../messaging-channel-setup";
14+
import {
15+
detectMessagingChannelsFromEnv,
16+
detectUnconfiguredMessagingChannels,
17+
} from "../../messaging-channel-setup";
1518
import { handleSandboxState } from "./sandbox";
1619
import {
1720
baseOptions,
@@ -28,6 +31,7 @@ vi.mock("../../messaging-channel-setup", () => ({
2831
}));
2932

3033
const detectMessagingChannelsFromEnvMock = vi.mocked(detectMessagingChannelsFromEnv);
34+
const detectUnconfiguredMessagingChannelsMock = vi.mocked(detectUnconfiguredMessagingChannels);
3135

3236
function dcodeRegistryEntry(name: string, observabilityEnabled?: boolean) {
3337
return {
@@ -47,6 +51,7 @@ function dcodeRegistryEntry(name: string, observabilityEnabled?: boolean) {
4751
describe("handleSandboxState", () => {
4852
beforeEach(() => {
4953
detectMessagingChannelsFromEnvMock.mockReturnValue([]);
54+
detectUnconfiguredMessagingChannelsMock.mockReturnValue([]);
5055
});
5156

5257
it("creates a sandbox and records messaging/web search state", async () => {
@@ -603,6 +608,38 @@ describe("handleSandboxState", () => {
603608
expect(result.session).toBe(skippedSession);
604609
});
605610

611+
it("omits an unconfigured host-backed channel when reusing a Ready sandbox (#9283)", async () => {
612+
const registryPlan = makeMinimalPlan("saved", "openclaw", ["discord"]);
613+
const session = createSession({ sandboxName: "saved", messagingPlan: registryPlan });
614+
session.steps.sandbox.status = "complete";
615+
vi.stubEnv("DISCORD_BOT_TOKEN", "");
616+
detectUnconfiguredMessagingChannelsMock.mockReturnValue(["discord"]);
617+
const { deps, calls } = createDeps({
618+
getSandboxReuseState: () => "ready",
619+
getSandboxRegistryEntry: () => ({
620+
name: "saved",
621+
pendingRouteReservation: true,
622+
provider: "provider",
623+
model: "model",
624+
endpointUrl: null,
625+
preferredInferenceApi: "openai-completions",
626+
toolDisclosure: "progressive",
627+
fromDockerfile: null,
628+
hermesAuthMethod: null,
629+
}),
630+
getRegistrySandboxMessagingAuthority: () => ({ authoritative: true, plan: registryPlan }),
631+
});
632+
633+
const result = await handleSandboxState({
634+
...baseOptions(deps, session),
635+
resume: true,
636+
sandboxName: "saved",
637+
});
638+
639+
expect(calls.createSandbox).not.toHaveBeenCalled();
640+
expect(result.selectedMessagingChannels).toEqual([]);
641+
});
642+
606643
it("treats checkpoint machine-state progress past sandbox as step-complete even when the legacy step status is stale (#6228)", async () => {
607644
const session = createSession({
608645
sandboxName: "saved",

0 commit comments

Comments
 (0)