clawspec 1.0.19 → 1.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -2
- package/src/watchers/manager.ts +69 -1
- package/test/acp-client.test.ts +0 -309
- package/test/acpx-dependency.test.ts +0 -133
- package/test/assistant-journal.test.ts +0 -203
- package/test/command-surface.test.ts +0 -24
- package/test/config.test.ts +0 -77
- package/test/detach-attach.test.ts +0 -98
- package/test/doctor.test.ts +0 -142
- package/test/file-lock.test.ts +0 -88
- package/test/fs-utils.test.ts +0 -22
- package/test/helpers/harness.ts +0 -305
- package/test/helpers.test.ts +0 -108
- package/test/keywords.test.ts +0 -92
- package/test/notifier.test.ts +0 -29
- package/test/openspec-dependency.test.ts +0 -68
- package/test/paths-utils.test.ts +0 -30
- package/test/pause-cancel.test.ts +0 -55
- package/test/planning-journal.test.ts +0 -155
- package/test/plugin-registration.test.ts +0 -35
- package/test/project-memory.test.ts +0 -42
- package/test/proposal.test.ts +0 -24
- package/test/queue-planning.test.ts +0 -322
- package/test/queue-work.test.ts +0 -220
- package/test/recovery.test.ts +0 -603
- package/test/service-archive.test.ts +0 -87
- package/test/shell-command.test.ts +0 -48
- package/test/state-store.test.ts +0 -74
- package/test/tasks-and-checkpoint.test.ts +0 -60
- package/test/use-project.test.ts +0 -67
- package/test/watcher-planning.test.ts +0 -533
- package/test/watcher-work.test.ts +0 -1771
- package/test/worker-command.test.ts +0 -66
- package/test/worker-io-helper.test.ts +0 -97
- package/test/worker-skills.test.ts +0 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawspec",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.20",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw plugin that orchestrates OpenSpec project workflows with visible main-agent execution.",
|
|
6
6
|
"keywords": [
|
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
"index.ts",
|
|
24
24
|
"src",
|
|
25
25
|
"skills",
|
|
26
|
-
"test",
|
|
27
26
|
"README.md",
|
|
28
27
|
"README.zh-CN.md",
|
|
29
28
|
"openclaw.plugin.json",
|
package/src/watchers/manager.ts
CHANGED
|
@@ -2795,7 +2795,7 @@ function parseWorkerProgressEvent(line: string): WorkerProgressEvent | undefined
|
|
|
2795
2795
|
|
|
2796
2796
|
function formatWorkerProgressMessage(project: ProjectState, event: WorkerProgressEvent): string | undefined {
|
|
2797
2797
|
const rawMessage = typeof event.message === "string" ? event.message : "";
|
|
2798
|
-
const message = shortenActivityText(rawMessage, 120);
|
|
2798
|
+
const message = shortenActivityText(compactWorkerProgressDisplayPaths(project, rawMessage), 120);
|
|
2799
2799
|
if (!message) {
|
|
2800
2800
|
return undefined;
|
|
2801
2801
|
}
|
|
@@ -2916,6 +2916,74 @@ function compactProjectLabel(project: ProjectState): string {
|
|
|
2916
2916
|
return `${projectName}-${changeName}`;
|
|
2917
2917
|
}
|
|
2918
2918
|
|
|
2919
|
+
function compactWorkerProgressDisplayPaths(project: ProjectState, text: string): string {
|
|
2920
|
+
try {
|
|
2921
|
+
const compactRoot = compactWorkerProgressRoot(project);
|
|
2922
|
+
if (!compactRoot) {
|
|
2923
|
+
return text;
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
let compacted = text;
|
|
2927
|
+
if (project.changeDir) {
|
|
2928
|
+
compacted = replaceDisplayPathPrefix(compacted, project.changeDir, `${compactRoot}:`);
|
|
2929
|
+
}
|
|
2930
|
+
if (project.repoPath) {
|
|
2931
|
+
compacted = replaceDisplayPathPrefix(compacted, project.repoPath, `${compactRoot}:`);
|
|
2932
|
+
}
|
|
2933
|
+
compacted = normalizeCompactedDisplayPaths(compacted, compactRoot);
|
|
2934
|
+
return compacted.length < text.length ? compacted : text;
|
|
2935
|
+
} catch {
|
|
2936
|
+
return text;
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
function compactWorkerProgressRoot(project: ProjectState): string | undefined {
|
|
2941
|
+
const changeName = project.changeName?.trim();
|
|
2942
|
+
if (!changeName) {
|
|
2943
|
+
return undefined;
|
|
2944
|
+
}
|
|
2945
|
+
const projectName = project.projectName?.trim()
|
|
2946
|
+
|| (project.repoPath ? path.basename(project.repoPath) : undefined)
|
|
2947
|
+
|| "project";
|
|
2948
|
+
return `${projectName}@${changeName}`;
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
function replaceDisplayPathPrefix(text: string, targetPath: string, replacement: string): string {
|
|
2952
|
+
const pattern = buildDisplayPathPrefixPattern(targetPath);
|
|
2953
|
+
if (!pattern) {
|
|
2954
|
+
return text;
|
|
2955
|
+
}
|
|
2956
|
+
return text.replace(pattern, replacement);
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
function normalizeCompactedDisplayPaths(text: string, compactRoot: string): string {
|
|
2960
|
+
const prefix = escapeRegExp(`${compactRoot}:`);
|
|
2961
|
+
const pattern = new RegExp("(" + prefix + ")([^\\s\"'`,)\\]}]+)", "g");
|
|
2962
|
+
return text.replace(pattern, (_match, prefix: string, suffix: string) => `${prefix}${suffix.replace(/\\/g, "/")}`);
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
function buildDisplayPathPrefixPattern(targetPath: string): RegExp | undefined {
|
|
2966
|
+
const normalized = normalizeSlashes(targetPath).replace(/\/+$/, "");
|
|
2967
|
+
if (!normalized) {
|
|
2968
|
+
return undefined;
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
const escaped = normalized
|
|
2972
|
+
.split("/")
|
|
2973
|
+
.map((segment) => escapeRegExp(segment))
|
|
2974
|
+
.join("[/\\\\]+");
|
|
2975
|
+
if (!escaped) {
|
|
2976
|
+
return undefined;
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
const flags = /^[A-Za-z]:/.test(normalized) ? "gi" : "g";
|
|
2980
|
+
return new RegExp(`${escaped}(?:[/\\\\]+)?`, flags);
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
function escapeRegExp(value: string): string {
|
|
2984
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2919
2987
|
function compactProgressMarker(current?: number, total?: number): string {
|
|
2920
2988
|
if (!total || total <= 0 || !current || current <= 0) {
|
|
2921
2989
|
return "";
|
package/test/acp-client.test.ts
DELETED
|
@@ -1,309 +0,0 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { spawn } from "node:child_process";
|
|
6
|
-
import { chmod, mkdtemp, writeFile } from "node:fs/promises";
|
|
7
|
-
import { AcpWorkerClient } from "../src/acp/client.ts";
|
|
8
|
-
import { terminateChildProcess } from "../src/utils/shell-command.ts";
|
|
9
|
-
|
|
10
|
-
test("AcpWorkerClient tracks active worker lifecycle through acpx CLI", async () => {
|
|
11
|
-
const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-acpx-client-"));
|
|
12
|
-
const fake = await createFakeAcpx(tempRoot);
|
|
13
|
-
const client = new AcpWorkerClient({
|
|
14
|
-
agentId: "codex",
|
|
15
|
-
logger: createLogger(),
|
|
16
|
-
command: fake.command,
|
|
17
|
-
env: fake.env,
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
const events: Array<{ type: string; text?: string }> = [];
|
|
21
|
-
const runPromise = client.runTurn({
|
|
22
|
-
sessionKey: "session-1",
|
|
23
|
-
cwd: tempRoot,
|
|
24
|
-
text: "fix tests",
|
|
25
|
-
onEvent: async (event) => {
|
|
26
|
-
events.push(event);
|
|
27
|
-
},
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
await waitFor(async () => {
|
|
31
|
-
const status = await client.getSessionStatus({
|
|
32
|
-
sessionKey: "session-1",
|
|
33
|
-
cwd: tempRoot,
|
|
34
|
-
agentId: "codex",
|
|
35
|
-
});
|
|
36
|
-
return status?.details?.status === "alive";
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
await runPromise;
|
|
40
|
-
|
|
41
|
-
const finalStatus = await client.getSessionStatus({
|
|
42
|
-
sessionKey: "session-1",
|
|
43
|
-
cwd: tempRoot,
|
|
44
|
-
agentId: "codex",
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
assert.equal(events.some((event) => event.type === "text_delta" && event.text?.includes("Working on fix tests")), true);
|
|
48
|
-
assert.match(finalStatus?.summary ?? "", /status=dead/);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("AcpWorkerClient stops the worker when the gateway process disappears", async () => {
|
|
52
|
-
const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-acpx-watchdog-"));
|
|
53
|
-
const fake = await createFakeAcpx(tempRoot);
|
|
54
|
-
const gateway = spawn(process.execPath, ["-e", "setInterval(() => {}, 1000)"], {
|
|
55
|
-
stdio: "ignore",
|
|
56
|
-
windowsHide: true,
|
|
57
|
-
detached: true,
|
|
58
|
-
});
|
|
59
|
-
gateway.unref();
|
|
60
|
-
const client = new AcpWorkerClient({
|
|
61
|
-
agentId: "codex",
|
|
62
|
-
logger: createLogger(),
|
|
63
|
-
command: fake.command,
|
|
64
|
-
env: fake.env,
|
|
65
|
-
gatewayPid: gateway.pid,
|
|
66
|
-
gatewayWatchdogPollMs: 50,
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
const startedAt = Date.now();
|
|
70
|
-
let sawStarted = false;
|
|
71
|
-
const runPromise = client.runTurn({
|
|
72
|
-
sessionKey: "session-watchdog",
|
|
73
|
-
cwd: tempRoot,
|
|
74
|
-
text: "stay alive",
|
|
75
|
-
onEvent: async (event) => {
|
|
76
|
-
if (event.type === "text_delta" && event.text?.includes("Working on stay alive")) {
|
|
77
|
-
sawStarted = true;
|
|
78
|
-
}
|
|
79
|
-
},
|
|
80
|
-
});
|
|
81
|
-
const observedRunPromise = runPromise.catch((error) => {
|
|
82
|
-
throw error;
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
try {
|
|
86
|
-
await waitFor(async () => sawStarted === true);
|
|
87
|
-
|
|
88
|
-
terminateChildProcess(gateway, { force: true });
|
|
89
|
-
|
|
90
|
-
await assert.rejects(
|
|
91
|
-
observedRunPromise,
|
|
92
|
-
/acpx exited with code|signal=SIGTERM|terminated/i,
|
|
93
|
-
);
|
|
94
|
-
} finally {
|
|
95
|
-
terminateChildProcess(gateway, { force: true });
|
|
96
|
-
await observedRunPromise.catch(() => undefined);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
assert.ok(Date.now() - startedAt < 4_000, "watchdog should stop the worker quickly");
|
|
100
|
-
const finalStatus = await client.getSessionStatus({
|
|
101
|
-
sessionKey: "session-watchdog",
|
|
102
|
-
cwd: tempRoot,
|
|
103
|
-
agentId: "codex",
|
|
104
|
-
});
|
|
105
|
-
assert.match(finalStatus?.summary ?? "", /status=dead/);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
async function createFakeAcpx(tempRoot: string): Promise<{ command: string; env: NodeJS.ProcessEnv }> {
|
|
109
|
-
const scriptPath = path.join(tempRoot, "fake-acpx.js");
|
|
110
|
-
const wrapperPath = path.join(tempRoot, process.platform === "win32" ? "fake-acpx.cmd" : "fake-acpx");
|
|
111
|
-
|
|
112
|
-
await writeFile(scriptPath, `
|
|
113
|
-
const fs = require("node:fs/promises");
|
|
114
|
-
const path = require("node:path");
|
|
115
|
-
|
|
116
|
-
const args = process.argv.slice(2);
|
|
117
|
-
const stateDir = process.env.FAKE_ACPX_STATE;
|
|
118
|
-
|
|
119
|
-
function consumeGlobals(argv) {
|
|
120
|
-
const out = [...argv];
|
|
121
|
-
const result = [];
|
|
122
|
-
while (out.length > 0) {
|
|
123
|
-
const head = out[0];
|
|
124
|
-
if (head === "--format" || head === "--cwd" || head === "--ttl") {
|
|
125
|
-
out.shift();
|
|
126
|
-
out.shift();
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
if (head === "--json-strict" || head === "--approve-all" || head === "--approve-reads" || head === "--deny-all") {
|
|
130
|
-
out.shift();
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
result.push(...out);
|
|
134
|
-
break;
|
|
135
|
-
}
|
|
136
|
-
return result;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function flagValue(argv, name) {
|
|
140
|
-
const index = argv.indexOf(name);
|
|
141
|
-
if (index >= 0 && index + 1 < argv.length) {
|
|
142
|
-
return argv[index + 1];
|
|
143
|
-
}
|
|
144
|
-
return undefined;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async function readStdin() {
|
|
148
|
-
const chunks = [];
|
|
149
|
-
for await (const chunk of process.stdin) {
|
|
150
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
151
|
-
}
|
|
152
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function isAlive(pid) {
|
|
156
|
-
if (!Number.isFinite(pid) || pid <= 0) {
|
|
157
|
-
return false;
|
|
158
|
-
}
|
|
159
|
-
try {
|
|
160
|
-
process.kill(pid, 0);
|
|
161
|
-
return true;
|
|
162
|
-
} catch (error) {
|
|
163
|
-
return error && typeof error === "object" && error.code === "EPERM";
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
async function writeJsonLine(value) {
|
|
168
|
-
process.stdout.write(JSON.stringify(value) + "\\n");
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
async function main() {
|
|
172
|
-
if (args.includes("--version")) {
|
|
173
|
-
process.stdout.write("0.3.1\\n");
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const rest = consumeGlobals(args);
|
|
178
|
-
const agent = rest[0];
|
|
179
|
-
const verb = rest[1];
|
|
180
|
-
const tail = rest.slice(2);
|
|
181
|
-
const sessionName = flagValue(tail, "--session") || flagValue(tail, "--name") || tail[1];
|
|
182
|
-
const sessionFile = sessionName ? path.join(stateDir, sessionName + ".json") : "";
|
|
183
|
-
const runningFile = sessionName ? path.join(stateDir, sessionName + ".running") : "";
|
|
184
|
-
|
|
185
|
-
async function sessionExists() {
|
|
186
|
-
try {
|
|
187
|
-
await fs.access(sessionFile);
|
|
188
|
-
return true;
|
|
189
|
-
} catch {
|
|
190
|
-
return false;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
async function runningExists() {
|
|
195
|
-
try {
|
|
196
|
-
const payload = JSON.parse(await fs.readFile(runningFile, "utf8"));
|
|
197
|
-
return isAlive(payload && payload.pid);
|
|
198
|
-
} catch {
|
|
199
|
-
return false;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (verb === "sessions" && tail[0] === "ensure") {
|
|
204
|
-
if (await sessionExists()) {
|
|
205
|
-
await writeJsonLine({ acpxRecordId: "record-" + sessionName, acpxSessionId: "backend-" + sessionName, agentSessionId: "agent-" + sessionName, agent });
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
await writeJsonLine({ type: "error", code: "NO_SESSION", message: "missing session" });
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (verb === "sessions" && tail[0] === "new") {
|
|
213
|
-
await fs.writeFile(sessionFile, JSON.stringify({ sessionName, agent }), "utf8");
|
|
214
|
-
await writeJsonLine({ acpxRecordId: "record-" + sessionName, acpxSessionId: "backend-" + sessionName, agentSessionId: "agent-" + sessionName, agent });
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (verb === "status") {
|
|
219
|
-
if (!(await sessionExists())) {
|
|
220
|
-
await writeJsonLine({ type: "error", code: "NO_SESSION", message: "missing session" });
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
await writeJsonLine({
|
|
224
|
-
status: await runningExists() ? "alive" : "dead",
|
|
225
|
-
acpxRecordId: "record-" + sessionName,
|
|
226
|
-
acpxSessionId: "backend-" + sessionName,
|
|
227
|
-
agentSessionId: "agent-" + sessionName,
|
|
228
|
-
pid: 1234,
|
|
229
|
-
});
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (verb === "cancel") {
|
|
234
|
-
await fs.rm(runningFile, { force: true });
|
|
235
|
-
await writeJsonLine({ ok: true });
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (verb === "prompt") {
|
|
240
|
-
const text = (await readStdin()).trim();
|
|
241
|
-
await fs.writeFile(runningFile, JSON.stringify({ pid: process.pid }), "utf8");
|
|
242
|
-
process.on("SIGTERM", async () => {
|
|
243
|
-
await fs.rm(runningFile, { force: true });
|
|
244
|
-
process.exit(143);
|
|
245
|
-
});
|
|
246
|
-
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
247
|
-
await writeJsonLine({ text: "Working on " + text });
|
|
248
|
-
if (sessionName === "session-watchdog" || text.includes("stay alive")) {
|
|
249
|
-
setInterval(() => {}, 1_000);
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
253
|
-
await fs.rm(runningFile, { force: true });
|
|
254
|
-
await writeJsonLine({ type: "done" });
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (verb === "sessions" && tail[0] === "close") {
|
|
259
|
-
await fs.rm(sessionFile, { force: true });
|
|
260
|
-
await fs.rm(runningFile, { force: true });
|
|
261
|
-
await writeJsonLine({ ok: true });
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
process.stderr.write("unexpected args: " + JSON.stringify(args));
|
|
266
|
-
process.exit(1);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
main().catch((error) => {
|
|
270
|
-
process.stderr.write(String(error && error.stack ? error.stack : error));
|
|
271
|
-
process.exit(1);
|
|
272
|
-
});
|
|
273
|
-
`, "utf8");
|
|
274
|
-
|
|
275
|
-
if (process.platform === "win32") {
|
|
276
|
-
await writeFile(wrapperPath, `@echo off\r\nnode "${scriptPath}" %*\r\n`, "utf8");
|
|
277
|
-
} else {
|
|
278
|
-
await writeFile(wrapperPath, `#!/usr/bin/env bash\nnode "${scriptPath}" "$@"\n`, "utf8");
|
|
279
|
-
await chmod(wrapperPath, 0o755);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
return {
|
|
283
|
-
command: wrapperPath,
|
|
284
|
-
env: {
|
|
285
|
-
...process.env,
|
|
286
|
-
FAKE_ACPX_STATE: tempRoot,
|
|
287
|
-
},
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function createLogger() {
|
|
292
|
-
return {
|
|
293
|
-
info() {},
|
|
294
|
-
warn() {},
|
|
295
|
-
error() {},
|
|
296
|
-
debug() {},
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
async function waitFor(predicate: () => Promise<boolean>, timeoutMs = 2_000): Promise<void> {
|
|
301
|
-
const deadline = Date.now() + timeoutMs;
|
|
302
|
-
while (Date.now() < deadline) {
|
|
303
|
-
if (await predicate()) {
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
307
|
-
}
|
|
308
|
-
throw new Error("timed out waiting for condition");
|
|
309
|
-
}
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
ACPX_EXPECTED_VERSION,
|
|
6
|
-
ACPX_PACKAGE_NAME,
|
|
7
|
-
ensureAcpxCli,
|
|
8
|
-
} from "../src/dependencies/acpx.ts";
|
|
9
|
-
|
|
10
|
-
const ROOT_PREFIX = process.platform === "win32" ? "C:\\clawspec-test" : "/tmp/clawspec-test";
|
|
11
|
-
const OPENCLAW_PREFIX = process.platform === "win32" ? "C:\\openclaw-test" : "/opt/openclaw";
|
|
12
|
-
|
|
13
|
-
const LOCAL_COMMAND = path.join(
|
|
14
|
-
ROOT_PREFIX,
|
|
15
|
-
"node_modules",
|
|
16
|
-
".bin",
|
|
17
|
-
process.platform === "win32" ? "acpx.cmd" : "acpx",
|
|
18
|
-
);
|
|
19
|
-
const BUILTIN_COMMAND = path.join(
|
|
20
|
-
OPENCLAW_PREFIX,
|
|
21
|
-
"dist",
|
|
22
|
-
"extensions",
|
|
23
|
-
"acpx",
|
|
24
|
-
"node_modules",
|
|
25
|
-
".bin",
|
|
26
|
-
process.platform === "win32" ? "acpx.cmd" : "acpx",
|
|
27
|
-
);
|
|
28
|
-
const OPENCLAW_RUNTIME_ENTRYPOINT = path.join(
|
|
29
|
-
OPENCLAW_PREFIX,
|
|
30
|
-
"dist",
|
|
31
|
-
"index.js",
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
test("ensureAcpxCli uses the global acpx command when available", async () => {
|
|
35
|
-
const calls: Array<{ command: string; args: string[] }> = [];
|
|
36
|
-
const result = await ensureAcpxCli({
|
|
37
|
-
pluginRoot: ROOT_PREFIX,
|
|
38
|
-
runner: async ({ command, args }) => {
|
|
39
|
-
calls.push({ command, args });
|
|
40
|
-
if (command === LOCAL_COMMAND) {
|
|
41
|
-
return { code: 1, stdout: "", stderr: "not found" };
|
|
42
|
-
}
|
|
43
|
-
if (command === "acpx") {
|
|
44
|
-
return { code: 0, stdout: `${ACPX_EXPECTED_VERSION}\n`, stderr: "" };
|
|
45
|
-
}
|
|
46
|
-
return { code: 1, stdout: "", stderr: "unexpected command" };
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
assert.equal(result.source, "global");
|
|
51
|
-
assert.equal(result.version, ACPX_EXPECTED_VERSION);
|
|
52
|
-
assert.equal(calls.some((call) => call.command === "npm"), false);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test("ensureAcpxCli accepts a newer global acpx version", async () => {
|
|
56
|
-
const calls: Array<{ command: string; args: string[] }> = [];
|
|
57
|
-
const result = await ensureAcpxCli({
|
|
58
|
-
pluginRoot: ROOT_PREFIX,
|
|
59
|
-
runner: async ({ command, args }) => {
|
|
60
|
-
calls.push({ command, args });
|
|
61
|
-
if (command === LOCAL_COMMAND) {
|
|
62
|
-
return { code: 1, stdout: "", stderr: "not found" };
|
|
63
|
-
}
|
|
64
|
-
if (command === "acpx") {
|
|
65
|
-
return { code: 0, stdout: "0.3.2\n", stderr: "" };
|
|
66
|
-
}
|
|
67
|
-
return { code: 1, stdout: "", stderr: "unexpected command" };
|
|
68
|
-
},
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
assert.equal(result.source, "global");
|
|
72
|
-
assert.equal(result.version, "0.3.2");
|
|
73
|
-
assert.equal(calls.some((call) => call.command === "npm"), false);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test("ensureAcpxCli prefers the OpenClaw builtin acpx over an incompatible PATH acpx", async () => {
|
|
77
|
-
const calls: Array<{ command: string; args: string[] }> = [];
|
|
78
|
-
const result = await ensureAcpxCli({
|
|
79
|
-
pluginRoot: ROOT_PREFIX,
|
|
80
|
-
runtimeEntrypoint: OPENCLAW_RUNTIME_ENTRYPOINT,
|
|
81
|
-
runner: async ({ command, args }) => {
|
|
82
|
-
calls.push({ command, args });
|
|
83
|
-
if (command === LOCAL_COMMAND) {
|
|
84
|
-
return { code: 1, stdout: "", stderr: "not found" };
|
|
85
|
-
}
|
|
86
|
-
if (command === BUILTIN_COMMAND) {
|
|
87
|
-
return { code: 0, stdout: `${ACPX_EXPECTED_VERSION}\n`, stderr: "" };
|
|
88
|
-
}
|
|
89
|
-
if (command === "acpx") {
|
|
90
|
-
return { code: 0, stdout: "0.1.15\n", stderr: "" };
|
|
91
|
-
}
|
|
92
|
-
return { code: 1, stdout: "", stderr: "unexpected command" };
|
|
93
|
-
},
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
assert.equal(result.source, "builtin");
|
|
97
|
-
assert.equal(result.version, ACPX_EXPECTED_VERSION);
|
|
98
|
-
assert.equal(result.command, BUILTIN_COMMAND);
|
|
99
|
-
assert.equal(calls.some((call) => call.command === "npm"), false);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test("ensureAcpxCli installs a plugin-local acpx when none is available", async () => {
|
|
103
|
-
const calls: Array<{ command: string; args: string[] }> = [];
|
|
104
|
-
let localCheckCount = 0;
|
|
105
|
-
|
|
106
|
-
const result = await ensureAcpxCli({
|
|
107
|
-
pluginRoot: ROOT_PREFIX,
|
|
108
|
-
runner: async ({ command, args }) => {
|
|
109
|
-
calls.push({ command, args });
|
|
110
|
-
if (command === LOCAL_COMMAND) {
|
|
111
|
-
localCheckCount += 1;
|
|
112
|
-
if (localCheckCount === 1) {
|
|
113
|
-
return { code: 1, stdout: "", stderr: "not found" };
|
|
114
|
-
}
|
|
115
|
-
return { code: 0, stdout: `${ACPX_EXPECTED_VERSION}\n`, stderr: "" };
|
|
116
|
-
}
|
|
117
|
-
if (command === "acpx") {
|
|
118
|
-
return { code: 1, stdout: "", stderr: "not found" };
|
|
119
|
-
}
|
|
120
|
-
if (command === "npm") {
|
|
121
|
-
return { code: 0, stdout: "installed\n", stderr: "" };
|
|
122
|
-
}
|
|
123
|
-
return { code: 1, stdout: "", stderr: "unexpected command" };
|
|
124
|
-
},
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
assert.equal(result.source, "local");
|
|
128
|
-
assert.equal(result.version, ACPX_EXPECTED_VERSION);
|
|
129
|
-
assert.equal(
|
|
130
|
-
calls.some((call) => call.command === "npm" && call.args.includes(`${ACPX_PACKAGE_NAME}@${ACPX_EXPECTED_VERSION}`)),
|
|
131
|
-
true,
|
|
132
|
-
);
|
|
133
|
-
});
|