mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-24 10:58:37 +00:00
325 lines
10 KiB
TypeScript
325 lines
10 KiB
TypeScript
// Npm Onboard Channel Agent Assertions tests cover npm onboard channel agent assertions script behavior.
|
|
import { spawnSync } from "node:child_process";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { DatabaseSync } from "node:sqlite";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
const assertionsPath = path.resolve("scripts/e2e/lib/npm-onboard-channel-agent/assertions.mjs");
|
|
const disableExperimentalWarning = "--disable-warning=ExperimentalWarning";
|
|
|
|
function nodeOptionsWithoutExperimentalWarnings(): string {
|
|
const current = process.env.NODE_OPTIONS ?? "";
|
|
return current.includes(disableExperimentalWarning)
|
|
? current
|
|
: [current, disableExperimentalWarning].filter(Boolean).join(" ");
|
|
}
|
|
|
|
function writeConfig(home: string, channels: Record<string, unknown>): void {
|
|
const configDir = path.join(home, ".openclaw");
|
|
fs.mkdirSync(configDir, { recursive: true });
|
|
fs.writeFileSync(path.join(configDir, "openclaw.json"), JSON.stringify({ channels }));
|
|
}
|
|
|
|
function writeOnboardConfig(home: string): void {
|
|
const configDir = path.join(home, ".openclaw");
|
|
fs.mkdirSync(configDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(configDir, "openclaw.json"),
|
|
JSON.stringify({
|
|
auth: {
|
|
profiles: {
|
|
"openai:api-key": { provider: "openai", mode: "api_key" },
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
|
|
function writeAuthProfileStoreSqlite(agentDir: string, store: unknown): void {
|
|
fs.mkdirSync(agentDir, { recursive: true });
|
|
const db = new DatabaseSync(path.join(agentDir, "openclaw-agent.sqlite"));
|
|
try {
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS auth_profile_store (
|
|
store_key TEXT NOT NULL PRIMARY KEY,
|
|
store_json TEXT NOT NULL,
|
|
updated_at INTEGER NOT NULL
|
|
);
|
|
`);
|
|
db.prepare(
|
|
`
|
|
INSERT INTO auth_profile_store (store_key, store_json, updated_at)
|
|
VALUES (?, ?, ?)
|
|
`,
|
|
).run("primary", JSON.stringify(store), Date.now());
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
|
|
function runAssert(home: string, channel: string, ...tokens: string[]) {
|
|
return spawnSync(
|
|
process.execPath,
|
|
[assertionsPath, "assert-channel-config", channel, ...tokens],
|
|
{
|
|
encoding: "utf8",
|
|
env: {
|
|
...process.env,
|
|
HOME: home,
|
|
NODE_OPTIONS: nodeOptionsWithoutExperimentalWarnings(),
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
function runOnboardAssert(home: string) {
|
|
return spawnSync(process.execPath, [assertionsPath, "assert-onboard-state", home], {
|
|
encoding: "utf8",
|
|
env: {
|
|
...process.env,
|
|
NODE_OPTIONS: nodeOptionsWithoutExperimentalWarnings(),
|
|
},
|
|
});
|
|
}
|
|
|
|
function runMockModelAssert(home: string, command: string, port: string) {
|
|
return spawnSync(process.execPath, [assertionsPath, command, port], {
|
|
encoding: "utf8",
|
|
env: {
|
|
...process.env,
|
|
HOME: home,
|
|
NODE_OPTIONS: nodeOptionsWithoutExperimentalWarnings(),
|
|
},
|
|
});
|
|
}
|
|
|
|
function runStatusAssert(
|
|
channel: string,
|
|
channelsStatus: unknown,
|
|
statusText: string,
|
|
env: NodeJS.ProcessEnv = {},
|
|
) {
|
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-status-assertions-"));
|
|
try {
|
|
const channelsStatusPath = path.join(tempDir, "channels-status.json");
|
|
const statusTextPath = path.join(tempDir, "status.txt");
|
|
fs.writeFileSync(channelsStatusPath, JSON.stringify(channelsStatus));
|
|
fs.writeFileSync(statusTextPath, statusText);
|
|
return spawnSync(
|
|
process.execPath,
|
|
[assertionsPath, "assert-status-surfaces", channel, channelsStatusPath, statusTextPath],
|
|
{
|
|
encoding: "utf8",
|
|
env: { ...process.env, ...env },
|
|
},
|
|
);
|
|
} finally {
|
|
fs.rmSync(tempDir, { force: true, recursive: true });
|
|
}
|
|
}
|
|
|
|
describe("npm onboard channel agent assertions", () => {
|
|
it("rejects loose mock OpenAI port args", () => {
|
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-onboard-assertions-"));
|
|
|
|
try {
|
|
for (const command of ["configure-mock-model", "assert-mock-model-config"]) {
|
|
const result = runMockModelAssert(tempDir, command, "1e3");
|
|
|
|
expect(result.status).not.toBe(0);
|
|
expect(result.stderr).toContain("mock OpenAI port must be a TCP port from 1 to 65535");
|
|
expect(result.stderr).toContain('"1e3"');
|
|
}
|
|
} finally {
|
|
fs.rmSync(tempDir, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it("validates OpenAI env refs from the SQLite auth profile store", () => {
|
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-onboard-assertions-"));
|
|
const agentDir = path.join(tempDir, ".openclaw", "agents", "main", "agent");
|
|
|
|
try {
|
|
writeOnboardConfig(tempDir);
|
|
writeAuthProfileStoreSqlite(agentDir, {
|
|
version: 1,
|
|
profiles: {
|
|
"openai:api-key": {
|
|
type: "api_key",
|
|
provider: "openai",
|
|
keyRef: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = runOnboardAssert(tempDir);
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(result.stderr).toBe("");
|
|
expect(fs.existsSync(path.join(agentDir, "auth-profiles.json"))).toBe(false);
|
|
} finally {
|
|
fs.rmSync(tempDir, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it("rejects auth profile stores without a usable OpenAI env ref", () => {
|
|
const cases: unknown[] = [
|
|
"OPENAI_API_KEY",
|
|
{
|
|
version: 1,
|
|
profiles: {
|
|
"openai:api-key": { note: "OPENAI_API_KEY" },
|
|
},
|
|
},
|
|
];
|
|
|
|
for (const store of cases) {
|
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-onboard-assertions-"));
|
|
const agentDir = path.join(tempDir, ".openclaw", "agents", "main", "agent");
|
|
|
|
try {
|
|
writeOnboardConfig(tempDir);
|
|
writeAuthProfileStoreSqlite(agentDir, store);
|
|
|
|
const result = runOnboardAssert(tempDir);
|
|
|
|
expect(result.status).not.toBe(0);
|
|
expect(result.stderr).toContain("auth profile did not persist OPENAI_API_KEY env ref");
|
|
} finally {
|
|
fs.rmSync(tempDir, { force: true, recursive: true });
|
|
}
|
|
}
|
|
});
|
|
|
|
it("rejects inline OpenAI keys in the SQLite auth profile store", () => {
|
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-onboard-assertions-"));
|
|
const agentDir = path.join(tempDir, ".openclaw", "agents", "main", "agent");
|
|
|
|
try {
|
|
writeOnboardConfig(tempDir);
|
|
writeAuthProfileStoreSqlite(agentDir, {
|
|
version: 1,
|
|
profiles: {
|
|
"openai:api-key": {
|
|
type: "api_key",
|
|
provider: "openai",
|
|
key: "sk-openclaw-npm-onboard-e2e",
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = runOnboardAssert(tempDir);
|
|
|
|
expect(result.status).not.toBe(0);
|
|
expect(result.stderr).toContain("auth profile persisted the raw OpenAI test key");
|
|
} finally {
|
|
fs.rmSync(tempDir, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it("validates channel tokens in their canonical config fields", () => {
|
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-channel-assertions-"));
|
|
try {
|
|
writeConfig(tempDir, {
|
|
discord: { enabled: true, token: "discord-token" },
|
|
slack: { enabled: true, appToken: "xapp-token", botToken: "xoxb-token" },
|
|
telegram: { enabled: true, botToken: "telegram-token" },
|
|
});
|
|
|
|
expect(runAssert(tempDir, "telegram", "telegram-token").status).toBe(0);
|
|
expect(runAssert(tempDir, "discord", "discord-token").status).toBe(0);
|
|
expect(runAssert(tempDir, "slack", "xoxb-token", "xapp-token").status).toBe(0);
|
|
} finally {
|
|
fs.rmSync(tempDir, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it("rejects tokens persisted on the wrong channel config field", () => {
|
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-channel-assertions-"));
|
|
try {
|
|
writeConfig(tempDir, {
|
|
telegram: { enabled: true, token: "telegram-token" },
|
|
});
|
|
|
|
const result = runAssert(tempDir, "telegram", "telegram-token");
|
|
|
|
expect(result.status).not.toBe(0);
|
|
expect(result.stderr).toContain("telegram config did not persist botToken");
|
|
} finally {
|
|
fs.rmSync(tempDir, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it("validates configured channels in the plain status Channels section", () => {
|
|
const result = runStatusAssert(
|
|
"telegram",
|
|
{ configuredChannels: ["telegram"] },
|
|
[
|
|
"# OpenClaw status",
|
|
"",
|
|
"# Overview",
|
|
"OS macOS",
|
|
"",
|
|
"# Channels",
|
|
"Channel State Detail",
|
|
"telegram ok configured",
|
|
"",
|
|
"# Sessions",
|
|
"No sessions",
|
|
].join("\n"),
|
|
);
|
|
|
|
expect(result.status).toBe(0);
|
|
});
|
|
|
|
it("rejects plain status output that mentions the channel outside the Channels section", () => {
|
|
const result = runStatusAssert(
|
|
"telegram",
|
|
{ configuredChannels: ["telegram"] },
|
|
[
|
|
"# OpenClaw status",
|
|
"",
|
|
"# Overview",
|
|
"OS macOS",
|
|
"",
|
|
"# Channels",
|
|
"No channels configured",
|
|
"",
|
|
"# Sessions",
|
|
"telegram appeared in an unrelated session note",
|
|
].join("\n"),
|
|
);
|
|
|
|
expect(result.status).not.toBe(0);
|
|
expect(result.stderr).toContain(
|
|
"plain status output did not mention telegram in the Channels section",
|
|
);
|
|
});
|
|
|
|
it("rejects oversized plain status output before parsing it", () => {
|
|
const result = runStatusAssert(
|
|
"telegram",
|
|
{ configuredChannels: ["telegram"] },
|
|
`# OpenClaw status\n${"x".repeat(128)}`,
|
|
{ OPENCLAW_NPM_ONBOARD_STATUS_TEXT_MAX_BYTES: "64" },
|
|
);
|
|
|
|
expect(result.status).not.toBe(0);
|
|
expect(result.stderr).toContain("plain status output exceeded 64 bytes");
|
|
});
|
|
|
|
it("rejects oversized channels status JSON before parsing it", () => {
|
|
const result = runStatusAssert(
|
|
"telegram",
|
|
{ configuredChannels: ["telegram"], filler: "x".repeat(128) },
|
|
"# Channels\ntelegram ok configured",
|
|
{ OPENCLAW_NPM_ONBOARD_JSON_ARTIFACT_MAX_BYTES: "64" },
|
|
);
|
|
|
|
expect(result.status).not.toBe(0);
|
|
expect(result.stderr).toContain("JSON artifact exceeded 64 bytes");
|
|
});
|
|
});
|