diff --git a/docs/cli/crestodian.md b/docs/cli/crestodian.md index b99ac7adce0..d419f57ce17 100644 --- a/docs/cli/crestodian.md +++ b/docs/cli/crestodian.md @@ -304,6 +304,10 @@ Fresh configless setup through Crestodian is covered by: pnpm test:docker:crestodian-first-run ``` +That lane starts with an empty state dir, routes bare `openclaw` to Crestodian, +sets the default model, creates an additional agent, configures Discord through +a token SecretRef, validates config, and checks the audit log. + ## Related - [CLI reference](/cli) diff --git a/docs/help/testing.md b/docs/help/testing.md index 6e0efac7802..28fb45fb95c 100644 --- a/docs/help/testing.md +++ b/docs/help/testing.md @@ -63,6 +63,10 @@ When debugging real providers/models (requires real creds): - Runs Crestodian in a configless container with a fake Claude CLI on `PATH` and verifies the fuzzy planner fallback translates into an audited typed config write. +- Crestodian first-run Docker smoke: `pnpm test:docker:crestodian-first-run` + - Starts from an empty OpenClaw state dir, routes bare `openclaw` to + Crestodian, applies setup/model/agent/Discord SecretRef writes, validates + config, and verifies audit entries. - Moonshot/Kimi cost smoke: with `MOONSHOT_API_KEY` set, run `openclaw models list --provider moonshot --json`, then run an isolated `openclaw agent --local --session-id live-kimi-cost --message 'Reply exactly: KIMI_LIVE_OK' --thinking off --json` diff --git a/scripts/e2e/crestodian-first-run-docker-client.ts b/scripts/e2e/crestodian-first-run-docker-client.ts index 07bba98df3d..84f1f0ed216 100644 --- a/scripts/e2e/crestodian-first-run-docker-client.ts +++ b/scripts/e2e/crestodian-first-run-docker-client.ts @@ -57,10 +57,12 @@ async function main() { "fresh overview did not report missing config", ); assert( - overviewOutput.includes('Next: say "setup" to create a starter config'), + overviewOutput.includes('Next: run "setup" to create a starter config'), "fresh overview did not include setup recommendation", ); + process.env.DISCORD_BOT_TOKEN = "openclaw-crestodian-discord-e2e-token"; + const setupRuntime = createRuntime(); await runCrestodian( { @@ -76,6 +78,66 @@ async function main() { "Crestodian setup did not apply", ); + clearConfigCache(); + const modelRuntime = createRuntime(); + await runCrestodian( + { + message: "set default model openai/gpt-5.2", + yes: true, + interactive: false, + }, + modelRuntime.runtime, + ); + assert( + modelRuntime.lines.join("\n").includes("[crestodian] done: config.setDefaultModel"), + "Crestodian default model update did not apply", + ); + + clearConfigCache(); + const agentRuntime = createRuntime(); + await runCrestodian( + { + message: "create agent reef workspace /tmp/openclaw-reef model openai/gpt-5.2", + yes: true, + interactive: false, + }, + agentRuntime.runtime, + ); + assert( + agentRuntime.lines.join("\n").includes("[crestodian] done: agents.create"), + "Crestodian agent creation did not apply", + ); + + clearConfigCache(); + const discordTokenRuntime = createRuntime(); + await runCrestodian( + { + message: "config set-ref channels.discord.token env DISCORD_BOT_TOKEN", + yes: true, + interactive: false, + }, + discordTokenRuntime.runtime, + ); + assert( + discordTokenRuntime.lines.join("\n").includes("[crestodian] done: config.setRef"), + "Crestodian Discord token SecretRef did not apply", + ); + + clearConfigCache(); + const discordEnabledRuntime = createRuntime(); + await runCrestodian( + { + message: "config set channels.discord.enabled true", + yes: true, + interactive: false, + }, + discordEnabledRuntime.runtime, + ); + assert( + discordEnabledRuntime.lines.join("\n").includes("[crestodian] done: config.set"), + "Crestodian Discord enabled flag did not apply", + ); + clearConfigCache(); const validateRuntime = createRuntime(); await runCrestodian({ message: "validate config", interactive: false }, validateRuntime.runtime); @@ -96,10 +158,36 @@ async function main() { config.agents.defaults.model.primary === "openai/gpt-5.2", "first-run setup did not write default model", ); + const reef = config.agents?.list?.find((agent) => agent.id === "reef"); + assert(reef, "Crestodian did not create reef agent"); + assert(reef.workspace === "/tmp/openclaw-reef", "Crestodian did not write reef workspace"); + assert(reef.model === "openai/gpt-5.2", "Crestodian did not write reef model"); + assert(config.channels?.discord?.enabled === true, "Crestodian did not enable Discord"); + const discordToken = config.channels?.discord?.token; + assert( + discordToken && + typeof discordToken === "object" && + "source" in discordToken && + discordToken.source === "env" && + "id" in discordToken && + discordToken.id === "DISCORD_BOT_TOKEN", + "Crestodian did not write Discord token SecretRef", + ); + assert( + !JSON.stringify(config.channels.discord).includes(process.env.DISCORD_BOT_TOKEN), + "Crestodian persisted the raw Discord token", + ); const auditPath = path.join(stateDir, "audit", "crestodian.jsonl"); const audit = (await fs.readFile(auditPath, "utf8")).trim(); assert(audit.includes('"operation":"crestodian.setup"'), "setup audit entry missing"); + assert( + audit.includes('"operation":"config.setDefaultModel"'), + "default model audit entry missing", + ); + assert(audit.includes('"operation":"agents.create"'), "agent creation audit entry missing"); + assert(audit.includes('"operation":"config.setRef"'), "Discord SecretRef audit entry missing"); + assert(audit.includes('"operation":"config.set"'), "Discord enabled audit entry missing"); console.log("Crestodian first-run Docker E2E passed"); }