@usezombie/zombiectl 0.3.0
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/README.md +76 -0
- package/bin/zombiectl.js +11 -0
- package/bun.lock +29 -0
- package/package.json +28 -0
- package/scripts/run-tests.mjs +38 -0
- package/src/cli.js +275 -0
- package/src/commands/admin.js +39 -0
- package/src/commands/agent.js +98 -0
- package/src/commands/agent_harness.js +43 -0
- package/src/commands/agent_improvement_report.js +42 -0
- package/src/commands/agent_profile.js +39 -0
- package/src/commands/agent_proposals.js +158 -0
- package/src/commands/agent_scores.js +44 -0
- package/src/commands/core-ops.js +108 -0
- package/src/commands/core.js +537 -0
- package/src/commands/harness.js +35 -0
- package/src/commands/harness_activate.js +53 -0
- package/src/commands/harness_active.js +32 -0
- package/src/commands/harness_compile.js +40 -0
- package/src/commands/harness_source.js +72 -0
- package/src/commands/run_preview.js +212 -0
- package/src/commands/run_preview_walk.js +1 -0
- package/src/commands/runs.js +35 -0
- package/src/commands/spec_init.js +287 -0
- package/src/commands/workspace_billing.js +26 -0
- package/src/constants/error-codes.js +1 -0
- package/src/lib/agent-loop.js +106 -0
- package/src/lib/analytics.js +114 -0
- package/src/lib/api-paths.js +2 -0
- package/src/lib/browser.js +96 -0
- package/src/lib/http.js +149 -0
- package/src/lib/sse-parser.js +50 -0
- package/src/lib/state.js +67 -0
- package/src/lib/tool-executors.js +110 -0
- package/src/lib/walk-dir.js +41 -0
- package/src/program/args.js +95 -0
- package/src/program/auth-guard.js +12 -0
- package/src/program/auth-token.js +44 -0
- package/src/program/banner.js +46 -0
- package/src/program/command-registry.js +17 -0
- package/src/program/http-client.js +38 -0
- package/src/program/io.js +83 -0
- package/src/program/routes.js +20 -0
- package/src/program/suggest.js +76 -0
- package/src/program/validate.js +24 -0
- package/src/ui-progress.js +59 -0
- package/src/ui-theme.js +62 -0
- package/test/admin_config.unit.test.js +25 -0
- package/test/agent-loop.unit.test.js +497 -0
- package/test/agent_harness.unit.test.js +52 -0
- package/test/agent_improvement_report.unit.test.js +74 -0
- package/test/agent_profile.unit.test.js +156 -0
- package/test/agent_proposals.unit.test.js +167 -0
- package/test/agent_scores.unit.test.js +220 -0
- package/test/analytics.unit.test.js +41 -0
- package/test/args.unit.test.js +69 -0
- package/test/auth-guard.test.js +33 -0
- package/test/auth-token.unit.test.js +112 -0
- package/test/banner.unit.test.js +442 -0
- package/test/browser.unit.test.js +16 -0
- package/test/cli-analytics.unit.test.js +296 -0
- package/test/did-you-mean.integration.test.js +76 -0
- package/test/doctor-json.test.js +81 -0
- package/test/error-codes.unit.test.js +7 -0
- package/test/harness-command.unit.test.js +180 -0
- package/test/harness-compile.test.js +81 -0
- package/test/harness-lifecycle.integration.test.js +339 -0
- package/test/harness-source-put.test.js +72 -0
- package/test/harness_activate.unit.test.js +48 -0
- package/test/harness_active.unit.test.js +53 -0
- package/test/harness_compile.unit.test.js +54 -0
- package/test/harness_source.unit.test.js +59 -0
- package/test/help.test.js +276 -0
- package/test/helpers-fs.js +32 -0
- package/test/helpers.js +31 -0
- package/test/io.unit.test.js +57 -0
- package/test/login.unit.test.js +115 -0
- package/test/logout.unit.test.js +65 -0
- package/test/parse.test.js +16 -0
- package/test/run-preview.edge.test.js +422 -0
- package/test/run-preview.integration.test.js +135 -0
- package/test/run-preview.security.test.js +246 -0
- package/test/run-preview.unit.test.js +131 -0
- package/test/run.unit.test.js +149 -0
- package/test/runs-cancel.unit.test.js +288 -0
- package/test/runs-list.unit.test.js +105 -0
- package/test/skill-secret.unit.test.js +94 -0
- package/test/spec-init.edge.test.js +232 -0
- package/test/spec-init.integration.test.js +128 -0
- package/test/spec-init.security.test.js +285 -0
- package/test/spec-init.unit.test.js +160 -0
- package/test/specs-sync.unit.test.js +164 -0
- package/test/sse-parser.unit.test.js +54 -0
- package/test/state.unit.test.js +34 -0
- package/test/streamfetch.unit.test.js +211 -0
- package/test/suggest.test.js +75 -0
- package/test/tool-executors.unit.test.js +165 -0
- package/test/validate.test.js +81 -0
- package/test/workspace-add.test.js +106 -0
- package/test/workspace.unit.test.js +230 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { test } from "bun:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { Writable } from "node:stream";
|
|
7
|
+
|
|
8
|
+
import { runCli } from "../src/cli.js";
|
|
9
|
+
import { cliAnalytics } from "../src/lib/analytics.js";
|
|
10
|
+
|
|
11
|
+
function bufferStream() {
|
|
12
|
+
let data = "";
|
|
13
|
+
return {
|
|
14
|
+
stream: new Writable({
|
|
15
|
+
write(chunk, _enc, cb) {
|
|
16
|
+
data += String(chunk);
|
|
17
|
+
cb();
|
|
18
|
+
},
|
|
19
|
+
}),
|
|
20
|
+
read: () => data,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function makeToken(payload) {
|
|
25
|
+
const header = Buffer.from(JSON.stringify({ alg: "none", typ: "JWT" })).toString("base64url");
|
|
26
|
+
const body = Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
27
|
+
return `${header}.${body}.sig`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function withAnalyticsStub(fn) {
|
|
31
|
+
const originalCreate = cliAnalytics.createCliAnalytics;
|
|
32
|
+
const originalTrack = cliAnalytics.trackCliEvent;
|
|
33
|
+
const originalShutdown = cliAnalytics.shutdownCliAnalytics;
|
|
34
|
+
|
|
35
|
+
return (async () => {
|
|
36
|
+
try {
|
|
37
|
+
await fn();
|
|
38
|
+
} finally {
|
|
39
|
+
cliAnalytics.createCliAnalytics = originalCreate;
|
|
40
|
+
cliAnalytics.trackCliEvent = originalTrack;
|
|
41
|
+
cliAnalytics.shutdownCliAnalytics = originalShutdown;
|
|
42
|
+
}
|
|
43
|
+
})();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function withStateDir(fn) {
|
|
47
|
+
const previous = process.env.ZOMBIE_STATE_DIR;
|
|
48
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "zombiectl-analytics-"));
|
|
49
|
+
process.env.ZOMBIE_STATE_DIR = dir;
|
|
50
|
+
try {
|
|
51
|
+
return await fn(dir);
|
|
52
|
+
} finally {
|
|
53
|
+
if (previous === undefined) delete process.env.ZOMBIE_STATE_DIR;
|
|
54
|
+
else process.env.ZOMBIE_STATE_DIR = previous;
|
|
55
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
test("runCli tracks login success with post-login distinct id and shuts down analytics", async () => {
|
|
60
|
+
await withStateDir(async () => {
|
|
61
|
+
await withAnalyticsStub(async () => {
|
|
62
|
+
const events = [];
|
|
63
|
+
let shutdownClient = null;
|
|
64
|
+
const analyticsClient = { name: "test-client" };
|
|
65
|
+
const clerkToken = makeToken({ sub: "user_login_123" });
|
|
66
|
+
|
|
67
|
+
cliAnalytics.createCliAnalytics = async () => analyticsClient;
|
|
68
|
+
cliAnalytics.trackCliEvent = (client, distinctId, event, properties = {}) => {
|
|
69
|
+
events.push({ client, distinctId, event, properties });
|
|
70
|
+
};
|
|
71
|
+
cliAnalytics.shutdownCliAnalytics = async (client) => {
|
|
72
|
+
shutdownClient = client;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const stdout = bufferStream();
|
|
76
|
+
const stderr = bufferStream();
|
|
77
|
+
let pollCount = 0;
|
|
78
|
+
const fetchImpl = async (_url, options = {}) => {
|
|
79
|
+
if (options.method === "POST") {
|
|
80
|
+
return {
|
|
81
|
+
ok: true,
|
|
82
|
+
status: 201,
|
|
83
|
+
text: async () => JSON.stringify({ session_id: "sess_analytics", login_url: "https://login.test" }),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
pollCount += 1;
|
|
87
|
+
return {
|
|
88
|
+
ok: true,
|
|
89
|
+
status: 200,
|
|
90
|
+
text: async () => JSON.stringify({ status: "complete", token: clerkToken }),
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const code = await runCli(["login", "--no-open"], {
|
|
95
|
+
env: { ...process.env, NO_COLOR: "1", BROWSER: "false" },
|
|
96
|
+
stdout: stdout.stream,
|
|
97
|
+
stderr: stderr.stream,
|
|
98
|
+
fetchImpl,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
assert.equal(code, 0);
|
|
102
|
+
assert.equal(pollCount, 1);
|
|
103
|
+
assert.equal(events.length, 4);
|
|
104
|
+
assert.deepEqual(events.map(({ event }) => event), [
|
|
105
|
+
"cli_command_started",
|
|
106
|
+
"cli_command_finished",
|
|
107
|
+
"user_authenticated",
|
|
108
|
+
"login_completed",
|
|
109
|
+
]);
|
|
110
|
+
assert.deepEqual(events[0], {
|
|
111
|
+
client: analyticsClient,
|
|
112
|
+
distinctId: null,
|
|
113
|
+
event: "cli_command_started",
|
|
114
|
+
properties: { command: "login", json_mode: "false" },
|
|
115
|
+
});
|
|
116
|
+
assert.deepEqual(events[1], {
|
|
117
|
+
client: analyticsClient,
|
|
118
|
+
distinctId: null,
|
|
119
|
+
event: "cli_command_finished",
|
|
120
|
+
properties: { command: "login", exit_code: "0", session_id: "sess_analytics" },
|
|
121
|
+
});
|
|
122
|
+
assert.deepEqual(events[2], {
|
|
123
|
+
client: analyticsClient,
|
|
124
|
+
distinctId: "user_login_123",
|
|
125
|
+
event: "user_authenticated",
|
|
126
|
+
properties: { command: "login", session_id: "sess_analytics" },
|
|
127
|
+
});
|
|
128
|
+
assert.deepEqual(events[3], {
|
|
129
|
+
client: analyticsClient,
|
|
130
|
+
distinctId: "user_login_123",
|
|
131
|
+
event: "login_completed",
|
|
132
|
+
properties: { command: "login", session_id: "sess_analytics" },
|
|
133
|
+
});
|
|
134
|
+
assert.equal(shutdownClient, analyticsClient);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("runCli tracks workspace creation with existing distinct id", async () => {
|
|
140
|
+
await withStateDir(async () => {
|
|
141
|
+
await withAnalyticsStub(async () => {
|
|
142
|
+
const events = [];
|
|
143
|
+
const analyticsClient = { name: "workspace-client" };
|
|
144
|
+
const clerkToken = makeToken({ sub: "user_workspace_456" });
|
|
145
|
+
|
|
146
|
+
cliAnalytics.createCliAnalytics = async () => analyticsClient;
|
|
147
|
+
cliAnalytics.trackCliEvent = (client, distinctId, event, properties = {}) => {
|
|
148
|
+
events.push({ client, distinctId, event, properties });
|
|
149
|
+
};
|
|
150
|
+
cliAnalytics.shutdownCliAnalytics = async () => {};
|
|
151
|
+
|
|
152
|
+
const stdout = bufferStream();
|
|
153
|
+
const stderr = bufferStream();
|
|
154
|
+
const fetchImpl = async () => ({
|
|
155
|
+
ok: true,
|
|
156
|
+
status: 201,
|
|
157
|
+
text: async () =>
|
|
158
|
+
JSON.stringify({
|
|
159
|
+
workspace_id: "ws_123456789abc",
|
|
160
|
+
repo_url: "https://github.com/acme/repo",
|
|
161
|
+
default_branch: "main",
|
|
162
|
+
install_url: "https://github.com/apps/usezombie/installations/new?state=ws_123456789abc",
|
|
163
|
+
request_id: "req_workspace",
|
|
164
|
+
}),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const code = await runCli(["workspace", "add", "https://github.com/acme/repo"], {
|
|
168
|
+
env: { ...process.env, NO_COLOR: "1", BROWSER: "false", ZOMBIE_TOKEN: clerkToken },
|
|
169
|
+
stdout: stdout.stream,
|
|
170
|
+
stderr: stderr.stream,
|
|
171
|
+
fetchImpl,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
assert.equal(code, 0);
|
|
175
|
+
assert.equal(events.length, 4);
|
|
176
|
+
assert.deepEqual(events.map(({ event }) => event), [
|
|
177
|
+
"cli_command_started",
|
|
178
|
+
"cli_command_finished",
|
|
179
|
+
"workspace_created",
|
|
180
|
+
"workspace_add_completed",
|
|
181
|
+
]);
|
|
182
|
+
assert.deepEqual(events[0], {
|
|
183
|
+
client: analyticsClient,
|
|
184
|
+
distinctId: "user_workspace_456",
|
|
185
|
+
event: "cli_command_started",
|
|
186
|
+
properties: { command: "workspace", json_mode: "false" },
|
|
187
|
+
});
|
|
188
|
+
assert.deepEqual(events[1], {
|
|
189
|
+
client: analyticsClient,
|
|
190
|
+
distinctId: "user_workspace_456",
|
|
191
|
+
event: "cli_command_finished",
|
|
192
|
+
properties: {
|
|
193
|
+
command: "workspace",
|
|
194
|
+
exit_code: "0",
|
|
195
|
+
workspace_id: "ws_123456789abc",
|
|
196
|
+
repo_url: "https://github.com/acme/repo",
|
|
197
|
+
branch: "main",
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
assert.deepEqual(events[2], {
|
|
201
|
+
client: analyticsClient,
|
|
202
|
+
distinctId: "user_workspace_456",
|
|
203
|
+
event: "workspace_created",
|
|
204
|
+
properties: {
|
|
205
|
+
command: "workspace",
|
|
206
|
+
workspace_id: "ws_123456789abc",
|
|
207
|
+
repo_url: "https://github.com/acme/repo",
|
|
208
|
+
branch: "main",
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
assert.deepEqual(events[3], {
|
|
212
|
+
client: analyticsClient,
|
|
213
|
+
distinctId: "user_workspace_456",
|
|
214
|
+
event: "workspace_add_completed",
|
|
215
|
+
properties: {
|
|
216
|
+
command: "workspace",
|
|
217
|
+
workspace_id: "ws_123456789abc",
|
|
218
|
+
repo_url: "https://github.com/acme/repo",
|
|
219
|
+
branch: "main",
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("runCli tracks unknown-command errors and still shuts down analytics when tracking throws", async () => {
|
|
227
|
+
await withStateDir(async () => {
|
|
228
|
+
await withAnalyticsStub(async () => {
|
|
229
|
+
const events = [];
|
|
230
|
+
let shutdownCalls = 0;
|
|
231
|
+
const analyticsClient = {
|
|
232
|
+
capture({ distinctId, event, properties }) {
|
|
233
|
+
events.push({ distinctId, event, properties });
|
|
234
|
+
throw new Error("capture failed");
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
cliAnalytics.createCliAnalytics = async () => analyticsClient;
|
|
239
|
+
cliAnalytics.shutdownCliAnalytics = async () => {
|
|
240
|
+
shutdownCalls += 1;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const stdout = bufferStream();
|
|
244
|
+
const stderr = bufferStream();
|
|
245
|
+
|
|
246
|
+
const code = await runCli(["runx"], {
|
|
247
|
+
env: { ...process.env, NO_COLOR: "1" },
|
|
248
|
+
stdout: stdout.stream,
|
|
249
|
+
stderr: stderr.stream,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
assert.equal(code, 2);
|
|
253
|
+
assert.match(stderr.read(), /unknown command: runx/);
|
|
254
|
+
assert.equal(events.length, 1);
|
|
255
|
+
assert.equal(events[0].event, "cli_error");
|
|
256
|
+
assert.equal(events[0].distinctId, "anonymous");
|
|
257
|
+
assert.equal(shutdownCalls, 1);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("runCli honors analytics opt-out with bundled key", async () => {
|
|
263
|
+
await withAnalyticsStub(async () => {
|
|
264
|
+
let createCalls = 0;
|
|
265
|
+
let trackCalls = 0;
|
|
266
|
+
let shutdownCalls = 0;
|
|
267
|
+
|
|
268
|
+
cliAnalytics.createCliAnalytics = async (env) => {
|
|
269
|
+
createCalls += 1;
|
|
270
|
+
assert.equal(env.ZOMBIE_POSTHOG_ENABLED, "false");
|
|
271
|
+
return null;
|
|
272
|
+
};
|
|
273
|
+
cliAnalytics.trackCliEvent = (client) => {
|
|
274
|
+
if (!client) return;
|
|
275
|
+
trackCalls += 1;
|
|
276
|
+
};
|
|
277
|
+
cliAnalytics.shutdownCliAnalytics = async () => {
|
|
278
|
+
shutdownCalls += 1;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const stdout = bufferStream();
|
|
282
|
+
const stderr = bufferStream();
|
|
283
|
+
|
|
284
|
+
const code = await runCli(["runx"], {
|
|
285
|
+
env: { ...process.env, NO_COLOR: "1", ZOMBIE_POSTHOG_ENABLED: "false" },
|
|
286
|
+
stdout: stdout.stream,
|
|
287
|
+
stderr: stderr.stream,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
assert.equal(code, 2);
|
|
291
|
+
assert.match(stderr.read(), /unknown command: runx/);
|
|
292
|
+
assert.equal(createCalls, 1);
|
|
293
|
+
assert.equal(trackCalls, 0);
|
|
294
|
+
assert.equal(shutdownCalls, 1);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { Writable } from "node:stream";
|
|
3
|
+
import { runCli } from "../src/cli.js";
|
|
4
|
+
|
|
5
|
+
function bufferStream() {
|
|
6
|
+
let data = "";
|
|
7
|
+
return {
|
|
8
|
+
stream: new Writable({
|
|
9
|
+
write(chunk, _enc, cb) {
|
|
10
|
+
data += String(chunk);
|
|
11
|
+
cb();
|
|
12
|
+
},
|
|
13
|
+
}),
|
|
14
|
+
read: () => data,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("did-you-mean integration", () => {
|
|
19
|
+
test("'runx' suggests 'run'", async () => {
|
|
20
|
+
const out = bufferStream();
|
|
21
|
+
const err = bufferStream();
|
|
22
|
+
const code = await runCli(["runx"], {
|
|
23
|
+
stdout: out.stream,
|
|
24
|
+
stderr: err.stream,
|
|
25
|
+
env: { ...process.env, NO_COLOR: "1" },
|
|
26
|
+
});
|
|
27
|
+
expect(code).toBe(2);
|
|
28
|
+
const errText = err.read();
|
|
29
|
+
expect(errText).toContain("unknown command: runx");
|
|
30
|
+
expect(errText).toContain("run");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("'workspace ad' suggests 'workspace add'", async () => {
|
|
34
|
+
const out = bufferStream();
|
|
35
|
+
const err = bufferStream();
|
|
36
|
+
// runCli sees command="workspace" args=["ad"], which IS a valid route (workspace)
|
|
37
|
+
// but the workspace handler itself handles bad subcommands
|
|
38
|
+
// For did-you-mean, we test a truly unknown top-level command
|
|
39
|
+
const code = await runCli(["workspac"], {
|
|
40
|
+
stdout: out.stream,
|
|
41
|
+
stderr: err.stream,
|
|
42
|
+
env: { ...process.env, NO_COLOR: "1" },
|
|
43
|
+
});
|
|
44
|
+
expect(code).toBe(2);
|
|
45
|
+
const errText = err.read();
|
|
46
|
+
expect(errText).toContain("unknown command");
|
|
47
|
+
expect(errText).toContain("workspace");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("completely unrelated input shows help hint", async () => {
|
|
51
|
+
const out = bufferStream();
|
|
52
|
+
const err = bufferStream();
|
|
53
|
+
const code = await runCli(["zzzzzzzzzzzzzzzzz"], {
|
|
54
|
+
stdout: out.stream,
|
|
55
|
+
stderr: err.stream,
|
|
56
|
+
env: { ...process.env, NO_COLOR: "1" },
|
|
57
|
+
});
|
|
58
|
+
expect(code).toBe(2);
|
|
59
|
+
const errText = err.read();
|
|
60
|
+
expect(errText).toContain("unknown command");
|
|
61
|
+
expect(errText).toContain("--help");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("'logn' suggests 'login'", async () => {
|
|
65
|
+
const out = bufferStream();
|
|
66
|
+
const err = bufferStream();
|
|
67
|
+
const code = await runCli(["logn"], {
|
|
68
|
+
stdout: out.stream,
|
|
69
|
+
stderr: err.stream,
|
|
70
|
+
env: { ...process.env, NO_COLOR: "1" },
|
|
71
|
+
});
|
|
72
|
+
expect(code).toBe(2);
|
|
73
|
+
const errText = err.read();
|
|
74
|
+
expect(errText).toContain("login");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { test } from "bun:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { Writable } from "node:stream";
|
|
7
|
+
import { runCli } from "../src/cli.js";
|
|
8
|
+
|
|
9
|
+
function bufferStream() {
|
|
10
|
+
let data = "";
|
|
11
|
+
return {
|
|
12
|
+
stream: new Writable({
|
|
13
|
+
write(chunk, _enc, cb) {
|
|
14
|
+
data += String(chunk);
|
|
15
|
+
cb();
|
|
16
|
+
},
|
|
17
|
+
}),
|
|
18
|
+
read: () => data,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function withStateDir(fn) {
|
|
23
|
+
const previous = process.env.ZOMBIE_STATE_DIR;
|
|
24
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "zombiectl-doctor-"));
|
|
25
|
+
process.env.ZOMBIE_STATE_DIR = dir;
|
|
26
|
+
try {
|
|
27
|
+
await fs.writeFile(
|
|
28
|
+
path.join(dir, "workspaces.json"),
|
|
29
|
+
`${JSON.stringify({ current_workspace_id: "ws_test", items: [{ workspace_id: "ws_test" }] }, null, 2)}\n`,
|
|
30
|
+
"utf8",
|
|
31
|
+
);
|
|
32
|
+
return await fn();
|
|
33
|
+
} finally {
|
|
34
|
+
if (previous === undefined) delete process.env.ZOMBIE_STATE_DIR;
|
|
35
|
+
else process.env.ZOMBIE_STATE_DIR = previous;
|
|
36
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
test("doctor --json emits machine-parseable payload", async () => {
|
|
41
|
+
await withStateDir(async () => {
|
|
42
|
+
const out = bufferStream();
|
|
43
|
+
const err = bufferStream();
|
|
44
|
+
|
|
45
|
+
const fetchImpl = async (url) => {
|
|
46
|
+
if (url.endsWith("/healthz")) {
|
|
47
|
+
return {
|
|
48
|
+
ok: true,
|
|
49
|
+
status: 200,
|
|
50
|
+
text: async () => JSON.stringify({ status: "ok", service: "zombied" }),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (url.endsWith("/readyz")) {
|
|
54
|
+
return {
|
|
55
|
+
ok: true,
|
|
56
|
+
status: 200,
|
|
57
|
+
text: async () => JSON.stringify({ ready: true }),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
status: 404,
|
|
63
|
+
statusText: "Not Found",
|
|
64
|
+
text: async () => JSON.stringify({ error: { code: "NOT_FOUND", message: "Not found" } }),
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const code = await runCli(["--json", "doctor"], {
|
|
69
|
+
env: { ...process.env, ZOMBIE_TOKEN: "header.payload.sig" },
|
|
70
|
+
stdout: out.stream,
|
|
71
|
+
stderr: err.stream,
|
|
72
|
+
fetchImpl,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
assert.equal(code, 0);
|
|
76
|
+
const parsed = JSON.parse(out.read());
|
|
77
|
+
assert.equal(parsed.ok, true);
|
|
78
|
+
assert.equal(Array.isArray(parsed.checks), true);
|
|
79
|
+
assert.equal(parsed.checks.length >= 4, true);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { test } from "bun:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { ERR_BILLING_CREDIT_EXHAUSTED } from "../src/constants/error-codes.js";
|
|
4
|
+
|
|
5
|
+
test("ERR_BILLING_CREDIT_EXHAUSTED matches canonical code", () => {
|
|
6
|
+
assert.equal(ERR_BILLING_CREDIT_EXHAUSTED, "UZ-BILLING-005");
|
|
7
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { test } from "bun:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { Writable } from "node:stream";
|
|
4
|
+
import { commandHarness } from "../src/commands/harness.js";
|
|
5
|
+
|
|
6
|
+
function bufferStream() {
|
|
7
|
+
let data = "";
|
|
8
|
+
return {
|
|
9
|
+
stream: new Writable({
|
|
10
|
+
write(chunk, _enc, cb) {
|
|
11
|
+
data += String(chunk);
|
|
12
|
+
cb();
|
|
13
|
+
},
|
|
14
|
+
}),
|
|
15
|
+
read: () => data,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseFlags(tokens) {
|
|
20
|
+
const options = {};
|
|
21
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
22
|
+
const token = tokens[i];
|
|
23
|
+
if (!token.startsWith("--")) continue;
|
|
24
|
+
const key = token.slice(2);
|
|
25
|
+
const next = tokens[i + 1];
|
|
26
|
+
if (next && !next.startsWith("--")) {
|
|
27
|
+
options[key] = next;
|
|
28
|
+
i += 1;
|
|
29
|
+
} else {
|
|
30
|
+
options[key] = true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { options, positionals: [] };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
test("commandHarness source put builds source_markdown payload", async () => {
|
|
37
|
+
const out = bufferStream();
|
|
38
|
+
const err = bufferStream();
|
|
39
|
+
let captured = null;
|
|
40
|
+
const deps = {
|
|
41
|
+
parseFlags,
|
|
42
|
+
request: async (_ctx, reqPath, options) => {
|
|
43
|
+
captured = { reqPath, options };
|
|
44
|
+
return { config_version_id: "pver_1" };
|
|
45
|
+
},
|
|
46
|
+
apiHeaders: () => ({ "Content-Type": "application/json" }),
|
|
47
|
+
ui: { ok: (s) => s, err: (s) => s, info: (s) => s },
|
|
48
|
+
printJson: () => {},
|
|
49
|
+
writeLine: (stream, line = "") => stream.write(`${line}\n`),
|
|
50
|
+
readFile: async () => "# Harness\n\n```json\n{\"profile_id\":\"ws_123-harness\",\"stages\":[]}\n```",
|
|
51
|
+
resolvePath: (p) => p,
|
|
52
|
+
};
|
|
53
|
+
const ctx = { stdout: out.stream, stderr: err.stream, jsonMode: false };
|
|
54
|
+
const workspaces = { current_workspace_id: "ws_123" };
|
|
55
|
+
|
|
56
|
+
const code = await commandHarness(
|
|
57
|
+
ctx,
|
|
58
|
+
["source", "put", "--file", "profile.md", "--agent-id", "ws_123-harness"],
|
|
59
|
+
workspaces,
|
|
60
|
+
deps,
|
|
61
|
+
);
|
|
62
|
+
assert.equal(code, 0);
|
|
63
|
+
assert.equal(err.read(), "");
|
|
64
|
+
assert.equal(captured.reqPath, "/v1/workspaces/ws_123/harness/source");
|
|
65
|
+
const body = JSON.parse(captured.options.body);
|
|
66
|
+
assert.equal(body.agent_id, "ws_123-harness");
|
|
67
|
+
assert.equal(body.name, "profile");
|
|
68
|
+
assert.match(body.source_markdown, /# Harness/);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("commandHarness compile sends explicit profile selectors", async () => {
|
|
72
|
+
let captured = null;
|
|
73
|
+
const deps = {
|
|
74
|
+
parseFlags,
|
|
75
|
+
request: async (_ctx, reqPath, options) => {
|
|
76
|
+
captured = { reqPath, options };
|
|
77
|
+
return { compile_job_id: "cjob_1", is_valid: true };
|
|
78
|
+
},
|
|
79
|
+
apiHeaders: () => ({ "Content-Type": "application/json" }),
|
|
80
|
+
ui: { ok: (s) => s, err: (s) => s, info: (s) => s },
|
|
81
|
+
printJson: () => {},
|
|
82
|
+
writeLine: () => {},
|
|
83
|
+
};
|
|
84
|
+
const ctx = { stdout: new Writable({ write(_c, _e, cb) { cb(); } }), stderr: new Writable({ write(_c, _e, cb) { cb(); } }), jsonMode: false };
|
|
85
|
+
const workspaces = { current_workspace_id: "ws_123" };
|
|
86
|
+
|
|
87
|
+
const code = await commandHarness(
|
|
88
|
+
ctx,
|
|
89
|
+
["compile", "--config-version-id", "pver_9"],
|
|
90
|
+
workspaces,
|
|
91
|
+
deps,
|
|
92
|
+
);
|
|
93
|
+
assert.equal(code, 0);
|
|
94
|
+
assert.equal(captured.reqPath, "/v1/workspaces/ws_123/harness/compile");
|
|
95
|
+
const body = JSON.parse(captured.options.body);
|
|
96
|
+
assert.equal(body.agent_id, null);
|
|
97
|
+
assert.equal(body.config_version_id, "pver_9");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("commandHarness activate requires profile version id", async () => {
|
|
101
|
+
const out = bufferStream();
|
|
102
|
+
const err = bufferStream();
|
|
103
|
+
const deps = {
|
|
104
|
+
parseFlags,
|
|
105
|
+
request: async () => {
|
|
106
|
+
throw new Error("should not be called");
|
|
107
|
+
},
|
|
108
|
+
apiHeaders: () => ({ "Content-Type": "application/json" }),
|
|
109
|
+
ui: { ok: (s) => s, err: (s) => s, info: (s) => s },
|
|
110
|
+
printJson: () => {},
|
|
111
|
+
writeLine: (stream, line = "") => stream.write(`${line}\n`),
|
|
112
|
+
};
|
|
113
|
+
const ctx = { stdout: out.stream, stderr: err.stream, jsonMode: false };
|
|
114
|
+
const workspaces = { current_workspace_id: "ws_123" };
|
|
115
|
+
|
|
116
|
+
const code = await commandHarness(ctx, ["activate"], workspaces, deps);
|
|
117
|
+
assert.equal(code, 2);
|
|
118
|
+
assert.match(err.read(), /requires --config-version-id/);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("commandHarness activate sends profile version and activated_by", async () => {
|
|
122
|
+
let captured = null;
|
|
123
|
+
const deps = {
|
|
124
|
+
parseFlags,
|
|
125
|
+
request: async (_ctx, reqPath, options) => {
|
|
126
|
+
captured = { reqPath, options };
|
|
127
|
+
return {
|
|
128
|
+
agent_id: "ws_123-harness",
|
|
129
|
+
config_version_id: "pver_2",
|
|
130
|
+
run_snapshot_version: "pver_2",
|
|
131
|
+
activated_at: 1730000000,
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
apiHeaders: () => ({ "Content-Type": "application/json" }),
|
|
135
|
+
ui: { ok: (s) => s, err: (s) => s, info: (s) => s },
|
|
136
|
+
printJson: () => {},
|
|
137
|
+
writeLine: () => {},
|
|
138
|
+
};
|
|
139
|
+
const ctx = { stdout: new Writable({ write(_c, _e, cb) { cb(); } }), stderr: new Writable({ write(_c, _e, cb) { cb(); } }), jsonMode: false };
|
|
140
|
+
const workspaces = { current_workspace_id: "ws_123" };
|
|
141
|
+
|
|
142
|
+
const code = await commandHarness(
|
|
143
|
+
ctx,
|
|
144
|
+
["activate", "--config-version-id", "pver_2", "--activated-by", "operator"],
|
|
145
|
+
workspaces,
|
|
146
|
+
deps,
|
|
147
|
+
);
|
|
148
|
+
assert.equal(code, 0);
|
|
149
|
+
assert.equal(captured.reqPath, "/v1/workspaces/ws_123/harness/activate");
|
|
150
|
+
const body = JSON.parse(captured.options.body);
|
|
151
|
+
assert.equal(body.config_version_id, "pver_2");
|
|
152
|
+
assert.equal(body.activated_by, "operator");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("commandHarness active queries active profile endpoint", async () => {
|
|
156
|
+
let captured = null;
|
|
157
|
+
const deps = {
|
|
158
|
+
parseFlags,
|
|
159
|
+
request: async (_ctx, reqPath, options) => {
|
|
160
|
+
captured = { reqPath, options };
|
|
161
|
+
return {
|
|
162
|
+
source: "active",
|
|
163
|
+
agent_id: "ws_123-harness",
|
|
164
|
+
config_version_id: "pver_2",
|
|
165
|
+
run_snapshot_version: "pver_2",
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
apiHeaders: () => ({ "Content-Type": "application/json" }),
|
|
169
|
+
ui: { ok: (s) => s, err: (s) => s, info: (s) => s },
|
|
170
|
+
printJson: () => {},
|
|
171
|
+
writeLine: () => {},
|
|
172
|
+
};
|
|
173
|
+
const ctx = { stdout: new Writable({ write(_c, _e, cb) { cb(); } }), stderr: new Writable({ write(_c, _e, cb) { cb(); } }), jsonMode: false };
|
|
174
|
+
const workspaces = { current_workspace_id: "ws_123" };
|
|
175
|
+
|
|
176
|
+
const code = await commandHarness(ctx, ["active"], workspaces, deps);
|
|
177
|
+
assert.equal(code, 0);
|
|
178
|
+
assert.equal(captured.reqPath, "/v1/workspaces/ws_123/harness/active");
|
|
179
|
+
assert.equal(captured.options.method, "GET");
|
|
180
|
+
});
|