refactor: split Crestodian planner backend selection

This commit is contained in:
Peter Steinberger
2026-04-25 17:56:40 +01:00
parent 60f9358348
commit e27e29c66e
7 changed files with 447 additions and 480 deletions

View File

@@ -7,6 +7,24 @@ import type { OpenClawConfig } from "../../src/config/types.openclaw.js";
import { runCrestodian } from "../../src/crestodian/crestodian.js";
import type { RuntimeEnv } from "../../src/runtime.js";
type CrestodianFirstRunCommand = {
id: string;
message: string;
expectOutput: string;
approve: boolean;
};
type CrestodianFirstRunSpec = {
dockerDefaultWorkspace: string;
dockerAgentWorkspace: string;
agentId: string;
model: string;
discordEnv: string;
discordToken: string;
commands: CrestodianFirstRunCommand[];
auditOperations: string[];
};
function assert(condition: unknown, message: string): asserts condition {
if (!condition) {
throw new Error(message);
@@ -27,7 +45,21 @@ function createRuntime(): { runtime: RuntimeEnv; lines: string[] } {
};
}
async function readFirstRunSpec(): Promise<CrestodianFirstRunSpec> {
return JSON.parse(
await fs.readFile(
path.join(process.cwd(), "scripts", "e2e", "crestodian-first-run-spec.json"),
"utf8",
),
) as CrestodianFirstRunSpec;
}
function renderCommandTemplate(template: string, vars: Record<string, string>): string {
return template.replace(/\{([A-Za-z0-9_]+)\}/g, (match, key: string) => vars[key] ?? match);
}
async function main() {
const spec = await readFirstRunSpec();
const stateDir =
process.env.OPENCLAW_STATE_DIR ??
(await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-crestodian-first-run-")));
@@ -61,137 +93,49 @@ async function main() {
"fresh overview did not include setup recommendation",
);
process.env.DISCORD_BOT_TOKEN = "openclaw-crestodian-discord-e2e-token";
process.env[spec.discordEnv] = spec.discordToken;
const setupRuntime = createRuntime();
await runCrestodian(
{
message: "setup workspace /tmp/openclaw-first-run model openai/gpt-5.2",
yes: true,
interactive: false,
},
setupRuntime.runtime,
);
const setupOutput = setupRuntime.lines.join("\n");
assert(
setupOutput.includes("[crestodian] done: crestodian.setup"),
"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 discordPluginAllowRuntime = createRuntime();
await runCrestodian(
{
message: 'config set plugins.allow ["discord"]',
yes: true,
interactive: false,
},
discordPluginAllowRuntime.runtime,
);
assert(
discordPluginAllowRuntime.lines.join("\n").includes("[crestodian] done: config.set"),
"Crestodian Discord plugin allowlist did not apply",
);
clearConfigCache();
const discordPluginEntryRuntime = createRuntime();
await runCrestodian(
{
message: "config set plugins.entries.discord.enabled true",
yes: true,
interactive: false,
},
discordPluginEntryRuntime.runtime,
);
assert(
discordPluginEntryRuntime.lines.join("\n").includes("[crestodian] done: config.set"),
"Crestodian Discord plugin entry 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);
assert(
validateRuntime.lines.join("\n").includes("Config valid:"),
"post-setup config validation did not pass",
);
const commandVars = {
defaultWorkspace: spec.dockerDefaultWorkspace,
agentWorkspace: spec.dockerAgentWorkspace,
agentId: spec.agentId,
model: spec.model,
discordEnv: spec.discordEnv,
};
for (const command of spec.commands) {
clearConfigCache();
const commandRuntime = createRuntime();
await runCrestodian(
{
message: renderCommandTemplate(command.message, commandVars),
yes: command.approve,
interactive: false,
},
commandRuntime.runtime,
);
const output = commandRuntime.lines.join("\n");
assert(
output.includes(command.expectOutput),
`Crestodian first-run command ${command.id} did not apply: ${output}`,
);
}
const config = JSON.parse(await fs.readFile(configPath, "utf8")) as OpenClawConfig;
assert(
config.agents?.defaults?.workspace === "/tmp/openclaw-first-run",
config.agents?.defaults?.workspace === spec.dockerDefaultWorkspace,
"first-run setup did not write default workspace",
);
assert(
config.agents?.defaults?.model &&
typeof config.agents.defaults.model === "object" &&
"primary" in config.agents.defaults.model &&
config.agents.defaults.model.primary === "openai/gpt-5.2",
config.agents.defaults.model.primary === spec.model,
"first-run setup did not write default model",
);
const reef = config.agents?.list?.find((agent) => agent.id === "reef");
const reef = config.agents?.list?.find((agent) => agent.id === spec.agentId);
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(reef.workspace === spec.dockerAgentWorkspace, "Crestodian did not write reef workspace");
assert(reef.model === spec.model, "Crestodian did not write reef model");
assert(config.plugins?.allow?.includes("discord"), "Crestodian did not allow Discord plugin");
assert(
config.plugins?.entries?.discord?.enabled === true,
@@ -205,24 +149,19 @@ async function main() {
"source" in discordToken &&
discordToken.source === "env" &&
"id" in discordToken &&
discordToken.id === "DISCORD_BOT_TOKEN",
discordToken.id === spec.discordEnv,
"Crestodian did not write Discord token SecretRef",
);
assert(
!JSON.stringify(config.channels.discord).includes(process.env.DISCORD_BOT_TOKEN),
!JSON.stringify(config.channels.discord).includes(spec.discordToken),
"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");
for (const operation of spec.auditOperations) {
assert(audit.includes(`"operation":"${operation}"`), `${operation} audit entry missing`);
}
console.log("Crestodian first-run Docker E2E passed");
}

View File

@@ -0,0 +1,68 @@
{
"stateDirName": "crestodian-ring-zero-state",
"defaultWorkspaceName": "crestodian-main-workspace",
"agentWorkspaceName": "crestodian-reef-workspace",
"dockerDefaultWorkspace": "/tmp/openclaw-first-run",
"dockerAgentWorkspace": "/tmp/openclaw-reef",
"agentId": "reef",
"model": "openai/gpt-5.2",
"discordEnv": "DISCORD_BOT_TOKEN",
"discordToken": "openclaw-crestodian-discord-e2e-token",
"commands": [
{
"id": "setup",
"message": "setup workspace {defaultWorkspace} model {model}",
"expectOutput": "[crestodian] done: crestodian.setup",
"approve": true
},
{
"id": "default-model",
"message": "set default model {model}",
"expectOutput": "[crestodian] done: config.setDefaultModel",
"approve": true
},
{
"id": "agent",
"message": "create agent {agentId} workspace {agentWorkspace} model {model}",
"expectOutput": "[crestodian] done: agents.create",
"approve": true
},
{
"id": "discord-plugin-allow",
"message": "config set plugins.allow [\"discord\"]",
"expectOutput": "[crestodian] done: config.set",
"approve": true
},
{
"id": "discord-plugin-entry",
"message": "config set plugins.entries.discord.enabled true",
"expectOutput": "[crestodian] done: config.set",
"approve": true
},
{
"id": "discord-token",
"message": "config set-ref channels.discord.token env {discordEnv}",
"expectOutput": "[crestodian] done: config.setRef",
"approve": true
},
{
"id": "discord-enabled",
"message": "config set channels.discord.enabled true",
"expectOutput": "[crestodian] done: config.set",
"approve": true
},
{
"id": "validate",
"message": "validate config",
"expectOutput": "Config valid:",
"approve": false
}
],
"auditOperations": [
"crestodian.setup",
"config.setDefaultModel",
"agents.create",
"config.setRef",
"config.set"
]
}