diff --git a/CHANGELOG.md b/CHANGELOG.md index cb20ebde6112..c736841df292 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- **Guided onboarding skip-UI routing:** keep `openclaw onboard --skip-ui` and `openclaw setup --skip-ui` on guided onboarding while skipping both browser and terminal handoffs, instead of silently switching to the classic wizard. Thanks @shakkernerd. - **Telegram durable ingress:** preserve pre-identity control-lane ownership during replay and attempt each drain snapshot row only once per pass, preventing targeted commands from spinning the spool and blocking polling shutdown. - **Control UI operator session permissions:** honor Gateway-advertised operator scopes for new-thread creation, thread management, checkpoints, and sharing controls while preserving read-only navigation and legacy Gateway compatibility. Fixes #117786. Thanks @shakkernerd. - **Control UI delayed session commands:** bind slash-command mutations and confirmed resets to their originating Gateway, recheck current operator scopes after asynchronous work, and retain reset authorization through queued delivery so reconnects cannot target a replacement connection. Thanks @shakkernerd. diff --git a/scripts/e2e/lib/onboard/scenario.sh b/scripts/e2e/lib/onboard/scenario.sh index 4f2e6b5ce910..443b09603104 100644 --- a/scripts/e2e/lib/onboard/scenario.sh +++ b/scripts/e2e/lib/onboard/scenario.sh @@ -24,6 +24,7 @@ export GATEWAY_LOG_PATH mkdir -p "$OPENCLAW_E2E_LOG_DIR" cleanup_onboard_artifacts() { openclaw_e2e_stop_process "${GATEWAY_PID:-}" + openclaw_e2e_stop_process "${mock_openai_pid:-}" rm -rf "$ONBOARD_TMP_DIR" } if [ "$OPENCLAW_ONBOARD_SCENARIO_SOURCE_ONLY" != "1" ]; then @@ -241,6 +242,72 @@ send_skills_flow() { send "" 2.0 } +send_guided_skip_ui_flow() { + wait_for_log "How should I set things up?" 120 || return $? + send $'\r' 0.8 + wait_for_log "Use Current model?" 120 || return $? + send $'\r' 0.8 +} + +validate_guided_skip_ui_log() { + local log_path="$1" + local mock_request_log="$2" + log_contains "Hi — I'm OpenClaw. I keep this system running. Let's get you set up." || { + echo "Guided onboarding introduction was not rendered" + return 1 + } + log_contains "OpenClaw is ready." || { + echo "Guided onboarding did not reach its skip-UI completion" + return 1 + } + if log_contains "Opening the Control UI dashboard"; then + echo "Guided skip-UI onboarding attempted a browser handoff" + return 1 + fi + if log_contains "Your browser is ready"; then + echo "Guided skip-UI onboarding completed a browser handoff" + return 1 + fi + if log_contains "Hatching your agent now"; then + echo "Guided skip-UI onboarding attempted a terminal handoff" + return 1 + fi + grep -q '"/v1/responses"' "$mock_request_log" || { + echo "Guided onboarding did not verify the configured model through /v1/responses" + return 1 + } +} + +run_case_guided_skip_ui() { + local mock_port="19091" + local mock_log="$ONBOARD_TMP_DIR/guided-skip-ui-mock-openai.log" + local mock_request_log="$ONBOARD_TMP_DIR/guided-skip-ui-mock-requests.jsonl" + set_isolated_openclaw_env guided-skip-ui + export OPENAI_API_KEY="sk-openclaw-guided-skip-ui-e2e" + node scripts/e2e/lib/onboard/write-config.mjs \ + guided-skip-ui \ + "$OPENCLAW_CONFIG_PATH" \ + "$OPENCLAW_TEST_WORKSPACE_DIR" \ + "$mock_port" + mock_openai_pid="$( + openclaw_e2e_start_tracked_process \ + "$mock_log" \ + env MOCK_PORT="$mock_port" MOCK_REQUEST_LOG="$mock_request_log" \ + node scripts/e2e/mock-openai-server.mjs + )" + openclaw_e2e_wait_mock_openai "$mock_port" + + run_wizard_cmd \ + guided-skip-ui \ + "$HOME" \ + "node \"$OPENCLAW_ENTRY\" onboard --skip-ui" \ + send_guided_skip_ui_flow + + validate_guided_skip_ui_log "$WIZARD_LOG_PATH" "$mock_request_log" + openclaw_e2e_stop_process "$mock_openai_pid" + mock_openai_pid="" +} + run_case_local_basic() { set_isolated_openclaw_env local-basic openclaw_e2e_run_logged local-basic node "$OPENCLAW_ENTRY" onboard \ @@ -325,6 +392,7 @@ validate_local_basic_log() { } if [ "$OPENCLAW_ONBOARD_SCENARIO_SOURCE_ONLY" != "1" ]; then + run_case_guided_skip_ui run_case_local_basic run_case_remote_non_interactive run_case_reset diff --git a/scripts/e2e/lib/onboard/write-config.mjs b/scripts/e2e/lib/onboard/write-config.mjs index d77996b4fd4d..027e60f3a452 100644 --- a/scripts/e2e/lib/onboard/write-config.mjs +++ b/scripts/e2e/lib/onboard/write-config.mjs @@ -1,19 +1,38 @@ // Config writer helper for onboard E2E scenarios. import fs from "node:fs"; +import { applyMockOpenAiModelConfig } from "../fixtures/mock-openai-config.mjs"; -const [scenario, configPath] = process.argv.slice(2); +const [scenario, configPath, ...scenarioArgs] = process.argv.slice(2); if (!scenario || !configPath) { - throw new Error("usage: write-config.mjs "); + throw new Error("usage: write-config.mjs [...args]"); } -const config = { - reset: { - meta: {}, - agents: { defaults: { workspace: "/root/old" } }, - gateway: { mode: "remote", remote: { url: "ws://old.example:18789", token: "old-token" } }, - }, - skills: { meta: {}, skills: { allowBundled: ["__none__"], install: { nodeManager: "bun" } } }, -}[scenario]; +let config; +if (scenario === "guided-skip-ui") { + const [workspace, mockPort] = scenarioArgs; + if (!workspace || !mockPort) { + throw new Error("guided-skip-ui requires workspace and mock OpenAI port arguments"); + } + config = { + gateway: { mode: "local", bind: "loopback", controlUi: { enabled: false } }, + agents: { defaults: { workspace } }, + wizard: { + securityAcknowledgedAt: "2026-01-01T00:00:00.000Z", + accessMode: "full", + appRecommendations: false, + }, + }; + applyMockOpenAiModelConfig(config, { mockPort }); +} else { + config = { + reset: { + meta: {}, + agents: { defaults: { workspace: "/root/old" } }, + gateway: { mode: "remote", remote: { url: "ws://old.example:18789", token: "old-token" } }, + }, + skills: { meta: {}, skills: { allowBundled: ["__none__"], install: { nodeManager: "bun" } } }, + }[scenario]; +} if (!config) { throw new Error(`unknown config scenario: ${scenario}`); } diff --git a/src/cli/program/register.onboard.test.ts b/src/cli/program/register.onboard.test.ts index 296265f1138d..80ba79fc25f6 100644 --- a/src/cli/program/register.onboard.test.ts +++ b/src/cli/program/register.onboard.test.ts @@ -210,6 +210,12 @@ describe("registerOnboardCommand", () => { expect(setupWizardOptions().tui).toBe(true); }); + it("forwards --skip-ui to guided onboarding", async () => { + await runCli(["onboard", "--skip-ui"]); + + expect(setupWizardOptions().skipUi).toBe(true); + }); + it("rejects conflicting custom model input capabilities", async () => { await runCli(["onboard", "--custom-image-input", "--custom-text-input"]); diff --git a/src/cli/program/register.setup.test.ts b/src/cli/program/register.setup.test.ts index 17feaca76ec8..74ecf865556d 100644 --- a/src/cli/program/register.setup.test.ts +++ b/src/cli/program/register.setup.test.ts @@ -378,6 +378,13 @@ describe("registerSetupCommand", () => { expect(setupCommandMock).not.toHaveBeenCalled(); }); + it("forwards --skip-ui through the canonical onboarding path", async () => { + await runCli(["setup", "--skip-ui"]); + + expect(lastWizardOptions()?.skipUi).toBe(true); + expect(setupCommandMock).not.toHaveBeenCalled(); + }); + it("rejects conflicting custom model input capabilities", async () => { await runCli(["setup", "--custom-image-input", "--custom-text-input"]); diff --git a/src/commands/onboard.test.ts b/src/commands/onboard.test.ts index 5ee8ec3def69..9a0be279b121 100644 --- a/src/commands/onboard.test.ts +++ b/src/commands/onboard.test.ts @@ -921,12 +921,15 @@ describe("setupWizardCommand", () => { expect(mocks.runNonInteractiveSetup).not.toHaveBeenCalled(); }); - it("keeps --tui on guided onboarding", async () => { + it.each([ + ["--tui", { tui: true }], + ["--skip-ui", { skipUi: true }], + ])("keeps %s on guided onboarding", async (_label, opts) => { const runtime = makeRuntime(); - await setupWizardCommand({ tui: true }, runtime); + await setupWizardCommand(opts, runtime); - expect(mocks.runGuidedOnboarding).toHaveBeenCalledWith({ tui: true }, runtime); + expect(mocks.runGuidedOnboarding).toHaveBeenCalledWith(opts, runtime); expect(mocks.runInteractiveSetup).not.toHaveBeenCalled(); }); diff --git a/src/commands/onboard.ts b/src/commands/onboard.ts index 2e34e49d6ab2..24cfb332668d 100644 --- a/src/commands/onboard.ts +++ b/src/commands/onboard.ts @@ -424,6 +424,7 @@ const GUIDED_SAFE_ONBOARD_KEYS = new Set([ "nonInteractive", "classic", "tui", + "skipUi", ]); function wantsClassicInteractiveSetup(opts: OnboardOptions): boolean { diff --git a/test/scripts/onboard-config-fixtures.test.ts b/test/scripts/onboard-config-fixtures.test.ts index 6c21f414b787..d9958d5be883 100644 --- a/test/scripts/onboard-config-fixtures.test.ts +++ b/test/scripts/onboard-config-fixtures.test.ts @@ -87,6 +87,41 @@ describe("onboard config fixture helpers", () => { expect(assertResult.stderr).toBe(""); }); + it("writes configured guided skip-UI fixtures with a local mock model", () => { + const root = makeTempDir(tempDirs, "openclaw-onboard-config-guided-"); + const configPath = path.join(root, "openclaw.json"); + const workspace = path.join(root, "workspace"); + + const writeResult = runScript(WRITE_CONFIG_SCRIPT, [ + "guided-skip-ui", + configPath, + workspace, + "19091", + ]); + const config = readJson(configPath); + + expect(writeResult.status).toBe(0); + expect(config.gateway).toEqual({ + mode: "local", + bind: "loopback", + controlUi: { enabled: false }, + }); + expect(config.agents.defaults.workspace).toBe(workspace); + expect(config.agents.defaults.model.primary).toBe("openai/gpt-5.6-luna"); + expect(config.models.providers.openai.baseUrl).toBe("http://127.0.0.1:19091/v1"); + expect(config.models.providers.openai.apiKey).toEqual({ + source: "env", + provider: "default", + id: "OPENAI_API_KEY", + }); + expect(config.wizard).toMatchObject({ + securityAcknowledgedAt: "2026-01-01T00:00:00.000Z", + accessMode: "full", + appRecommendations: false, + }); + expect(readFileSync(configPath, "utf8")).toMatch(/\n$/u); + }); + it("accepts local and remote onboard assertion fixtures", () => { const root = makeTempDir(tempDirs, "openclaw-onboard-config-success-"); const workspace = path.join(root, "workspace");