@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.
Files changed (100) hide show
  1. package/README.md +76 -0
  2. package/bin/zombiectl.js +11 -0
  3. package/bun.lock +29 -0
  4. package/package.json +28 -0
  5. package/scripts/run-tests.mjs +38 -0
  6. package/src/cli.js +275 -0
  7. package/src/commands/admin.js +39 -0
  8. package/src/commands/agent.js +98 -0
  9. package/src/commands/agent_harness.js +43 -0
  10. package/src/commands/agent_improvement_report.js +42 -0
  11. package/src/commands/agent_profile.js +39 -0
  12. package/src/commands/agent_proposals.js +158 -0
  13. package/src/commands/agent_scores.js +44 -0
  14. package/src/commands/core-ops.js +108 -0
  15. package/src/commands/core.js +537 -0
  16. package/src/commands/harness.js +35 -0
  17. package/src/commands/harness_activate.js +53 -0
  18. package/src/commands/harness_active.js +32 -0
  19. package/src/commands/harness_compile.js +40 -0
  20. package/src/commands/harness_source.js +72 -0
  21. package/src/commands/run_preview.js +212 -0
  22. package/src/commands/run_preview_walk.js +1 -0
  23. package/src/commands/runs.js +35 -0
  24. package/src/commands/spec_init.js +287 -0
  25. package/src/commands/workspace_billing.js +26 -0
  26. package/src/constants/error-codes.js +1 -0
  27. package/src/lib/agent-loop.js +106 -0
  28. package/src/lib/analytics.js +114 -0
  29. package/src/lib/api-paths.js +2 -0
  30. package/src/lib/browser.js +96 -0
  31. package/src/lib/http.js +149 -0
  32. package/src/lib/sse-parser.js +50 -0
  33. package/src/lib/state.js +67 -0
  34. package/src/lib/tool-executors.js +110 -0
  35. package/src/lib/walk-dir.js +41 -0
  36. package/src/program/args.js +95 -0
  37. package/src/program/auth-guard.js +12 -0
  38. package/src/program/auth-token.js +44 -0
  39. package/src/program/banner.js +46 -0
  40. package/src/program/command-registry.js +17 -0
  41. package/src/program/http-client.js +38 -0
  42. package/src/program/io.js +83 -0
  43. package/src/program/routes.js +20 -0
  44. package/src/program/suggest.js +76 -0
  45. package/src/program/validate.js +24 -0
  46. package/src/ui-progress.js +59 -0
  47. package/src/ui-theme.js +62 -0
  48. package/test/admin_config.unit.test.js +25 -0
  49. package/test/agent-loop.unit.test.js +497 -0
  50. package/test/agent_harness.unit.test.js +52 -0
  51. package/test/agent_improvement_report.unit.test.js +74 -0
  52. package/test/agent_profile.unit.test.js +156 -0
  53. package/test/agent_proposals.unit.test.js +167 -0
  54. package/test/agent_scores.unit.test.js +220 -0
  55. package/test/analytics.unit.test.js +41 -0
  56. package/test/args.unit.test.js +69 -0
  57. package/test/auth-guard.test.js +33 -0
  58. package/test/auth-token.unit.test.js +112 -0
  59. package/test/banner.unit.test.js +442 -0
  60. package/test/browser.unit.test.js +16 -0
  61. package/test/cli-analytics.unit.test.js +296 -0
  62. package/test/did-you-mean.integration.test.js +76 -0
  63. package/test/doctor-json.test.js +81 -0
  64. package/test/error-codes.unit.test.js +7 -0
  65. package/test/harness-command.unit.test.js +180 -0
  66. package/test/harness-compile.test.js +81 -0
  67. package/test/harness-lifecycle.integration.test.js +339 -0
  68. package/test/harness-source-put.test.js +72 -0
  69. package/test/harness_activate.unit.test.js +48 -0
  70. package/test/harness_active.unit.test.js +53 -0
  71. package/test/harness_compile.unit.test.js +54 -0
  72. package/test/harness_source.unit.test.js +59 -0
  73. package/test/help.test.js +276 -0
  74. package/test/helpers-fs.js +32 -0
  75. package/test/helpers.js +31 -0
  76. package/test/io.unit.test.js +57 -0
  77. package/test/login.unit.test.js +115 -0
  78. package/test/logout.unit.test.js +65 -0
  79. package/test/parse.test.js +16 -0
  80. package/test/run-preview.edge.test.js +422 -0
  81. package/test/run-preview.integration.test.js +135 -0
  82. package/test/run-preview.security.test.js +246 -0
  83. package/test/run-preview.unit.test.js +131 -0
  84. package/test/run.unit.test.js +149 -0
  85. package/test/runs-cancel.unit.test.js +288 -0
  86. package/test/runs-list.unit.test.js +105 -0
  87. package/test/skill-secret.unit.test.js +94 -0
  88. package/test/spec-init.edge.test.js +232 -0
  89. package/test/spec-init.integration.test.js +128 -0
  90. package/test/spec-init.security.test.js +285 -0
  91. package/test/spec-init.unit.test.js +160 -0
  92. package/test/specs-sync.unit.test.js +164 -0
  93. package/test/sse-parser.unit.test.js +54 -0
  94. package/test/state.unit.test.js +34 -0
  95. package/test/streamfetch.unit.test.js +211 -0
  96. package/test/suggest.test.js +75 -0
  97. package/test/tool-executors.unit.test.js +165 -0
  98. package/test/validate.test.js +81 -0
  99. package/test/workspace-add.test.js +106 -0
  100. package/test/workspace.unit.test.js +230 -0
@@ -0,0 +1,59 @@
1
+ import { test } from "bun:test";
2
+ import assert from "node:assert/strict";
3
+ import { commandHarnessSourcePut } from "../src/commands/harness_source.js";
4
+ import { makeNoop, makeBufferStream, ui } from "./helpers.js";
5
+
6
+ const bufferStream = makeBufferStream;
7
+
8
+ test("commandHarnessSourcePut builds source_markdown payload", async () => {
9
+ const out = bufferStream();
10
+ const err = bufferStream();
11
+ let captured = null;
12
+
13
+ const deps = {
14
+ request: async (_ctx, reqPath, options) => { captured = { reqPath, options }; return { config_version_id: "pver_1" }; },
15
+ apiHeaders: () => ({}),
16
+ ui,
17
+ printJson: () => {},
18
+ writeLine: (stream, line = "") => stream.write(`${line}\n`),
19
+ readFile: async () => "# Harness\n\n```json\n{}\n```",
20
+ resolvePath: (p) => p,
21
+ };
22
+
23
+ const parsed = { options: { file: "profile.md", "agent-id": "agent_1" }, positionals: [] };
24
+ const code = await commandHarnessSourcePut({ stdout: out.stream, stderr: err.stream, jsonMode: false }, parsed, "ws_123", deps);
25
+
26
+ assert.equal(code, 0);
27
+ assert.equal(err.read(), "");
28
+ assert.equal(captured.reqPath, "/v1/workspaces/ws_123/harness/source");
29
+ const body = JSON.parse(captured.options.body);
30
+ assert.equal(body.agent_id, "agent_1");
31
+ assert.equal(body.name, "profile");
32
+ assert.match(body.source_markdown, /# Harness/);
33
+ });
34
+
35
+ test("commandHarnessSourcePut returns 2 when --file is missing", async () => {
36
+ const err = bufferStream();
37
+ const deps = { ui, writeLine: (stream, line = "") => stream.write(`${line}\n`) };
38
+ const parsed = { options: {}, positionals: [] };
39
+ const code = await commandHarnessSourcePut({ stdout: makeNoop(), stderr: err.stream, jsonMode: false }, parsed, "ws_123", deps);
40
+ assert.equal(code, 2);
41
+ assert.match(err.read(), /--file/);
42
+ });
43
+
44
+ test("commandHarnessSourcePut json mode outputs raw response", async () => {
45
+ let printed = null;
46
+ const deps = {
47
+ request: async () => ({ config_version_id: "pver_9" }),
48
+ apiHeaders: () => ({}),
49
+ ui,
50
+ printJson: (_stream, v) => { printed = v; },
51
+ writeLine: () => {},
52
+ readFile: async () => "# H",
53
+ resolvePath: (p) => p,
54
+ };
55
+ const parsed = { options: { file: "f.md" }, positionals: [] };
56
+ const code = await commandHarnessSourcePut({ stdout: makeNoop(), stderr: makeNoop(), jsonMode: true }, parsed, "ws_123", deps);
57
+ assert.equal(code, 0);
58
+ assert.equal(printed.config_version_id, "pver_9");
59
+ });
@@ -0,0 +1,276 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { Writable } from "node:stream";
6
+ import { runCli } from "../src/cli.js";
7
+
8
+ function bufferStream() {
9
+ let data = "";
10
+ return {
11
+ stream: new Writable({
12
+ write(chunk, _enc, cb) {
13
+ data += String(chunk);
14
+ cb();
15
+ },
16
+ }),
17
+ read: () => data,
18
+ };
19
+ }
20
+
21
+ async function withIsolatedStateDir(run) {
22
+ const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "zombiectl-help-"));
23
+ const previousStateDir = process.env.ZOMBIE_STATE_DIR;
24
+ process.env.ZOMBIE_STATE_DIR = stateDir;
25
+ try {
26
+ return await run(stateDir);
27
+ } finally {
28
+ if (previousStateDir === undefined) delete process.env.ZOMBIE_STATE_DIR;
29
+ else process.env.ZOMBIE_STATE_DIR = previousStateDir;
30
+ await fs.rm(stateDir, { recursive: true, force: true });
31
+ }
32
+ }
33
+
34
+ describe("help output", () => {
35
+ test("--help output contains all user commands", async () => {
36
+ await withIsolatedStateDir(async () => {
37
+ const out = bufferStream();
38
+ const err = bufferStream();
39
+ const code = await runCli(["--help"], {
40
+ stdout: out.stream,
41
+ stderr: err.stream,
42
+ env: { NO_COLOR: "1" },
43
+ });
44
+ expect(code).toBe(0);
45
+ const output = out.read();
46
+ expect(output).toContain("login");
47
+ expect(output).toContain("logout");
48
+ expect(output).toContain("workspace add");
49
+ expect(output).toContain("workspace list");
50
+ expect(output).toContain("workspace remove");
51
+ expect(output).toContain("specs sync");
52
+ expect(output).toContain("run status");
53
+ expect(output).toContain("runs list");
54
+ expect(output).toContain("doctor");
55
+ expect(output).not.toContain("workspace upgrade-scale");
56
+ });
57
+ });
58
+
59
+ test("--help with ZOMBIE_OPERATOR=1 contains operator commands", async () => {
60
+ const out = bufferStream();
61
+ const err = bufferStream();
62
+ const code = await runCli(["--help"], {
63
+ stdout: out.stream,
64
+ stderr: err.stream,
65
+ env: { ...process.env, ZOMBIE_OPERATOR: "1", NO_COLOR: "1" },
66
+ });
67
+ expect(code).toBe(0);
68
+ const output = out.read();
69
+ expect(output).toContain("OPERATOR COMMANDS");
70
+ expect(output).toContain("harness source put");
71
+ expect(output).toContain("harness compile");
72
+ expect(output).toContain("harness activate");
73
+ expect(output).toContain("workspace upgrade-scale");
74
+ expect(output).toContain("skill-secret put");
75
+ expect(output).toContain("agent scores");
76
+ expect(output).toContain("agent profile");
77
+ expect(output).toContain("agent improvement-report");
78
+ expect(output).toContain("agent proposals <agent-id> veto <proposal-id>");
79
+ expect(output).toContain("agent harness revert");
80
+ });
81
+
82
+ test("--help without ZOMBIE_OPERATOR does NOT contain operator commands", async () => {
83
+ await withIsolatedStateDir(async () => {
84
+ const out = bufferStream();
85
+ const err = bufferStream();
86
+ const code = await runCli(["--help"], {
87
+ stdout: out.stream,
88
+ stderr: err.stream,
89
+ env: { NO_COLOR: "1" },
90
+ });
91
+ expect(code).toBe(0);
92
+ const output = out.read();
93
+ expect(output).not.toContain("OPERATOR COMMANDS");
94
+ expect(output).not.toContain("harness source put");
95
+ expect(output).not.toContain("workspace upgrade-scale");
96
+ });
97
+ });
98
+
99
+ test("--help with operator token contains operator commands without env override", async () => {
100
+ await withIsolatedStateDir(async () => {
101
+ const out = bufferStream();
102
+ const err = bufferStream();
103
+ const token = [
104
+ Buffer.from(JSON.stringify({ alg: "none", typ: "JWT" })).toString("base64url"),
105
+ Buffer.from(JSON.stringify({ sub: "user_123", role: "operator" })).toString("base64url"),
106
+ "sig",
107
+ ].join(".");
108
+ const code = await runCli(["--help"], {
109
+ stdout: out.stream,
110
+ stderr: err.stream,
111
+ env: { ZOMBIE_TOKEN: token, NO_COLOR: "1" },
112
+ });
113
+ expect(code).toBe(0);
114
+ const output = out.read();
115
+ expect(output).toContain("OPERATOR COMMANDS");
116
+ expect(output).toContain("workspace upgrade-scale");
117
+ expect(output).toContain("admin config set scoring_context_max_tokens");
118
+ });
119
+ });
120
+
121
+ test("--help with API_KEY contains operator commands without ZOMBIE_API_KEY", async () => {
122
+ await withIsolatedStateDir(async () => {
123
+ const out = bufferStream();
124
+ const err = bufferStream();
125
+ const code = await runCli(["--help"], {
126
+ stdout: out.stream,
127
+ stderr: err.stream,
128
+ env: { API_KEY: "dev-key", NO_COLOR: "1" },
129
+ });
130
+ expect(code).toBe(0);
131
+ const output = out.read();
132
+ expect(output).toContain("OPERATOR COMMANDS");
133
+ expect(output).toContain("workspace upgrade-scale");
134
+ });
135
+ });
136
+
137
+ test("--help uses saved login credentials to show operator commands", async () => {
138
+ await withIsolatedStateDir(async (stateDir) => {
139
+ const out = bufferStream();
140
+ const err = bufferStream();
141
+ const token = [
142
+ Buffer.from(JSON.stringify({ alg: "none", typ: "JWT" })).toString("base64url"),
143
+ Buffer.from(JSON.stringify({ sub: "user_123", metadata: { role: "operator" } })).toString("base64url"),
144
+ "sig",
145
+ ].join(".");
146
+
147
+ await fs.writeFile(path.join(stateDir, "credentials.json"), JSON.stringify({
148
+ token,
149
+ saved_at: Date.now(),
150
+ session_id: "sess_test",
151
+ api_url: null,
152
+ }));
153
+
154
+ const code = await runCli(["--help"], {
155
+ stdout: out.stream,
156
+ stderr: err.stream,
157
+ env: { NO_COLOR: "1" },
158
+ });
159
+ expect(code).toBe(0);
160
+ const output = out.read();
161
+ expect(output).toContain("OPERATOR COMMANDS");
162
+ expect(output).toContain("workspace upgrade-scale");
163
+ });
164
+ });
165
+
166
+ test("--help shows environment variables section", async () => {
167
+ const out = bufferStream();
168
+ const err = bufferStream();
169
+ await runCli(["--help"], {
170
+ stdout: out.stream,
171
+ stderr: err.stream,
172
+ env: { ...process.env, NO_COLOR: "1" },
173
+ });
174
+ const output = out.read();
175
+ expect(output).toContain("ZOMBIE_API_URL");
176
+ expect(output).toContain("ZOMBIE_TOKEN");
177
+ expect(output).toContain("ZOMBIE_API_KEY");
178
+ expect(output).toContain("NO_COLOR");
179
+ });
180
+
181
+ test("--help with admin role token shows OPERATOR COMMANDS", async () => {
182
+ await withIsolatedStateDir(async () => {
183
+ const out = bufferStream();
184
+ const err = bufferStream();
185
+ const token = [
186
+ Buffer.from(JSON.stringify({ alg: "none", typ: "JWT" })).toString("base64url"),
187
+ Buffer.from(JSON.stringify({ sub: "user_admin", role: "admin" })).toString("base64url"),
188
+ "sig",
189
+ ].join(".");
190
+ const code = await runCli(["--help"], {
191
+ stdout: out.stream,
192
+ stderr: err.stream,
193
+ env: { ZOMBIE_TOKEN: token, NO_COLOR: "1" },
194
+ });
195
+ expect(code).toBe(0);
196
+ const output = out.read();
197
+ expect(output).toContain("OPERATOR COMMANDS");
198
+ expect(output).toContain("workspace upgrade-scale");
199
+ });
200
+ });
201
+
202
+ test("--help with user role token does NOT show OPERATOR COMMANDS", async () => {
203
+ await withIsolatedStateDir(async () => {
204
+ const out = bufferStream();
205
+ const err = bufferStream();
206
+ const token = [
207
+ Buffer.from(JSON.stringify({ alg: "none", typ: "JWT" })).toString("base64url"),
208
+ Buffer.from(JSON.stringify({ sub: "user_basic", role: "user" })).toString("base64url"),
209
+ "sig",
210
+ ].join(".");
211
+ const code = await runCli(["--help"], {
212
+ stdout: out.stream,
213
+ stderr: err.stream,
214
+ env: { ZOMBIE_TOKEN: token, NO_COLOR: "1" },
215
+ });
216
+ expect(code).toBe(0);
217
+ const output = out.read();
218
+ expect(output).not.toContain("OPERATOR COMMANDS");
219
+ expect(output).not.toContain("workspace upgrade-scale");
220
+ });
221
+ });
222
+
223
+ test("--help shows global flags", async () => {
224
+ const out = bufferStream();
225
+ const err = bufferStream();
226
+ await runCli(["--help"], {
227
+ stdout: out.stream,
228
+ stderr: err.stream,
229
+ env: { ...process.env, NO_COLOR: "1" },
230
+ });
231
+ const output = out.read();
232
+ expect(output).toContain("--json");
233
+ expect(output).toContain("--no-input");
234
+ expect(output).toContain("--version");
235
+ });
236
+ });
237
+
238
+ describe("version output", () => {
239
+ test("--version shows banner", async () => {
240
+ const out = bufferStream();
241
+ const err = bufferStream();
242
+ const code = await runCli(["--version"], {
243
+ stdout: out.stream,
244
+ stderr: err.stream,
245
+ env: { ...process.env },
246
+ });
247
+ expect(code).toBe(0);
248
+ expect(out.read()).toContain("zombiectl v0.3.0");
249
+ });
250
+
251
+ test("--version --json suppresses banner", async () => {
252
+ const out = bufferStream();
253
+ const err = bufferStream();
254
+ const code = await runCli(["--json", "--version"], {
255
+ stdout: out.stream,
256
+ stderr: err.stream,
257
+ env: { ...process.env },
258
+ });
259
+ expect(code).toBe(0);
260
+ const parsed = JSON.parse(out.read());
261
+ expect(parsed.version).toBe("0.3.0");
262
+ });
263
+
264
+ test("--version with NO_COLOR shows plain text", async () => {
265
+ const out = bufferStream();
266
+ const err = bufferStream();
267
+ const code = await runCli(["--version"], {
268
+ stdout: out.stream,
269
+ stderr: err.stream,
270
+ env: { ...process.env, NO_COLOR: "1" },
271
+ });
272
+ expect(code).toBe(0);
273
+ const output = out.read();
274
+ expect(output).toContain("zombiectl v0.3.0");
275
+ });
276
+ });
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Filesystem test helpers shared across spec-init and run-preview test suites.
3
+ */
4
+ import { mkdirSync, rmSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import os from "node:os";
7
+
8
+ export function makeTmp() {
9
+ const dir = join(os.tmpdir(), `zctl-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
10
+ mkdirSync(dir, { recursive: true });
11
+ return dir;
12
+ }
13
+
14
+ export function cleanup(dir) {
15
+ try { rmSync(dir, { recursive: true, force: true }); } catch {}
16
+ }
17
+
18
+ /** Simple parseFlags for use in tests that bypasses the real arg parser. */
19
+ export function parseFlags(tokens) {
20
+ const options = {};
21
+ for (let i = 0; i < tokens.length; i++) {
22
+ if (tokens[i].startsWith("--")) {
23
+ const key = tokens[i].slice(2);
24
+ const next = tokens[i + 1];
25
+ if (next && !next.startsWith("--")) { options[key] = next; i++; }
26
+ else options[key] = true;
27
+ }
28
+ }
29
+ return { options, positionals: [] };
30
+ }
31
+
32
+ export const writeLine = (s, l = "") => s.write(`${l}\n`);
@@ -0,0 +1,31 @@
1
+ import { Writable } from "node:stream";
2
+ import { ApiError } from "../src/lib/http.js";
3
+
4
+ export { ApiError };
5
+
6
+ /** Discard-all writable stream (use one per test to avoid state leaks). */
7
+ export function makeNoop() {
8
+ return new Writable({ write(_c, _e, cb) { cb(); } });
9
+ }
10
+
11
+ /** Writable that buffers output; call .read() to inspect. */
12
+ export function makeBufferStream() {
13
+ let data = "";
14
+ return {
15
+ stream: new Writable({ write(chunk, _enc, cb) { data += String(chunk); cb(); } }),
16
+ read: () => data,
17
+ };
18
+ }
19
+
20
+ /** Passthrough UI theme (no ANSI escapes). */
21
+ export const ui = { ok: (s) => s, err: (s) => s, info: (s) => s, dim: (s) => s, head: (s) => s };
22
+
23
+ // ── Stable test constants ─────────────────────────────────────────────────────
24
+ export const AGENT_ID = "0195b4ba-8d3a-7f13-8abc-000000000001";
25
+ export const AGENT_NAME = "my-agent";
26
+ export const WS_ID = "0195b4ba-8d3a-7f13-8abc-000000000010";
27
+ export const SCORE_ID_1 = "0195b4ba-8d3a-7f13-8abc-000000000021";
28
+ export const SCORE_ID_2 = "0195b4ba-8d3a-7f13-8abc-000000000022";
29
+ export const RUN_ID_1 = "0195b4ba-8d3a-7f13-8abc-000000000031";
30
+ export const RUN_ID_2 = "0195b4ba-8d3a-7f13-8abc-000000000032";
31
+ export const PVER_ID = "0195b4ba-8d3a-7f13-8abc-000000000041";
@@ -0,0 +1,57 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { makeBufferStream, ui } from "./helpers.js";
3
+ import { printHelp } from "../src/program/io.js";
4
+
5
+ describe("printHelp authRole option", () => {
6
+ test("authRole=operator shows operator commands", () => {
7
+ const out = makeBufferStream();
8
+ printHelp(out.stream, ui, { authRole: "operator" });
9
+ const output = out.read();
10
+ expect(output).toContain("OPERATOR COMMANDS");
11
+ expect(output).toContain("workspace upgrade-scale");
12
+ });
13
+
14
+ test("authRole=admin shows operator commands", () => {
15
+ const out = makeBufferStream();
16
+ printHelp(out.stream, ui, { authRole: "admin" });
17
+ const output = out.read();
18
+ expect(output).toContain("OPERATOR COMMANDS");
19
+ expect(output).toContain("workspace upgrade-scale");
20
+ });
21
+
22
+ test("authRole=user does NOT show operator commands", () => {
23
+ const out = makeBufferStream();
24
+ printHelp(out.stream, ui, { authRole: "user" });
25
+ const output = out.read();
26
+ expect(output).not.toContain("OPERATOR COMMANDS");
27
+ expect(output).not.toContain("workspace upgrade-scale");
28
+ });
29
+
30
+ test("no authRole and no ZOMBIE_OPERATOR hides operator commands", () => {
31
+ const out = makeBufferStream();
32
+ printHelp(out.stream, ui, {});
33
+ const output = out.read();
34
+ expect(output).not.toContain("OPERATOR COMMANDS");
35
+ });
36
+
37
+ test("ZOMBIE_OPERATOR=1 still works as env override", () => {
38
+ const out = makeBufferStream();
39
+ printHelp(out.stream, ui, { env: { ZOMBIE_OPERATOR: "1" } });
40
+ const output = out.read();
41
+ expect(output).toContain("OPERATOR COMMANDS");
42
+ });
43
+
44
+ test("operator commands section includes workspace upgrade-scale", () => {
45
+ const out = makeBufferStream();
46
+ printHelp(out.stream, ui, { operator: true });
47
+ const output = out.read();
48
+ expect(output).toContain("workspace upgrade-scale --workspace-id ID --subscription-id SUBSCRIPTION_ID");
49
+ });
50
+
51
+ test("ZOMBIE_OPERATOR description says force-show", () => {
52
+ const out = makeBufferStream();
53
+ printHelp(out.stream, ui, {});
54
+ const output = out.read();
55
+ expect(output).toContain("force-show operator commands");
56
+ });
57
+ });
@@ -0,0 +1,115 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { makeNoop, makeBufferStream, ui } from "./helpers.js";
3
+ import { createCoreHandlers } from "../src/commands/core.js";
4
+
5
+ function makeDeps(overrides = {}) {
6
+ return {
7
+ clearCredentials: async () => {},
8
+ createSpinner: () => ({ start() {}, succeed() {}, fail() {} }),
9
+ newIdempotencyKey: () => "idem_test",
10
+ openUrl: async () => false,
11
+ parseFlags: (tokens) => {
12
+ const options = {};
13
+ const positionals = [];
14
+ for (let i = 0; i < tokens.length; i++) {
15
+ if (tokens[i].startsWith("--")) {
16
+ const key = tokens[i].slice(2);
17
+ const next = tokens[i + 1];
18
+ if (next && !next.startsWith("--")) { options[key] = next; i++; }
19
+ else options[key] = true;
20
+ } else { positionals.push(tokens[i]); }
21
+ }
22
+ return { options, positionals };
23
+ },
24
+ printJson: (_s, v) => {},
25
+ printKeyValue: () => {},
26
+ printTable: () => {},
27
+ request: async () => ({}),
28
+ saveCredentials: async () => {},
29
+ saveWorkspaces: async () => {},
30
+ ui,
31
+ writeLine: (stream, line = "") => stream.write(`${line}\n`),
32
+ apiHeaders: () => ({}),
33
+ ...overrides,
34
+ };
35
+ }
36
+
37
+ describe("commandLogin", () => {
38
+ test("successful login flow", async () => {
39
+ const out = makeBufferStream();
40
+ let pollCount = 0;
41
+ const deps = makeDeps({
42
+ request: async (_ctx, reqPath) => {
43
+ if (reqPath === "/v1/auth/sessions") {
44
+ return { session_id: "sess_1", login_url: "https://login.test" };
45
+ }
46
+ pollCount++;
47
+ return { status: "complete", token: "tok_123" };
48
+ },
49
+ saveCredentials: async (creds) => {
50
+ expect(creds.token).toBe("tok_123");
51
+ },
52
+ });
53
+ const ctx = { stdout: out.stream, stderr: makeNoop(), jsonMode: false, noOpen: true, env: {} };
54
+ const workspaces = { current_workspace_id: null, items: [] };
55
+ const core = createCoreHandlers(ctx, workspaces, deps);
56
+ const code = await core.commandLogin([]);
57
+ expect(code).toBe(0);
58
+ expect(out.read()).toContain("login complete");
59
+ });
60
+
61
+ test("expired session returns 1", async () => {
62
+ const err = makeBufferStream();
63
+ const deps = makeDeps({
64
+ request: async (_ctx, reqPath) => {
65
+ if (reqPath === "/v1/auth/sessions") {
66
+ return { session_id: "sess_2", login_url: "https://login.test" };
67
+ }
68
+ return { status: "expired" };
69
+ },
70
+ });
71
+ const ctx = { stdout: makeNoop(), stderr: err.stream, jsonMode: false, noOpen: true, env: {} };
72
+ const workspaces = { current_workspace_id: null, items: [] };
73
+ const core = createCoreHandlers(ctx, workspaces, deps);
74
+ const code = await core.commandLogin([]);
75
+ expect(code).toBe(1);
76
+ expect(err.read()).toContain("expired");
77
+ });
78
+
79
+ test("timeout returns 1", async () => {
80
+ const err = makeBufferStream();
81
+ const deps = makeDeps({
82
+ request: async (_ctx, reqPath) => {
83
+ if (reqPath === "/v1/auth/sessions") {
84
+ return { session_id: "sess_3", login_url: "https://login.test" };
85
+ }
86
+ return { status: "pending", token: null };
87
+ },
88
+ });
89
+ const ctx = { stdout: makeNoop(), stderr: err.stream, jsonMode: false, noOpen: true, env: {} };
90
+ const workspaces = { current_workspace_id: null, items: [] };
91
+ const core = createCoreHandlers(ctx, workspaces, deps);
92
+ // use very short timeout
93
+ const code = await core.commandLogin(["--timeout-sec", "1", "--poll-ms", "100"]);
94
+ expect(code).toBe(1);
95
+ expect(err.read()).toContain("timed out");
96
+ });
97
+
98
+ test("--no-open flag skips browser", async () => {
99
+ let browserOpened = false;
100
+ const deps = makeDeps({
101
+ request: async (_ctx, reqPath) => {
102
+ if (reqPath === "/v1/auth/sessions") {
103
+ return { session_id: "sess_4", login_url: "https://login.test" };
104
+ }
105
+ return { status: "complete", token: "tok_456" };
106
+ },
107
+ openUrl: async () => { browserOpened = true; return true; },
108
+ });
109
+ const ctx = { stdout: makeNoop(), stderr: makeNoop(), jsonMode: false, noOpen: false, env: {} };
110
+ const workspaces = { current_workspace_id: null, items: [] };
111
+ const core = createCoreHandlers(ctx, workspaces, deps);
112
+ await core.commandLogin(["--no-open"]);
113
+ expect(browserOpened).toBe(false);
114
+ });
115
+ });
@@ -0,0 +1,65 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { makeNoop, makeBufferStream, ui } from "./helpers.js";
3
+ import { createCoreHandlers } from "../src/commands/core.js";
4
+
5
+ function makeDeps(overrides = {}) {
6
+ return {
7
+ clearCredentials: async () => {},
8
+ createSpinner: () => ({ start() {}, succeed() {}, fail() {} }),
9
+ newIdempotencyKey: () => "idem_test",
10
+ openUrl: async () => false,
11
+ parseFlags: (tokens) => {
12
+ const options = {};
13
+ const positionals = [];
14
+ for (let i = 0; i < tokens.length; i++) {
15
+ if (tokens[i].startsWith("--")) {
16
+ const key = tokens[i].slice(2);
17
+ const next = tokens[i + 1];
18
+ if (next && !next.startsWith("--")) { options[key] = next; i++; }
19
+ else options[key] = true;
20
+ } else { positionals.push(tokens[i]); }
21
+ }
22
+ return { options, positionals };
23
+ },
24
+ printJson: (_s, v) => {},
25
+ printKeyValue: () => {},
26
+ printTable: () => {},
27
+ request: async () => ({}),
28
+ saveCredentials: async () => {},
29
+ saveWorkspaces: async () => {},
30
+ ui,
31
+ writeLine: (stream, line = "") => stream.write(`${line}\n`),
32
+ apiHeaders: () => ({}),
33
+ ...overrides,
34
+ };
35
+ }
36
+
37
+ describe("commandLogout", () => {
38
+ test("clears credentials", async () => {
39
+ let cleared = false;
40
+ const out = makeBufferStream();
41
+ const deps = makeDeps({
42
+ clearCredentials: async () => { cleared = true; },
43
+ });
44
+ const ctx = { stdout: out.stream, stderr: makeNoop(), jsonMode: false, env: {} };
45
+ const workspaces = { current_workspace_id: null, items: [] };
46
+ const core = createCoreHandlers(ctx, workspaces, deps);
47
+ const code = await core.commandLogout();
48
+ expect(code).toBe(0);
49
+ expect(cleared).toBe(true);
50
+ expect(out.read()).toContain("logout complete");
51
+ });
52
+
53
+ test("JSON mode output", async () => {
54
+ let printed = null;
55
+ const deps = makeDeps({
56
+ printJson: (_s, v) => { printed = v; },
57
+ });
58
+ const ctx = { stdout: makeNoop(), stderr: makeNoop(), jsonMode: true, env: {} };
59
+ const workspaces = { current_workspace_id: null, items: [] };
60
+ const core = createCoreHandlers(ctx, workspaces, deps);
61
+ const code = await core.commandLogout();
62
+ expect(code).toBe(0);
63
+ expect(printed).toEqual({ status: "ok", logged_out: true });
64
+ });
65
+ });
@@ -0,0 +1,16 @@
1
+ import { test } from "bun:test";
2
+ import assert from "node:assert/strict";
3
+ import { parseGlobalArgs } from "../src/cli.js";
4
+
5
+ test("parseGlobalArgs uses flag api over env", () => {
6
+ const env = { ZOMBIE_API_URL: "https://env.example" };
7
+ const out = parseGlobalArgs(["--api", "https://flag.example", "doctor"], env);
8
+ assert.equal(out.global.apiUrl, "https://flag.example");
9
+ assert.equal(out.rest[0], "doctor");
10
+ });
11
+
12
+ test("parseGlobalArgs falls back to env api", () => {
13
+ const env = { ZOMBIE_API_URL: "https://env.example" };
14
+ const out = parseGlobalArgs(["doctor"], env);
15
+ assert.equal(out.global.apiUrl, "https://env.example");
16
+ });