mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-16 20:31:40 +00:00
* chore(models): migrate active GPT-5.5 references * test(workboard): expect GPT-5.6 Sol default * chore: keep release notes in PR body * test(models): align picker fixtures with Sol default * test: update PDF default model expectation * test(qa): migrate thinking smoke to Luna * test(gateway): align mock catalog with Sol default * ci: retrigger exact-head PR checks * test(gateway): document default catalog invariant
276 lines
8.9 KiB
JavaScript
276 lines
8.9 KiB
JavaScript
// Assertions for npm onboard channel-agent E2E scenarios.
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { DatabaseSync } from "node:sqlite";
|
|
import {
|
|
assertAgentReplyContainsMarker,
|
|
assertOpenAiRequestLogUsed,
|
|
} from "../agent-turn-output.mjs";
|
|
import { assertOpenAiEnvAuthProfileStore } from "../auth-profile-store-assertions.mjs";
|
|
import { readPositiveIntEnv } from "../env-limits.mjs";
|
|
import {
|
|
applyMockOpenAiModelConfig,
|
|
parseMockOpenAiPort,
|
|
} from "../fixtures/mock-openai-config.mjs";
|
|
import { readTextFileBounded, readTextFileTail } from "../text-file-utils.mjs";
|
|
|
|
const command = process.argv[2];
|
|
const ERROR_DETAIL_TAIL_BYTES = 16 * 1024;
|
|
const JSON_ARTIFACT_MAX_BYTES = readPositiveIntEnv(
|
|
"OPENCLAW_NPM_ONBOARD_JSON_ARTIFACT_MAX_BYTES",
|
|
1024 * 1024,
|
|
);
|
|
const STATUS_TEXT_MAX_BYTES = readPositiveIntEnv(
|
|
"OPENCLAW_NPM_ONBOARD_STATUS_TEXT_MAX_BYTES",
|
|
1024 * 1024,
|
|
);
|
|
const ansiEscapePattern = new RegExp(String.raw`\u001b\[[0-?]*[ -/]*[@-~]`, "g");
|
|
|
|
function readJson(file) {
|
|
return JSON.parse(
|
|
readTextFileBounded(file, "JSON artifact", JSON_ARTIFACT_MAX_BYTES, {
|
|
tailBytes: ERROR_DETAIL_TAIL_BYTES,
|
|
}),
|
|
);
|
|
}
|
|
|
|
function stripAnsi(text) {
|
|
return text.replace(ansiEscapePattern, "");
|
|
}
|
|
|
|
const statusSectionTitles = new Set([
|
|
"openclaw status",
|
|
"overview",
|
|
"plugin compatibility",
|
|
"model selection",
|
|
"security audit",
|
|
"channels",
|
|
"sessions",
|
|
"system events",
|
|
"health",
|
|
"usage",
|
|
]);
|
|
|
|
function normalizedStatusHeading(line) {
|
|
return stripAnsi(line)
|
|
.trim()
|
|
.replace(/^#+\s*/, "")
|
|
.trim()
|
|
.toLowerCase();
|
|
}
|
|
|
|
function extractStatusSection(text, title) {
|
|
const target = title.toLowerCase();
|
|
const lines = text.split(/\r?\n/);
|
|
const start = lines.findIndex((line) => normalizedStatusHeading(line) === target);
|
|
if (start === -1) {
|
|
return null;
|
|
}
|
|
const section = [];
|
|
for (const line of lines.slice(start + 1)) {
|
|
const normalized = normalizedStatusHeading(line);
|
|
if (normalized && statusSectionTitles.has(normalized)) {
|
|
break;
|
|
}
|
|
section.push(line);
|
|
}
|
|
return stripAnsi(section.join("\n"));
|
|
}
|
|
|
|
function readAuthProfileStoreText(agentDir) {
|
|
const dbPath = path.join(agentDir, "openclaw-agent.sqlite");
|
|
if (!fs.existsSync(dbPath)) {
|
|
return "";
|
|
}
|
|
let db;
|
|
try {
|
|
db = new DatabaseSync(dbPath, { readOnly: true });
|
|
const row = db
|
|
.prepare("SELECT store_json FROM auth_profile_store WHERE store_key = ?")
|
|
.get("primary");
|
|
return typeof row?.store_json === "string" ? row.store_json : "";
|
|
} catch {
|
|
return "";
|
|
} finally {
|
|
db?.close();
|
|
}
|
|
}
|
|
|
|
function assertOnboardState() {
|
|
const home = process.argv[3];
|
|
const stateDir = path.join(home, ".openclaw");
|
|
const configPath = path.join(stateDir, "openclaw.json");
|
|
const agentDir = path.join(stateDir, "agents", "main", "agent");
|
|
|
|
if (!fs.existsSync(configPath)) {
|
|
throw new Error("onboard did not write openclaw.json");
|
|
}
|
|
if (!fs.existsSync(agentDir)) {
|
|
throw new Error("onboard did not create main agent dir");
|
|
}
|
|
const authStoreText = readAuthProfileStoreText(agentDir);
|
|
if (!authStoreText) {
|
|
throw new Error("onboard did not persist auth profile store");
|
|
}
|
|
assertOpenAiEnvAuthProfileStore(authStoreText, {
|
|
envRefMessage: "auth profile did not persist OPENAI_API_KEY env ref",
|
|
rawKeyMessage: "auth profile persisted the raw OpenAI test key",
|
|
rawKeyNeedle: "sk-openclaw-npm-onboard-e2e",
|
|
});
|
|
}
|
|
|
|
function configureMockModel() {
|
|
const mockPort = parseMockOpenAiPort(process.argv[3]);
|
|
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
|
|
const cfg = readJson(configPath);
|
|
applyMockOpenAiModelConfig(cfg, { mockPort });
|
|
fs.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}\n`);
|
|
}
|
|
|
|
function assertMockModelConfig() {
|
|
const mockPort = parseMockOpenAiPort(process.argv[3]);
|
|
const expectedModelRef = "openai/gpt-5.6-luna";
|
|
const expectedBaseUrl = `http://127.0.0.1:${mockPort}/v1`;
|
|
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
|
|
const cfg = readJson(configPath);
|
|
const provider = cfg.models?.providers?.openai;
|
|
const defaultModel = cfg.agents?.defaults?.model?.primary;
|
|
const defaultRuntime = cfg.agents?.defaults?.models?.[expectedModelRef]?.agentRuntime?.id;
|
|
const agent = Array.isArray(cfg.agents?.list)
|
|
? (cfg.agents.list.find((entry) => entry?.id === "main") ?? cfg.agents.list[0])
|
|
: undefined;
|
|
const agentModel = agent?.model?.primary;
|
|
const agentRuntime = agent?.models?.[expectedModelRef]?.agentRuntime?.id;
|
|
if (provider?.baseUrl !== expectedBaseUrl) {
|
|
throw new Error(
|
|
`mock OpenAI baseUrl was not preserved; expected ${expectedBaseUrl}, got ${provider?.baseUrl}`,
|
|
);
|
|
}
|
|
if (provider?.api !== "openai-responses") {
|
|
throw new Error(`mock OpenAI api was not preserved; got ${provider?.api}`);
|
|
}
|
|
if (provider?.agentRuntime?.id !== "openclaw") {
|
|
throw new Error(`mock OpenAI runtime was not preserved; got ${provider?.agentRuntime?.id}`);
|
|
}
|
|
if (defaultModel !== expectedModelRef) {
|
|
throw new Error(
|
|
`mock default model was not preserved; expected ${expectedModelRef}, got ${defaultModel}`,
|
|
);
|
|
}
|
|
if (defaultRuntime !== "openclaw") {
|
|
throw new Error(`mock default runtime was not preserved; got ${defaultRuntime}`);
|
|
}
|
|
if (agent && agentModel !== expectedModelRef) {
|
|
throw new Error(
|
|
`mock agent model was not preserved; expected ${expectedModelRef}, got ${agentModel}`,
|
|
);
|
|
}
|
|
if (agent && agentRuntime !== "openclaw") {
|
|
throw new Error(`mock agent runtime was not preserved; got ${agentRuntime}`);
|
|
}
|
|
}
|
|
|
|
function assertChannelConfig() {
|
|
const channel = process.argv[3];
|
|
const expectedTokens = process.argv.slice(4);
|
|
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
|
|
const cfg = readJson(configPath);
|
|
const entry = cfg.channels?.[channel];
|
|
if (!entry || entry.enabled === false) {
|
|
throw new Error(`${channel} was not enabled`);
|
|
}
|
|
const assertTokenField = (field, expected) => {
|
|
if (entry[field] !== expected) {
|
|
throw new Error(
|
|
`${channel} config did not persist ${field}; expected ${expected}, got ${JSON.stringify(entry[field])}`,
|
|
);
|
|
}
|
|
};
|
|
switch (channel) {
|
|
case "telegram": {
|
|
if (expectedTokens.length !== 1) {
|
|
throw new Error("telegram channel config assertion requires one bot token");
|
|
}
|
|
assertTokenField("botToken", expectedTokens[0]);
|
|
return;
|
|
}
|
|
case "discord": {
|
|
if (expectedTokens.length !== 1) {
|
|
throw new Error("discord channel config assertion requires one bot token");
|
|
}
|
|
assertTokenField("token", expectedTokens[0]);
|
|
return;
|
|
}
|
|
case "slack": {
|
|
if (expectedTokens.length !== 2) {
|
|
throw new Error("slack channel config assertion requires bot and app tokens");
|
|
}
|
|
assertTokenField("botToken", expectedTokens[0]);
|
|
assertTokenField("appToken", expectedTokens[1]);
|
|
return;
|
|
}
|
|
default:
|
|
throw new Error(`unsupported channel config assertion: ${channel}`);
|
|
}
|
|
}
|
|
|
|
function assertStatusSurfaces() {
|
|
const channel = process.argv[3];
|
|
const channelsStatusPath = process.argv[4];
|
|
const statusTextPath = process.argv[5];
|
|
const channelsStatus = readJson(channelsStatusPath);
|
|
const statusText = readTextFileBounded(
|
|
statusTextPath,
|
|
"plain status output",
|
|
STATUS_TEXT_MAX_BYTES,
|
|
{ tailBytes: ERROR_DETAIL_TAIL_BYTES },
|
|
);
|
|
const statusTail = readTextFileTail(statusTextPath, ERROR_DETAIL_TAIL_BYTES);
|
|
const configuredChannels = Array.isArray(channelsStatus.configuredChannels)
|
|
? channelsStatus.configuredChannels
|
|
: [];
|
|
if (!configuredChannels.includes(channel)) {
|
|
throw new Error(
|
|
`channels status did not list configured channel ${channel}. Payload: ${JSON.stringify(channelsStatus)}`,
|
|
);
|
|
}
|
|
if (!/channels/i.test(statusText)) {
|
|
throw new Error(
|
|
`plain status output did not render a Channels section. Output tail: ${statusTail}`,
|
|
);
|
|
}
|
|
const channelsSection = extractStatusSection(statusText, "channels");
|
|
if (!channelsSection) {
|
|
throw new Error(
|
|
`plain status output did not render a Channels section. Output tail: ${statusTail}`,
|
|
);
|
|
}
|
|
if (!channelsSection.toLowerCase().includes(channel.toLowerCase())) {
|
|
throw new Error(
|
|
`plain status output did not mention ${channel} in the Channels section. Output tail: ${statusTail}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertAgentTurn() {
|
|
const marker = process.argv[3];
|
|
const logPath = process.argv[4];
|
|
assertAgentReplyContainsMarker(marker, "/tmp/openclaw-agent.combined");
|
|
assertOpenAiRequestLogUsed(logPath);
|
|
}
|
|
|
|
const commands = {
|
|
"assert-onboard-state": assertOnboardState,
|
|
"configure-mock-model": configureMockModel,
|
|
"assert-mock-model-config": assertMockModelConfig,
|
|
"assert-channel-config": assertChannelConfig,
|
|
"assert-status-surfaces": assertStatusSurfaces,
|
|
"assert-agent-turn": assertAgentTurn,
|
|
};
|
|
|
|
const fn = commands[command];
|
|
if (!fn) {
|
|
throw new Error(`unknown npm onboard/channel/agent assertion command: ${command}`);
|
|
}
|
|
fn();
|