Skip to content

Commit ce9e393

Browse files
authored
fix: make Back work within channel setup (#108007)
* fix: support Back within channel setup * docs: note channel setup Back navigation * fix: keep navigation outcome type private * style: format navigation outcome type * chore: leave changelog to release prep
1 parent 94a2494 commit ce9e393

9 files changed

Lines changed: 1037 additions & 84 deletions

File tree

extensions/reef/src/setup.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { reefSetupWizard } from "./setup.js";
1313
import {
1414
finalizeReefIdentityBinding,
1515
generateAndStoreKeys,
16+
loadKeys,
1617
loadReefIdentityBinding,
1718
reserveReefIdentityBinding,
1819
} from "./state.js";
@@ -170,4 +171,39 @@ describe("Reef setup wizard identity binding", () => {
170171
relayUrl: "https://reefwire.ai",
171172
});
172173
});
174+
175+
it("declares its persistence boundary before writing keys or creating the handle", async () => {
176+
const runtime = installRuntime();
177+
const beforePersistentEffect = vi.fn(async () => {
178+
await expect(loadKeys(runtime)).rejects.toMatchObject({ code: "ENOENT" });
179+
});
180+
vi.spyOn(ReefTransportClient.prototype, "createHandle").mockImplementation(async () => {
181+
expect(beforePersistentEffect).toHaveBeenCalledTimes(1);
182+
return { handle: "molty", key_epoch: 1 };
183+
});
184+
const textAnswers = [
185+
"https://reefwire.ai",
186+
187+
"setup-session",
188+
"molty",
189+
"gpt-5.6-terra",
190+
"REEF_GUARD_OPENAI_KEY",
191+
"reef-v1",
192+
];
193+
const selectAnswers = ["code-only", "openai"];
194+
const prompter = {
195+
note: vi.fn(async () => undefined),
196+
text: vi.fn(async () => textAnswers.shift() ?? ""),
197+
select: vi.fn(async () => selectAnswers.shift()),
198+
};
199+
200+
await reefSetupWizard.configureInteractive({
201+
cfg: {},
202+
prompter: prompter as never,
203+
options: { beforePersistentEffect },
204+
});
205+
206+
expect(beforePersistentEffect).toHaveBeenCalledTimes(1);
207+
await expect(loadKeys(runtime)).resolves.toBeDefined();
208+
});
173209
});

extensions/reef/src/setup.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,15 @@ export const reefSetupWizard = {
6969
};
7070
},
7171
configure: async ({ cfg }: { cfg: OpenClawConfig }) => ({ cfg }),
72-
configureInteractive: async ({ cfg, prompter }: { cfg: OpenClawConfig; prompter: Prompt }) => {
72+
configureInteractive: async ({
73+
cfg,
74+
prompter,
75+
options,
76+
}: {
77+
cfg: OpenClawConfig;
78+
prompter: Prompt;
79+
options?: { beforePersistentEffect?: () => Promise<void> };
80+
}) => {
7381
const rawRelayUrl = await prompter.text({
7482
message: "Reef relay origin URL",
7583
initialValue: "https://reefwire.ai",
@@ -125,6 +133,7 @@ export const reefSetupWizard = {
125133
);
126134
}
127135
const configuredStateDir = (cfg.channels?.reef as { stateDir?: unknown } | undefined)?.stateDir;
136+
await options?.beforePersistentEffect?.();
128137
const keys = await loadKeys(runtime).catch(async (error: unknown) => {
129138
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
130139
throw error;

src/commands/channels.add.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,40 @@ describe("channelsAddCommand", () => {
478478
expect(channelWizardMocks.prompter.outro).toHaveBeenCalledWith("No channel changes made.");
479479
});
480480

481+
it("persists an accepted plugin install after setup returns to an empty selection", async () => {
482+
const config: OpenClawConfig = { channels: {} };
483+
const installedConfig: OpenClawConfig = {
484+
...config,
485+
plugins: {
486+
entries: { "external-chat": { enabled: true } },
487+
installs: {
488+
"external-chat": {
489+
source: "npm",
490+
spec: "@vendor/[email protected]",
491+
},
492+
},
493+
},
494+
};
495+
configMocks.readConfigFileSnapshot.mockResolvedValue({
496+
...baseConfigSnapshot,
497+
sourceConfig: config,
498+
config,
499+
});
500+
channelWizardMocks.setupChannels.mockResolvedValueOnce(installedConfig);
501+
502+
await channelsAddCommand({}, runtime, { hasFlags: false });
503+
504+
expect(
505+
pluginInstallRecordCommitMocks.commitConfigWithPendingPluginInstalls,
506+
).toHaveBeenCalledWith(expect.objectContaining({ nextConfig: installedConfig }));
507+
expect(
508+
pluginInstallRecordCommitMocks.commitConfigWithPendingPluginInstalls,
509+
).toHaveBeenCalledOnce();
510+
expect(configMocks.writeConfigFile).toHaveBeenCalledWith(installedConfig);
511+
expect(channelWizardMocks.prompter.confirm).not.toHaveBeenCalled();
512+
expect(channelWizardMocks.prompter.outro).toHaveBeenCalledWith("Channels updated.");
513+
});
514+
481515
it("preselects an installable catalog channel in guided setup", async () => {
482516
const config: OpenClawConfig = { channels: {} };
483517
configMocks.readConfigFileSnapshot.mockResolvedValue({

src/commands/channels/add-wizard.ts

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,36 @@ export async function runChannelsAddWizardFlow(params: ChannelsAddWizardFlowPara
102102
resolvedPlugins.set(channel, plugin);
103103
},
104104
});
105+
const commitWizardConfig = async (config: OpenClawConfig) => {
106+
await params.beforePersistentEffect?.();
107+
const committed = await commitConfigWithPendingPluginInstalls({
108+
nextConfig: config,
109+
...(baseHash !== undefined ? { baseHash } : {}),
110+
});
111+
if (committed.movedInstallRecords) {
112+
await refreshPluginRegistryAfterConfigMutation({
113+
config: committed.config,
114+
reason: "source-changed",
115+
installRecords: committed.installRecords,
116+
logger: { warn: (message) => runtime.log(message) },
117+
});
118+
}
119+
await onboardChannels.runCollectedChannelOnboardingPostWriteHooks({
120+
hooks: postWriteHooks.drain(),
121+
cfg: committed.config,
122+
runtime,
123+
...(params.beforePersistentEffect
124+
? { beforePersistentEffect: params.beforePersistentEffect }
125+
: {}),
126+
});
127+
return committed.config;
128+
};
105129
if (selection.length === 0) {
130+
if (nextConfig !== cfg) {
131+
await commitWizardConfig(nextConfig);
132+
await prompter.outro("Channels updated.");
133+
return;
134+
}
106135
await prompter.outro("No channel changes made.");
107136
return;
108137
}
@@ -198,28 +227,7 @@ export async function runChannelsAddWizardFlow(params: ChannelsAddWizardFlowPara
198227
}
199228
}
200229

201-
await params.beforePersistentEffect?.();
202-
const committed = await commitConfigWithPendingPluginInstalls({
203-
nextConfig,
204-
...(baseHash !== undefined ? { baseHash } : {}),
205-
});
206-
const writtenConfig = committed.config;
207-
if (committed.movedInstallRecords) {
208-
await refreshPluginRegistryAfterConfigMutation({
209-
config: writtenConfig,
210-
reason: "source-changed",
211-
installRecords: committed.installRecords,
212-
logger: { warn: (message) => runtime.log(message) },
213-
});
214-
}
215-
await onboardChannels.runCollectedChannelOnboardingPostWriteHooks({
216-
hooks: postWriteHooks.drain(),
217-
cfg: writtenConfig,
218-
runtime,
219-
...(params.beforePersistentEffect
220-
? { beforePersistentEffect: params.beforePersistentEffect }
221-
: {}),
222-
});
230+
await commitWizardConfig(nextConfig);
223231
params.onConfigured?.(
224232
selection.map((channel) => ({
225233
channel,
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import type { SetupChannelsOptions } from "../channels/plugins/setup-wizard-types.js";
2+
import { ensureChannelSetupPluginInstalled } from "../commands/channel-setup/plugin-install.js";
3+
import { runWizardWithPromptNavigationScope } from "../wizard/navigation-prompter.js";
4+
import type { WizardPrompter } from "../wizard/prompts.js";
5+
6+
type ScopedChannelStepParams<T> = {
7+
prompter: WizardPrompter;
8+
options?: SetupChannelsOptions;
9+
runner: (prompter: WizardPrompter, options: SetupChannelsOptions) => Promise<T>;
10+
onPersistentEffect?: () => void;
11+
};
12+
13+
export async function runScopedChannelStep<T>(params: ScopedChannelStepParams<T>) {
14+
return await runWizardWithPromptNavigationScope(params.prompter, async (scopedPrompter) =>
15+
params.runner(scopedPrompter, {
16+
...params.options,
17+
beforePersistentEffect: async () => {
18+
params.onPersistentEffect?.();
19+
scopedPrompter.disableBackNavigation?.();
20+
await params.options?.beforePersistentEffect?.();
21+
},
22+
}),
23+
);
24+
}
25+
26+
type ChannelPluginInstallParams = Omit<
27+
Parameters<typeof ensureChannelSetupPluginInstalled>[0],
28+
"prompter" | "beforePersistentEffect"
29+
>;
30+
31+
export async function ensureChannelSetupPluginInstalledWithNavigation(params: {
32+
install: ChannelPluginInstallParams;
33+
prompter: WizardPrompter;
34+
options?: SetupChannelsOptions;
35+
}) {
36+
let persistentEffectStarted = false;
37+
const outcome = await runScopedChannelStep({
38+
prompter: params.prompter,
39+
options: params.options,
40+
runner: async (scopedPrompter, scopedOptions) =>
41+
await ensureChannelSetupPluginInstalled({
42+
...params.install,
43+
prompter: scopedPrompter,
44+
beforePersistentEffect: scopedOptions.beforePersistentEffect,
45+
}),
46+
onPersistentEffect: () => {
47+
persistentEffectStarted = true;
48+
},
49+
});
50+
return { ...outcome, persistentEffectStarted };
51+
}

0 commit comments

Comments
 (0)