fix: keep skip-ui onboarding on guided flow

This commit is contained in:
Shakker
2026-08-03 16:36:46 +01:00
parent b132ceba1a
commit 4e284c4523
8 changed files with 153 additions and 13 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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 <reset|skills> <config-path>");
throw new Error("usage: write-config.mjs <reset|skills|guided-skip-ui> <config-path> [...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}`);
}

View File

@@ -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"]);

View File

@@ -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"]);

View File

@@ -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();
});

View File

@@ -424,6 +424,7 @@ const GUIDED_SAFE_ONBOARD_KEYS = new Set([
"nonInteractive",
"classic",
"tui",
"skipUi",
]);
function wantsClassicInteractiveSetup(opts: OnboardOptions): boolean {

View File

@@ -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");