






















@@ -0,0 +1,245 @@
1+import fs from "node:fs";
2+import path from "node:path";
3+4+const command = process.argv[2];
5+6+function readJson(file) {
7+return JSON.parse(fs.readFileSync(file, "utf8"));
8+}
9+10+function configPath() {
11+return (
12+process.env.OPENCLAW_CONFIG_PATH ??
13+path.join(process.env.HOME ?? "", ".openclaw", "openclaw.json")
14+);
15+}
16+17+function assert(condition, message) {
18+if (!condition) {
19+throw new Error(message);
20+}
21+}
22+23+function writeConfig(cfg) {
24+fs.writeFileSync(configPath(), `${JSON.stringify(cfg, null, 2)}\n`);
25+}
26+27+function assertOnboard() {
28+const home = process.argv[3];
29+const stateDir = path.join(home, ".openclaw");
30+const authPath = path.join(stateDir, "agents", "main", "agent", "auth-profiles.json");
31+assert(fs.existsSync(configPath()), "onboard did not write openclaw.json");
32+const stateRaw =
33+fs.readFileSync(configPath(), "utf8") +
34+(fs.existsSync(authPath) ? fs.readFileSync(authPath, "utf8") : "");
35+assert(
36+!stateRaw.includes("sk-openclaw-release-user-journey"),
37+"onboard persisted raw OpenAI key",
38+);
39+}
40+41+function configureMockModel() {
42+const mockPort = Number(process.argv[3]);
43+const cfg = readJson(configPath());
44+const modelRef = "openai/gpt-5.5";
45+const cost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
46+cfg.models = {
47+ ...cfg.models,
48+mode: "merge",
49+providers: {
50+ ...cfg.models?.providers,
51+openai: {
52+ ...cfg.models?.providers?.openai,
53+baseUrl: `http://127.0.0.1:${mockPort}/v1`,
54+apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
55+api: "openai-responses",
56+request: { ...cfg.models?.providers?.openai?.request, allowPrivateNetwork: true },
57+models: [
58+{
59+id: "gpt-5.5",
60+name: "gpt-5.5",
61+api: "openai-responses",
62+reasoning: false,
63+input: ["text", "image"],
64+ cost,
65+contextWindow: 128000,
66+contextTokens: 96000,
67+maxTokens: 4096,
68+},
69+],
70+},
71+},
72+};
73+cfg.agents = {
74+ ...cfg.agents,
75+defaults: {
76+ ...cfg.agents?.defaults,
77+model: { primary: modelRef },
78+models: {
79+ ...cfg.agents?.defaults?.models,
80+[modelRef]: { params: { transport: "sse", openaiWsWarmup: false } },
81+},
82+},
83+};
84+cfg.plugins = {
85+ ...cfg.plugins,
86+enabled: true,
87+};
88+writeConfig(cfg);
89+}
90+91+function assertAgentTurn() {
92+const marker = process.argv[3];
93+const outputPath = process.argv[4];
94+const requestLogPath = process.argv[5];
95+const output = fs.readFileSync(outputPath, "utf8");
96+assert(output.includes(marker), `agent output did not contain marker. Output: ${output}`);
97+const requestLog = fs.existsSync(requestLogPath) ? fs.readFileSync(requestLogPath, "utf8") : "";
98+assert(
99+/\/v1\/(responses|chat\/completions)/u.test(requestLog),
100+"mock OpenAI server was not used",
101+);
102+}
103+104+function assertFileContains() {
105+const file = process.argv[3];
106+const needle = process.argv[4];
107+const raw = fs.readFileSync(file, "utf8");
108+assert(raw.includes(needle), `${file} did not contain ${needle}. Output: ${raw}`);
109+}
110+111+function assertPluginUninstalled() {
112+const pluginId = process.argv[3];
113+const cfg = readJson(configPath());
114+const recordsPath = path.join(process.env.HOME ?? "", ".openclaw", "plugins", "installs.json");
115+const records = fs.existsSync(recordsPath) ? readJson(recordsPath) : {};
116+const installRecords = records.installRecords ?? records.records ?? {};
117+assert(!installRecords[pluginId], `install record still present for ${pluginId}`);
118+assert(!cfg.plugins?.entries?.[pluginId], `plugin config entry still present for ${pluginId}`);
119+assert(!(cfg.plugins?.allow ?? []).includes(pluginId), `allowlist still contains ${pluginId}`);
120+assert(!(cfg.plugins?.deny ?? []).includes(pluginId), `denylist still contains ${pluginId}`);
121+}
122+123+function configureClickClack() {
124+const baseUrl = process.argv[3];
125+const cfg = readJson(configPath());
126+cfg.plugins = {
127+ ...cfg.plugins,
128+enabled: true,
129+entries: {
130+ ...cfg.plugins?.entries,
131+clickclack: {
132+ ...cfg.plugins?.entries?.clickclack,
133+enabled: true,
134+llm: {
135+ ...cfg.plugins?.entries?.clickclack?.llm,
136+allowAgentIdOverride: true,
137+allowModelOverride: true,
138+allowedModels: ["openai/gpt-5.5"],
139+},
140+},
141+},
142+};
143+cfg.channels = {
144+ ...cfg.channels,
145+clickclack: {
146+ ...cfg.channels?.clickclack,
147+enabled: true,
148+ baseUrl,
149+token: { source: "env", provider: "default", id: "CLICKCLACK_BOT_TOKEN" },
150+workspace: "release",
151+defaultTo: "channel:general",
152+replyMode: "model",
153+model: "openai/gpt-5.5",
154+reconnectMs: 250,
155+},
156+};
157+writeConfig(cfg);
158+}
159+160+function assertChannelStatus() {
161+const channel = process.argv[3];
162+const statusPath = process.argv[4];
163+const status = readJson(statusPath);
164+const configured = Array.isArray(status.configuredChannels) ? status.configuredChannels : [];
165+const liveStatus = status.channels?.[channel];
166+assert(
167+configured.includes(channel) || liveStatus?.ok === true,
168+`${channel} missing from channels status: ${JSON.stringify(status)}`,
169+);
170+}
171+172+async function postClickClackInbound() {
173+const baseUrl = process.argv[3];
174+const body = process.argv[4];
175+const response = await fetch(`${baseUrl}/fixture/inbound`, {
176+method: "POST",
177+headers: { "content-type": "application/json" },
178+body: JSON.stringify({ body }),
179+});
180+assert(response.ok, `fixture inbound failed: ${response.status} ${await response.text()}`);
181+}
182+183+async function waitClickClackSocket() {
184+const baseUrl = process.argv[3];
185+const timeoutSeconds = Number(process.argv[4] ?? 30);
186+const deadline = Date.now() + timeoutSeconds * 1000;
187+while (Date.now() < deadline) {
188+const response = await fetch(`${baseUrl}/fixture/state`).catch(() => undefined);
189+if (response?.ok) {
190+const state = await response.json();
191+if (Number(state.socketCount ?? 0) > 0) {
192+return;
193+}
194+}
195+await new Promise((resolve) => setTimeout(resolve, 250));
196+}
197+throw new Error(`Timed out waiting for ClickClack websocket connection at ${baseUrl}`);
198+}
199+200+function assertClickClackState() {
201+const mode = process.argv[3];
202+const statePath = process.argv[4];
203+const needle = process.argv[5];
204+const state = readJson(statePath);
205+const haystack = JSON.stringify(mode === "outbound" ? state.outboundMessages : state);
206+assert(haystack.includes(needle), `ClickClack state did not contain ${needle}: ${haystack}`);
207+}
208+209+async function waitClickClackReply() {
210+const statePath = process.argv[3];
211+const marker = process.argv[4];
212+const timeoutSeconds = Number(process.argv[5] ?? 30);
213+const deadline = Date.now() + timeoutSeconds * 1000;
214+while (Date.now() < deadline) {
215+if (fs.existsSync(statePath)) {
216+const state = readJson(statePath);
217+if (JSON.stringify(state.threadReplies ?? []).includes(marker)) {
218+return;
219+}
220+}
221+await new Promise((resolve) => setTimeout(resolve, 250));
222+}
223+const state = fs.existsSync(statePath) ? fs.readFileSync(statePath, "utf8") : "<missing>";
224+throw new Error(`Timed out waiting for ClickClack reply marker ${marker}. State: ${state}`);
225+}
226+227+const commands = {
228+"assert-onboard": assertOnboard,
229+"configure-mock-model": configureMockModel,
230+"assert-agent-turn": assertAgentTurn,
231+"assert-file-contains": assertFileContains,
232+"assert-plugin-uninstalled": assertPluginUninstalled,
233+"configure-clickclack": configureClickClack,
234+"assert-channel-status": assertChannelStatus,
235+"post-clickclack-inbound": postClickClackInbound,
236+"wait-clickclack-socket": waitClickClackSocket,
237+"assert-clickclack-state": assertClickClackState,
238+"wait-clickclack-reply": waitClickClackReply,
239+};
240+241+const fn = commands[command];
242+if (!fn) {
243+throw new Error(`unknown release-user-journey assertion command: ${command ?? "<missing>"}`);
244+}
245+await fn();
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。