@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,156 @@
|
|
|
1
|
+
import { test } from "bun:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { commandAgentProfile } from "../src/commands/agent_profile.js";
|
|
4
|
+
import {
|
|
5
|
+
makeNoop,
|
|
6
|
+
ui, ApiError,
|
|
7
|
+
AGENT_ID, AGENT_NAME, WS_ID,
|
|
8
|
+
} from "./helpers.js";
|
|
9
|
+
|
|
10
|
+
const SAMPLE_AGENT = {
|
|
11
|
+
agent_id: AGENT_ID,
|
|
12
|
+
name: AGENT_NAME,
|
|
13
|
+
status: "ACTIVE",
|
|
14
|
+
workspace_id: WS_ID,
|
|
15
|
+
trust_level: "TRUSTED",
|
|
16
|
+
trust_streak_runs: 10,
|
|
17
|
+
improvement_stalled_warning: false,
|
|
18
|
+
created_at: 1700000000000,
|
|
19
|
+
updated_at: 1700000001000,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// ── T1: Happy path ────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
test("commandAgentProfile calls GET /v1/agents/{agent_id}", async () => {
|
|
25
|
+
let calledUrl = null;
|
|
26
|
+
const deps = {
|
|
27
|
+
request: async (_ctx, url) => { calledUrl = url; return SAMPLE_AGENT; },
|
|
28
|
+
apiHeaders: () => ({}),
|
|
29
|
+
ui, printJson: () => {}, printKeyValue: () => {}, writeLine: () => {},
|
|
30
|
+
};
|
|
31
|
+
const parsed = { options: {}, positionals: [] };
|
|
32
|
+
const code = await commandAgentProfile({ stdout: makeNoop(), stderr: makeNoop(), jsonMode: false }, parsed, AGENT_ID, deps);
|
|
33
|
+
assert.equal(code, 0);
|
|
34
|
+
assert.match(calledUrl, new RegExp(AGENT_ID));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("commandAgentProfile json mode outputs raw response", async () => {
|
|
38
|
+
let printed = null;
|
|
39
|
+
const deps = {
|
|
40
|
+
request: async () => SAMPLE_AGENT,
|
|
41
|
+
apiHeaders: () => ({}),
|
|
42
|
+
ui, printJson: (_stream, v) => { printed = v; }, printKeyValue: () => {}, writeLine: () => {},
|
|
43
|
+
};
|
|
44
|
+
const parsed = { options: {}, positionals: [] };
|
|
45
|
+
const code = await commandAgentProfile({ stdout: makeNoop(), stderr: makeNoop(), jsonMode: true }, parsed, AGENT_ID, deps);
|
|
46
|
+
assert.equal(code, 0);
|
|
47
|
+
assert.deepEqual(printed, SAMPLE_AGENT);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("commandAgentProfile human mode calls printKeyValue with agent fields", async () => {
|
|
51
|
+
let kvData = null;
|
|
52
|
+
const deps = {
|
|
53
|
+
request: async () => SAMPLE_AGENT,
|
|
54
|
+
apiHeaders: () => ({}),
|
|
55
|
+
ui, printJson: () => {},
|
|
56
|
+
printKeyValue: (_stream, v) => { kvData = v; },
|
|
57
|
+
writeLine: () => {},
|
|
58
|
+
};
|
|
59
|
+
const parsed = { options: {}, positionals: [] };
|
|
60
|
+
await commandAgentProfile({ stdout: makeNoop(), stderr: makeNoop(), jsonMode: false }, parsed, AGENT_ID, deps);
|
|
61
|
+
assert.equal(kvData.agent_id, SAMPLE_AGENT.agent_id);
|
|
62
|
+
assert.equal(kvData.name, SAMPLE_AGENT.name);
|
|
63
|
+
assert.equal(kvData.status, SAMPLE_AGENT.status);
|
|
64
|
+
assert.equal(kvData.trust_level, SAMPLE_AGENT.trust_level);
|
|
65
|
+
assert.equal(kvData.trust_streak_runs, SAMPLE_AGENT.trust_streak_runs);
|
|
66
|
+
assert.equal(kvData.improvement_stalled_warning, SAMPLE_AGENT.improvement_stalled_warning);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// ── T2: Edge cases ────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
test("commandAgentProfile URL-encodes agent_id with special characters", async () => {
|
|
72
|
+
let calledUrl = null;
|
|
73
|
+
const deps = {
|
|
74
|
+
request: async (_ctx, url) => { calledUrl = url; return SAMPLE_AGENT; },
|
|
75
|
+
apiHeaders: () => ({}),
|
|
76
|
+
ui, printJson: () => {}, printKeyValue: () => {}, writeLine: () => {},
|
|
77
|
+
};
|
|
78
|
+
const parsed = { options: {}, positionals: [] };
|
|
79
|
+
await commandAgentProfile({ stdout: makeNoop(), stderr: makeNoop(), jsonMode: false }, parsed, "agent/with spaces", deps);
|
|
80
|
+
assert.match(calledUrl, /agent%2Fwith%20spaces/);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("commandAgentProfile human mode includes all expected keys", async () => {
|
|
84
|
+
let kvData = null;
|
|
85
|
+
const deps = {
|
|
86
|
+
request: async () => SAMPLE_AGENT,
|
|
87
|
+
apiHeaders: () => ({}),
|
|
88
|
+
ui, printJson: () => {},
|
|
89
|
+
printKeyValue: (_stream, v) => { kvData = v; },
|
|
90
|
+
writeLine: () => {},
|
|
91
|
+
};
|
|
92
|
+
const parsed = { options: {}, positionals: [] };
|
|
93
|
+
await commandAgentProfile({ stdout: makeNoop(), stderr: makeNoop(), jsonMode: false }, parsed, AGENT_ID, deps);
|
|
94
|
+
assert.ok(Object.prototype.hasOwnProperty.call(kvData, "workspace_id"), "missing workspace_id");
|
|
95
|
+
assert.ok(Object.prototype.hasOwnProperty.call(kvData, "trust_level"), "missing trust_level");
|
|
96
|
+
assert.ok(Object.prototype.hasOwnProperty.call(kvData, "trust_streak_runs"), "missing trust_streak_runs");
|
|
97
|
+
assert.ok(Object.prototype.hasOwnProperty.call(kvData, "improvement_stalled_warning"), "missing improvement_stalled_warning");
|
|
98
|
+
assert.ok(Object.prototype.hasOwnProperty.call(kvData, "created_at"), "missing created_at");
|
|
99
|
+
assert.ok(Object.prototype.hasOwnProperty.call(kvData, "updated_at"), "missing updated_at");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ── T3: Error paths ───────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
test("commandAgentProfile propagates ApiError 404 when agent not found", async () => {
|
|
105
|
+
const deps = {
|
|
106
|
+
request: async () => { throw new ApiError("not found", { status: 404, code: "UZ-AGENT-001" }); },
|
|
107
|
+
apiHeaders: () => ({}),
|
|
108
|
+
ui, printJson: () => {}, printKeyValue: () => {}, writeLine: () => {},
|
|
109
|
+
};
|
|
110
|
+
const parsed = { options: {}, positionals: [] };
|
|
111
|
+
await assert.rejects(
|
|
112
|
+
() => commandAgentProfile({ stdout: makeNoop(), stderr: makeNoop(), jsonMode: false }, parsed, AGENT_ID, deps),
|
|
113
|
+
(err) => err instanceof ApiError && err.status === 404,
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("commandAgentProfile propagates ApiError 403 when forbidden", async () => {
|
|
118
|
+
const deps = {
|
|
119
|
+
request: async () => { throw new ApiError("forbidden", { status: 403, code: "HTTP_403" }); },
|
|
120
|
+
apiHeaders: () => ({}),
|
|
121
|
+
ui, printJson: () => {}, printKeyValue: () => {}, writeLine: () => {},
|
|
122
|
+
};
|
|
123
|
+
const parsed = { options: {}, positionals: [] };
|
|
124
|
+
await assert.rejects(
|
|
125
|
+
() => commandAgentProfile({ stdout: makeNoop(), stderr: makeNoop(), jsonMode: false }, parsed, AGENT_ID, deps),
|
|
126
|
+
(err) => err instanceof ApiError && err.status === 403,
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("commandAgentProfile propagates network timeout error", async () => {
|
|
131
|
+
const deps = {
|
|
132
|
+
request: async () => { throw new ApiError("request timed out after 15000ms", { status: 408, code: "TIMEOUT" }); },
|
|
133
|
+
apiHeaders: () => ({}),
|
|
134
|
+
ui, printJson: () => {}, printKeyValue: () => {}, writeLine: () => {},
|
|
135
|
+
};
|
|
136
|
+
const parsed = { options: {}, positionals: [] };
|
|
137
|
+
await assert.rejects(
|
|
138
|
+
() => commandAgentProfile({ stdout: makeNoop(), stderr: makeNoop(), jsonMode: false }, parsed, AGENT_ID, deps),
|
|
139
|
+
(err) => err.code === "TIMEOUT",
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// ── T4: Output fidelity ───────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
test("commandAgentProfile json mode output is JSON.stringify-serializable", async () => {
|
|
146
|
+
let printed = null;
|
|
147
|
+
const deps = {
|
|
148
|
+
request: async () => SAMPLE_AGENT,
|
|
149
|
+
apiHeaders: () => ({}),
|
|
150
|
+
ui, printJson: (_stream, v) => { printed = v; }, printKeyValue: () => {}, writeLine: () => {},
|
|
151
|
+
};
|
|
152
|
+
const parsed = { options: {}, positionals: [] };
|
|
153
|
+
await commandAgentProfile({ stdout: makeNoop(), stderr: makeNoop(), jsonMode: true }, parsed, AGENT_ID, deps);
|
|
154
|
+
const roundTripped = JSON.parse(JSON.stringify(printed));
|
|
155
|
+
assert.deepEqual(roundTripped, SAMPLE_AGENT);
|
|
156
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { test } from "bun:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { commandAgentProposals } from "../src/commands/agent_proposals.js";
|
|
4
|
+
import {
|
|
5
|
+
makeNoop,
|
|
6
|
+
ui,
|
|
7
|
+
AGENT_ID,
|
|
8
|
+
} from "./helpers.js";
|
|
9
|
+
|
|
10
|
+
const PROPOSAL_ID = "0195b4ba-8d3a-7f13-8abc-000000000091";
|
|
11
|
+
|
|
12
|
+
test("commandAgentProposals lists proposal rows", async () => {
|
|
13
|
+
const realNow = Date.now;
|
|
14
|
+
Date.now = () => 1700000000000;
|
|
15
|
+
let calledUrl = null;
|
|
16
|
+
let columns = null;
|
|
17
|
+
let rows = null;
|
|
18
|
+
const deps = {
|
|
19
|
+
request: async (_ctx, url) => {
|
|
20
|
+
calledUrl = url;
|
|
21
|
+
return {
|
|
22
|
+
data: [
|
|
23
|
+
{
|
|
24
|
+
proposal_id: PROPOSAL_ID,
|
|
25
|
+
status: "VETO_WINDOW",
|
|
26
|
+
trigger_reason: "DECLINING_SCORE",
|
|
27
|
+
auto_apply_at: 1700003600000,
|
|
28
|
+
config_version_id: "0195b4ba-8d3a-7f13-8abc-000000000092",
|
|
29
|
+
created_at: 1700000000000,
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
apiHeaders: () => ({}),
|
|
35
|
+
printJson: () => {},
|
|
36
|
+
printTable: (_stream, cols, items) => {
|
|
37
|
+
columns = cols;
|
|
38
|
+
rows = items;
|
|
39
|
+
},
|
|
40
|
+
ui,
|
|
41
|
+
writeLine: () => {},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const parsed = { options: {}, positionals: [AGENT_ID] };
|
|
45
|
+
try {
|
|
46
|
+
const code = await commandAgentProposals({ stdout: makeNoop(), stderr: makeNoop(), jsonMode: false }, parsed, AGENT_ID, deps);
|
|
47
|
+
assert.equal(code, 0);
|
|
48
|
+
assert.match(calledUrl, /\/proposals$/);
|
|
49
|
+
assert.equal(columns[0].key, "proposal_id");
|
|
50
|
+
assert.equal(columns[1].key, "status");
|
|
51
|
+
assert.equal(rows[0].proposal_id, PROPOSAL_ID);
|
|
52
|
+
assert.match(rows[0].action, /Auto-applies in 1h 0m/);
|
|
53
|
+
assert.match(rows[0].action, /agent proposals veto/);
|
|
54
|
+
} finally {
|
|
55
|
+
Date.now = realNow;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("commandAgentProposals approve posts to the approve endpoint", async () => {
|
|
60
|
+
let called = null;
|
|
61
|
+
const deps = {
|
|
62
|
+
request: async (_ctx, url, init) => {
|
|
63
|
+
called = { url, init };
|
|
64
|
+
return { proposal_id: PROPOSAL_ID, status: "APPLIED" };
|
|
65
|
+
},
|
|
66
|
+
apiHeaders: () => ({ authorization: "Bearer t" }),
|
|
67
|
+
printJson: () => {},
|
|
68
|
+
printTable: () => {},
|
|
69
|
+
ui,
|
|
70
|
+
writeLine: () => {},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const parsed = { options: {}, positionals: [AGENT_ID, "approve", PROPOSAL_ID] };
|
|
74
|
+
const code = await commandAgentProposals({ stdout: makeNoop(), stderr: makeNoop(), jsonMode: false }, parsed, AGENT_ID, deps);
|
|
75
|
+
assert.equal(code, 0);
|
|
76
|
+
assert.match(called.url, /:approve$/);
|
|
77
|
+
assert.equal(called.init.method, "POST");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("commandAgentProposals reject sends optional reason body", async () => {
|
|
81
|
+
let called = null;
|
|
82
|
+
const deps = {
|
|
83
|
+
request: async (_ctx, url, init) => {
|
|
84
|
+
called = { url, init };
|
|
85
|
+
return { proposal_id: PROPOSAL_ID, rejection_reason: "EXPIRED", status: "REJECTED" };
|
|
86
|
+
},
|
|
87
|
+
apiHeaders: () => ({ authorization: "Bearer t" }),
|
|
88
|
+
printJson: () => {},
|
|
89
|
+
printTable: () => {},
|
|
90
|
+
ui,
|
|
91
|
+
writeLine: () => {},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const parsed = { options: { reason: "needs operator follow-up" }, positionals: [AGENT_ID, "reject", PROPOSAL_ID] };
|
|
95
|
+
const code = await commandAgentProposals({ stdout: makeNoop(), stderr: makeNoop(), jsonMode: false }, parsed, AGENT_ID, deps);
|
|
96
|
+
assert.equal(code, 0);
|
|
97
|
+
assert.match(called.url, /:reject$/);
|
|
98
|
+
assert.equal(JSON.parse(called.init.body).reason, "needs operator follow-up");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("commandAgentProposals veto sends optional reason body", async () => {
|
|
102
|
+
let called = null;
|
|
103
|
+
const deps = {
|
|
104
|
+
request: async (_ctx, url, init) => {
|
|
105
|
+
called = { url, init };
|
|
106
|
+
return { proposal_id: PROPOSAL_ID, rejection_reason: "operator pause", status: "VETOED" };
|
|
107
|
+
},
|
|
108
|
+
apiHeaders: () => ({ authorization: "Bearer t" }),
|
|
109
|
+
printJson: () => {},
|
|
110
|
+
printTable: () => {},
|
|
111
|
+
ui,
|
|
112
|
+
writeLine: () => {},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const parsed = { options: { reason: "operator pause" }, positionals: [AGENT_ID, "veto", PROPOSAL_ID] };
|
|
116
|
+
const code = await commandAgentProposals({ stdout: makeNoop(), stderr: makeNoop(), jsonMode: false }, parsed, AGENT_ID, deps);
|
|
117
|
+
assert.equal(code, 0);
|
|
118
|
+
assert.match(called.url, /:veto$/);
|
|
119
|
+
assert.equal(JSON.parse(called.init.body).reason, "operator pause");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("commandAgentProposals json mode preserves proposal response shape", async () => {
|
|
123
|
+
let jsonValue = null;
|
|
124
|
+
const deps = {
|
|
125
|
+
request: async () => ({
|
|
126
|
+
data: [{
|
|
127
|
+
proposal_id: PROPOSAL_ID,
|
|
128
|
+
status: "VETO_WINDOW",
|
|
129
|
+
approval_mode: "AUTO",
|
|
130
|
+
auto_apply_at: 1700003600000,
|
|
131
|
+
trigger_reason: "DECLINING_SCORE",
|
|
132
|
+
config_version_id: "0195b4ba-8d3a-7f13-8abc-000000000092",
|
|
133
|
+
created_at: 1700000000000,
|
|
134
|
+
updated_at: 1700000000001,
|
|
135
|
+
}],
|
|
136
|
+
}),
|
|
137
|
+
apiHeaders: () => ({}),
|
|
138
|
+
printJson: (_stream, value) => { jsonValue = value; },
|
|
139
|
+
printTable: () => {},
|
|
140
|
+
ui,
|
|
141
|
+
writeLine: () => {},
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const parsed = { options: {}, positionals: [AGENT_ID] };
|
|
145
|
+
const code = await commandAgentProposals({ stdout: makeNoop(), stderr: makeNoop(), jsonMode: true }, parsed, AGENT_ID, deps);
|
|
146
|
+
assert.equal(code, 0);
|
|
147
|
+
assert.equal(jsonValue.data[0].status, "VETO_WINDOW");
|
|
148
|
+
assert.equal(jsonValue.data[0].approval_mode, "AUTO");
|
|
149
|
+
assert.equal(jsonValue.data[0].auto_apply_at, 1700003600000);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("commandAgentProposals rejects missing proposal id for decision commands", async () => {
|
|
153
|
+
const stderr = [];
|
|
154
|
+
const deps = {
|
|
155
|
+
request: async () => ({ data: [] }),
|
|
156
|
+
apiHeaders: () => ({}),
|
|
157
|
+
printJson: () => {},
|
|
158
|
+
printTable: () => {},
|
|
159
|
+
ui,
|
|
160
|
+
writeLine: (_stream, line) => { stderr.push(line); },
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const parsed = { options: {}, positionals: [AGENT_ID, "approve"] };
|
|
164
|
+
const code = await commandAgentProposals({ stdout: makeNoop(), stderr: makeNoop(), jsonMode: false }, parsed, AGENT_ID, deps);
|
|
165
|
+
assert.equal(code, 2);
|
|
166
|
+
assert.match(stderr.join("\n"), /requires <proposal-id>/);
|
|
167
|
+
});
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { test } from "bun:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { commandAgentScores } from "../src/commands/agent_scores.js";
|
|
4
|
+
import {
|
|
5
|
+
makeNoop, makeBufferStream,
|
|
6
|
+
ui, ApiError,
|
|
7
|
+
AGENT_ID, SCORE_ID_1, SCORE_ID_2, RUN_ID_1, RUN_ID_2,
|
|
8
|
+
} from "./helpers.js";
|
|
9
|
+
|
|
10
|
+
const SAMPLE_SCORES = [
|
|
11
|
+
{ score_id: SCORE_ID_1, run_id: RUN_ID_1, score: 87, scored_at: 1700000002000 },
|
|
12
|
+
{ score_id: SCORE_ID_2, run_id: RUN_ID_2, score: 72, scored_at: 1700000001000 },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
// ── T1: Happy path ────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
test("commandAgentScores builds correct URL with limit", async () => {
|
|
18
|
+
let calledUrl = null;
|
|
19
|
+
const deps = {
|
|
20
|
+
request: async (_ctx, url) => { calledUrl = url; return { data: [], has_more: false, next_cursor: null }; },
|
|
21
|
+
apiHeaders: () => ({}),
|
|
22
|
+
ui, printJson: () => {}, printTable: () => {}, writeLine: () => {},
|
|
23
|
+
};
|
|
24
|
+
const parsed = { options: { limit: 10 }, positionals: [] };
|
|
25
|
+
const code = await commandAgentScores({ stdout: makeNoop(), stderr: makeNoop(), jsonMode: false }, parsed, AGENT_ID, deps);
|
|
26
|
+
assert.equal(code, 0);
|
|
27
|
+
assert.match(calledUrl, /limit=10/);
|
|
28
|
+
assert.match(calledUrl, new RegExp(`/v1/agents/${AGENT_ID}/scores`));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("commandAgentScores appends starting_after to URL when provided", async () => {
|
|
32
|
+
let calledUrl = null;
|
|
33
|
+
const deps = {
|
|
34
|
+
request: async (_ctx, url) => { calledUrl = url; return { data: [], has_more: false, next_cursor: null }; },
|
|
35
|
+
apiHeaders: () => ({}),
|
|
36
|
+
ui, printJson: () => {}, printTable: () => {}, writeLine: () => {},
|
|
37
|
+
};
|
|
38
|
+
const parsed = { options: { "starting-after": SCORE_ID_1 }, positionals: [] };
|
|
39
|
+
await commandAgentScores({ stdout: makeNoop(), stderr: makeNoop(), jsonMode: false }, parsed, AGENT_ID, deps);
|
|
40
|
+
assert.match(calledUrl, new RegExp(`starting_after=${encodeURIComponent(SCORE_ID_1)}`));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("commandAgentScores json mode outputs raw response", async () => {
|
|
44
|
+
let printed = null;
|
|
45
|
+
const res = { data: SAMPLE_SCORES, has_more: false, next_cursor: null };
|
|
46
|
+
const deps = {
|
|
47
|
+
request: async () => res,
|
|
48
|
+
apiHeaders: () => ({}),
|
|
49
|
+
ui, printJson: (_stream, v) => { printed = v; }, printTable: () => {}, writeLine: () => {},
|
|
50
|
+
};
|
|
51
|
+
const parsed = { options: {}, positionals: [] };
|
|
52
|
+
const code = await commandAgentScores({ stdout: makeNoop(), stderr: makeNoop(), jsonMode: true }, parsed, AGENT_ID, deps);
|
|
53
|
+
assert.equal(code, 0);
|
|
54
|
+
assert.deepEqual(printed, res);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("commandAgentScores prints table in human mode", async () => {
|
|
58
|
+
let tableRows = null;
|
|
59
|
+
const deps = {
|
|
60
|
+
request: async () => ({ data: SAMPLE_SCORES, has_more: false, next_cursor: null }),
|
|
61
|
+
apiHeaders: () => ({}),
|
|
62
|
+
ui, printJson: () => {},
|
|
63
|
+
printTable: (_stream, _cols, rows) => { tableRows = rows; },
|
|
64
|
+
writeLine: () => {},
|
|
65
|
+
};
|
|
66
|
+
const parsed = { options: {}, positionals: [] };
|
|
67
|
+
await commandAgentScores({ stdout: makeNoop(), stderr: makeNoop(), jsonMode: false }, parsed, AGENT_ID, deps);
|
|
68
|
+
assert.equal(tableRows.length, 2);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("commandAgentScores shows next_cursor hint when has_more", async () => {
|
|
72
|
+
let output = "";
|
|
73
|
+
const deps = {
|
|
74
|
+
request: async () => ({ data: SAMPLE_SCORES, has_more: true, next_cursor: SCORE_ID_1 }),
|
|
75
|
+
apiHeaders: () => ({}),
|
|
76
|
+
ui, printJson: () => {},
|
|
77
|
+
printTable: () => {},
|
|
78
|
+
writeLine: (_stream, line = "") => { output += line; },
|
|
79
|
+
};
|
|
80
|
+
const parsed = { options: {}, positionals: [] };
|
|
81
|
+
await commandAgentScores({ stdout: makeNoop(), stderr: makeNoop(), jsonMode: false }, parsed, AGENT_ID, deps);
|
|
82
|
+
assert.match(output, new RegExp(SCORE_ID_1));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("commandAgentScores shows no scores message when data is empty", async () => {
|
|
86
|
+
let output = "";
|
|
87
|
+
const deps = {
|
|
88
|
+
request: async () => ({ data: [], has_more: false, next_cursor: null }),
|
|
89
|
+
apiHeaders: () => ({}),
|
|
90
|
+
ui, printJson: () => {},
|
|
91
|
+
printTable: () => {},
|
|
92
|
+
writeLine: (_stream, line = "") => { output += line; },
|
|
93
|
+
};
|
|
94
|
+
const parsed = { options: {}, positionals: [] };
|
|
95
|
+
await commandAgentScores({ stdout: makeNoop(), stderr: makeNoop(), jsonMode: false }, parsed, AGENT_ID, deps);
|
|
96
|
+
assert.match(output, /no scores/);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ── T2: Edge cases ────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
test("commandAgentScores uses default limit 20 when none specified", async () => {
|
|
102
|
+
let calledUrl = null;
|
|
103
|
+
const deps = {
|
|
104
|
+
request: async (_ctx, url) => { calledUrl = url; return { data: [], has_more: false, next_cursor: null }; },
|
|
105
|
+
apiHeaders: () => ({}),
|
|
106
|
+
ui, printJson: () => {}, printTable: () => {}, writeLine: () => {},
|
|
107
|
+
};
|
|
108
|
+
const parsed = { options: {}, positionals: [] };
|
|
109
|
+
await commandAgentScores({ stdout: makeNoop(), stderr: makeNoop(), jsonMode: false }, parsed, AGENT_ID, deps);
|
|
110
|
+
assert.match(calledUrl, /limit=20/);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("commandAgentScores with limit=0 sends limit=0 in URL", async () => {
|
|
114
|
+
let calledUrl = null;
|
|
115
|
+
const deps = {
|
|
116
|
+
request: async (_ctx, url) => { calledUrl = url; return { data: [], has_more: false, next_cursor: null }; },
|
|
117
|
+
apiHeaders: () => ({}),
|
|
118
|
+
ui, printJson: () => {}, printTable: () => {}, writeLine: () => {},
|
|
119
|
+
};
|
|
120
|
+
const parsed = { options: { limit: 0 }, positionals: [] };
|
|
121
|
+
await commandAgentScores({ stdout: makeNoop(), stderr: makeNoop(), jsonMode: false }, parsed, AGENT_ID, deps);
|
|
122
|
+
// limit=0 is falsy so falls back to default 20 — verify the documented behavior
|
|
123
|
+
assert.match(calledUrl, /limit=/);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("commandAgentScores omits starting_after when not provided", async () => {
|
|
127
|
+
let calledUrl = null;
|
|
128
|
+
const deps = {
|
|
129
|
+
request: async (_ctx, url) => { calledUrl = url; return { data: [], has_more: false, next_cursor: null }; },
|
|
130
|
+
apiHeaders: () => ({}),
|
|
131
|
+
ui, printJson: () => {}, printTable: () => {}, writeLine: () => {},
|
|
132
|
+
};
|
|
133
|
+
const parsed = { options: {}, positionals: [] };
|
|
134
|
+
await commandAgentScores({ stdout: makeNoop(), stderr: makeNoop(), jsonMode: false }, parsed, AGENT_ID, deps);
|
|
135
|
+
assert.doesNotMatch(calledUrl, /starting_after/);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("commandAgentScores suppresses cursor hint when has_more=true but next_cursor=null", async () => {
|
|
139
|
+
let output = "";
|
|
140
|
+
const deps = {
|
|
141
|
+
request: async () => ({ data: SAMPLE_SCORES, has_more: true, next_cursor: null }),
|
|
142
|
+
apiHeaders: () => ({}),
|
|
143
|
+
ui, printJson: () => {},
|
|
144
|
+
printTable: () => {},
|
|
145
|
+
writeLine: (_stream, line = "") => { output += line; },
|
|
146
|
+
};
|
|
147
|
+
const parsed = { options: {}, positionals: [] };
|
|
148
|
+
await commandAgentScores({ stdout: makeNoop(), stderr: makeNoop(), jsonMode: false }, parsed, AGENT_ID, deps);
|
|
149
|
+
assert.doesNotMatch(output, /--starting-after/);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("commandAgentScores URL-encodes agent_id with special characters", async () => {
|
|
153
|
+
let calledUrl = null;
|
|
154
|
+
const deps = {
|
|
155
|
+
request: async (_ctx, url) => { calledUrl = url; return { data: [], has_more: false, next_cursor: null }; },
|
|
156
|
+
apiHeaders: () => ({}),
|
|
157
|
+
ui, printJson: () => {}, printTable: () => {}, writeLine: () => {},
|
|
158
|
+
};
|
|
159
|
+
const parsed = { options: {}, positionals: [] };
|
|
160
|
+
await commandAgentScores({ stdout: makeNoop(), stderr: makeNoop(), jsonMode: false }, parsed, "agent/with spaces", deps);
|
|
161
|
+
assert.match(calledUrl, /agent%2Fwith%20spaces/);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ── T3: Error paths ───────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
test("commandAgentScores propagates ApiError 404 from request", async () => {
|
|
167
|
+
const deps = {
|
|
168
|
+
request: async () => { throw new ApiError("not found", { status: 404, code: "UZ-AGENT-001" }); },
|
|
169
|
+
apiHeaders: () => ({}),
|
|
170
|
+
ui, printJson: () => {}, printTable: () => {}, writeLine: () => {},
|
|
171
|
+
};
|
|
172
|
+
const parsed = { options: {}, positionals: [] };
|
|
173
|
+
await assert.rejects(
|
|
174
|
+
() => commandAgentScores({ stdout: makeNoop(), stderr: makeNoop(), jsonMode: false }, parsed, AGENT_ID, deps),
|
|
175
|
+
(err) => err instanceof ApiError && err.status === 404,
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("commandAgentScores propagates ApiError 500 from request", async () => {
|
|
180
|
+
const deps = {
|
|
181
|
+
request: async () => { throw new ApiError("internal error", { status: 500, code: "HTTP_500" }); },
|
|
182
|
+
apiHeaders: () => ({}),
|
|
183
|
+
ui, printJson: () => {}, printTable: () => {}, writeLine: () => {},
|
|
184
|
+
};
|
|
185
|
+
const parsed = { options: {}, positionals: [] };
|
|
186
|
+
await assert.rejects(
|
|
187
|
+
() => commandAgentScores({ stdout: makeNoop(), stderr: makeNoop(), jsonMode: false }, parsed, AGENT_ID, deps),
|
|
188
|
+
(err) => err instanceof ApiError && err.status === 500,
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("commandAgentScores propagates network timeout error", async () => {
|
|
193
|
+
const deps = {
|
|
194
|
+
request: async () => { throw new ApiError("request timed out after 15000ms", { status: 408, code: "TIMEOUT" }); },
|
|
195
|
+
apiHeaders: () => ({}),
|
|
196
|
+
ui, printJson: () => {}, printTable: () => {}, writeLine: () => {},
|
|
197
|
+
};
|
|
198
|
+
const parsed = { options: {}, positionals: [] };
|
|
199
|
+
await assert.rejects(
|
|
200
|
+
() => commandAgentScores({ stdout: makeNoop(), stderr: makeNoop(), jsonMode: false }, parsed, AGENT_ID, deps),
|
|
201
|
+
(err) => err.code === "TIMEOUT",
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// ── T4: Output fidelity ───────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
test("commandAgentScores json mode output is JSON.stringify-serializable", async () => {
|
|
208
|
+
const res = { data: SAMPLE_SCORES, has_more: false, next_cursor: null };
|
|
209
|
+
let printed = null;
|
|
210
|
+
const deps = {
|
|
211
|
+
request: async () => res,
|
|
212
|
+
apiHeaders: () => ({}),
|
|
213
|
+
ui, printJson: (_stream, v) => { printed = v; }, printTable: () => {}, writeLine: () => {},
|
|
214
|
+
};
|
|
215
|
+
const parsed = { options: {}, positionals: [] };
|
|
216
|
+
await commandAgentScores({ stdout: makeNoop(), stderr: makeNoop(), jsonMode: true }, parsed, AGENT_ID, deps);
|
|
217
|
+
const serialized = JSON.stringify(printed);
|
|
218
|
+
const roundTripped = JSON.parse(serialized);
|
|
219
|
+
assert.deepEqual(roundTripped, res);
|
|
220
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { test } from "bun:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { cliAnalyticsInternals } from "../src/lib/analytics.js";
|
|
4
|
+
|
|
5
|
+
test("analytics resolveConfig enables bundled default key when env key is absent", () => {
|
|
6
|
+
const cfg = cliAnalyticsInternals.resolveConfig({});
|
|
7
|
+
assert.equal(cfg.enabled, true);
|
|
8
|
+
assert.equal(cfg.host, "https://us.i.posthog.com");
|
|
9
|
+
assert.equal(cfg.key, cliAnalyticsInternals.DEFAULT_POSTHOG_KEY);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("analytics resolveConfig honors explicit env key override", () => {
|
|
13
|
+
const cfg = cliAnalyticsInternals.resolveConfig({
|
|
14
|
+
ZOMBIE_POSTHOG_KEY: "phc_test",
|
|
15
|
+
});
|
|
16
|
+
assert.equal(cfg.enabled, true);
|
|
17
|
+
assert.equal(cfg.key, "phc_test");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("analytics resolveConfig allows opt-out even with bundled key", () => {
|
|
21
|
+
const cfg = cliAnalyticsInternals.resolveConfig({
|
|
22
|
+
ZOMBIE_POSTHOG_ENABLED: "false",
|
|
23
|
+
});
|
|
24
|
+
assert.equal(cfg.enabled, false);
|
|
25
|
+
assert.equal(cfg.key, cliAnalyticsInternals.DEFAULT_POSTHOG_KEY);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("analytics sanitizeProperties stringifies values and drops nullish", () => {
|
|
29
|
+
const properties = cliAnalyticsInternals.sanitizeProperties({
|
|
30
|
+
command: "run",
|
|
31
|
+
exit_code: 0,
|
|
32
|
+
empty: null,
|
|
33
|
+
ignored: undefined,
|
|
34
|
+
ok: false,
|
|
35
|
+
});
|
|
36
|
+
assert.deepEqual(properties, {
|
|
37
|
+
command: "run",
|
|
38
|
+
exit_code: "0",
|
|
39
|
+
ok: "false",
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { test } from "bun:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { parseFlags, parseGlobalArgs, normalizeApiUrl } from "../src/program/args.js";
|
|
4
|
+
|
|
5
|
+
test("parseFlags parses mixed forms: inline values, separated values, booleans, and positionals", () => {
|
|
6
|
+
const parsed = parseFlags([
|
|
7
|
+
"--workspace-id=ws_123",
|
|
8
|
+
"--mode",
|
|
9
|
+
"api",
|
|
10
|
+
"run",
|
|
11
|
+
"status",
|
|
12
|
+
"run_123",
|
|
13
|
+
"--dry-run",
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
assert.equal(parsed.options["workspace-id"], "ws_123");
|
|
17
|
+
assert.equal(parsed.options.mode, "api");
|
|
18
|
+
assert.equal(parsed.options["dry-run"], true);
|
|
19
|
+
assert.deepEqual(parsed.positionals, ["run", "status", "run_123"]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("parseFlags treats subsequent option token as a new flag", () => {
|
|
23
|
+
const parsed = parseFlags(["--json", "--no-open", "--help"]);
|
|
24
|
+
assert.equal(parsed.options.json, true);
|
|
25
|
+
assert.equal(parsed.options["no-open"], true);
|
|
26
|
+
assert.equal(parsed.options.help, true);
|
|
27
|
+
assert.deepEqual(parsed.positionals, []);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("parseFlags preserves equals-containing values", () => {
|
|
31
|
+
const parsed = parseFlags(["--filter=a=b=c", "--empty=", "--name", "alpha=beta"]);
|
|
32
|
+
assert.equal(parsed.options.filter, "a=b=c");
|
|
33
|
+
assert.equal(parsed.options.empty, "");
|
|
34
|
+
assert.equal(parsed.options.name, "alpha=beta");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("parseFlags keeps unicode and whitespace positionals unchanged", () => {
|
|
38
|
+
const parsed = parseFlags(["--scope", "sandbox", "नमस्ते", "emoji_😀"]);
|
|
39
|
+
assert.equal(parsed.options.scope, "sandbox");
|
|
40
|
+
assert.deepEqual(parsed.positionals, ["नमस्ते", "emoji_😀"]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("normalizeApiUrl strips trailing slashes but keeps core URL", () => {
|
|
44
|
+
assert.equal(normalizeApiUrl("https://api.example.com///"), "https://api.example.com");
|
|
45
|
+
assert.equal(normalizeApiUrl("http://localhost:3000"), "http://localhost:3000");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("parseGlobalArgs prioritizes --api over env and forwards remaining args", () => {
|
|
49
|
+
const env = { ZOMBIE_API_URL: "https://env.example" };
|
|
50
|
+
const out = parseGlobalArgs(["--api", "https://flag.example/", "doctor"], env);
|
|
51
|
+
assert.equal(out.global.apiUrl, "https://flag.example");
|
|
52
|
+
assert.deepEqual(out.rest, ["doctor"]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("parseGlobalArgs falls back through env chain and defaults", () => {
|
|
56
|
+
const outApiUrl = parseGlobalArgs(["doctor"], { API_URL: "https://api-url.example" });
|
|
57
|
+
assert.equal(outApiUrl.global.apiUrl, "https://api-url.example");
|
|
58
|
+
|
|
59
|
+
const outDefault = parseGlobalArgs(["doctor"], {});
|
|
60
|
+
assert.equal(outDefault.global.apiUrl, "http://localhost:3000");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("parseGlobalArgs sets global boolean options and leaves command argv intact", () => {
|
|
64
|
+
const out = parseGlobalArgs(["--json", "--no-input", "--no-open", "run", "status", "run_1"], {});
|
|
65
|
+
assert.equal(out.global.json, true);
|
|
66
|
+
assert.equal(out.global.noInput, true);
|
|
67
|
+
assert.equal(out.global.noOpen, true);
|
|
68
|
+
assert.deepEqual(out.rest, ["run", "status", "run_1"]);
|
|
69
|
+
});
|