@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,33 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { requireAuth, AUTH_FAIL_MESSAGE } from "../src/program/auth-guard.js";
|
|
3
|
+
|
|
4
|
+
describe("requireAuth", () => {
|
|
5
|
+
test("token present returns ok", () => {
|
|
6
|
+
const result = requireAuth({ token: "header.payload.sig", apiKey: null });
|
|
7
|
+
expect(result.ok).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("API key present returns ok", () => {
|
|
11
|
+
const result = requireAuth({ token: null, apiKey: "sk-test-123" });
|
|
12
|
+
expect(result.ok).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("both present returns ok", () => {
|
|
16
|
+
const result = requireAuth({ token: "tok", apiKey: "key" });
|
|
17
|
+
expect(result.ok).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("neither present returns fail", () => {
|
|
21
|
+
const result = requireAuth({ token: null, apiKey: null });
|
|
22
|
+
expect(result.ok).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("empty strings treated as falsy", () => {
|
|
26
|
+
const result = requireAuth({ token: "", apiKey: "" });
|
|
27
|
+
expect(result.ok).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("AUTH_FAIL_MESSAGE contains login instruction", () => {
|
|
31
|
+
expect(AUTH_FAIL_MESSAGE).toContain("zombiectl login");
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { test } from "bun:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { decodeTokenPayload, extractDistinctIdFromToken, extractRoleFromToken } from "../src/program/auth-token.js";
|
|
4
|
+
|
|
5
|
+
function makeToken(payload) {
|
|
6
|
+
const header = Buffer.from(JSON.stringify({ alg: "none", typ: "JWT" })).toString("base64url");
|
|
7
|
+
const body = Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
8
|
+
return `${header}.${body}.sig`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
test("extractDistinctIdFromToken returns sub for valid JWT payload", () => {
|
|
12
|
+
const token = makeToken({ sub: "user_123" });
|
|
13
|
+
assert.equal(extractDistinctIdFromToken(token), "user_123");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("extractDistinctIdFromToken trims and returns normalized sub", () => {
|
|
17
|
+
const token = makeToken({ sub: " user_trim " });
|
|
18
|
+
assert.equal(extractDistinctIdFromToken(token), "user_trim");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("extractDistinctIdFromToken returns null for malformed token formats", () => {
|
|
22
|
+
assert.equal(extractDistinctIdFromToken("bad-token"), null);
|
|
23
|
+
assert.equal(extractDistinctIdFromToken("a.b"), null);
|
|
24
|
+
assert.equal(extractDistinctIdFromToken(""), null);
|
|
25
|
+
assert.equal(extractDistinctIdFromToken(null), null);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("extractDistinctIdFromToken returns null when sub is missing or blank", () => {
|
|
29
|
+
const missingSub = makeToken({ role: "admin" });
|
|
30
|
+
const blankSub = makeToken({ sub: " " });
|
|
31
|
+
assert.equal(extractDistinctIdFromToken(missingSub), null);
|
|
32
|
+
assert.equal(extractDistinctIdFromToken(blankSub), null);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("extractRoleFromToken reads supported role claims", () => {
|
|
36
|
+
assert.equal(extractRoleFromToken(makeToken({ role: "admin" })), "admin");
|
|
37
|
+
assert.equal(extractRoleFromToken(makeToken({ metadata: { role: "operator" } })), "operator");
|
|
38
|
+
assert.equal(extractRoleFromToken(makeToken({ custom_claims: { role: "user" } })), "user");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("extractRoleFromToken normalizes namespaced and invalid claims", () => {
|
|
42
|
+
assert.equal(extractRoleFromToken(makeToken({ "https://usezombie.dev/role": "ADMIN" })), "admin");
|
|
43
|
+
assert.equal(extractRoleFromToken(makeToken({ role: "owner" })), null);
|
|
44
|
+
assert.equal(extractRoleFromToken("bad-token"), null);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("extractRoleFromToken reads usezombie.com namespace claim", () => {
|
|
48
|
+
assert.equal(extractRoleFromToken(makeToken({ "https://usezombie.com/role": "operator" })), "operator");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("extractRoleFromToken returns first valid role in priority order", () => {
|
|
52
|
+
// top-level role wins over metadata.role
|
|
53
|
+
assert.equal(extractRoleFromToken(makeToken({ role: "admin", metadata: { role: "user" } })), "admin");
|
|
54
|
+
// metadata.role wins when top-level is absent
|
|
55
|
+
assert.equal(extractRoleFromToken(makeToken({ metadata: { role: "user" }, custom_claims: { role: "admin" } })), "user");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("extractRoleFromToken returns null for empty or whitespace-only role", () => {
|
|
59
|
+
assert.equal(extractRoleFromToken(makeToken({ role: "" })), null);
|
|
60
|
+
assert.equal(extractRoleFromToken(makeToken({ role: " " })), null);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("extractRoleFromToken rejects whitespace-padded roles (matches backend parseAuthRole)", () => {
|
|
64
|
+
// Backend rbac.parseAuthRole rejects " operator " — CLI must match.
|
|
65
|
+
assert.equal(extractRoleFromToken(makeToken({ role: " operator " })), null);
|
|
66
|
+
assert.equal(extractRoleFromToken(makeToken({ role: " admin" })), null);
|
|
67
|
+
assert.equal(extractRoleFromToken(makeToken({ role: "user " })), null);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("extractRoleFromToken returns null for null/undefined token", () => {
|
|
71
|
+
assert.equal(extractRoleFromToken(null), null);
|
|
72
|
+
assert.equal(extractRoleFromToken(undefined), null);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("extractRoleFromToken reads app_metadata.role", () => {
|
|
76
|
+
assert.equal(extractRoleFromToken(makeToken({ app_metadata: { role: "operator" } })), "operator");
|
|
77
|
+
assert.equal(extractRoleFromToken(makeToken({ app_metadata: { role: "Admin" } })), "admin");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("extractRoleFromToken reads namespaced metadata claims", () => {
|
|
81
|
+
assert.equal(
|
|
82
|
+
extractRoleFromToken(makeToken({ metadata: { "https://usezombie.dev/role": "operator" } })),
|
|
83
|
+
"operator",
|
|
84
|
+
);
|
|
85
|
+
assert.equal(
|
|
86
|
+
extractRoleFromToken(makeToken({ metadata: { "https://usezombie.com/role": "Admin" } })),
|
|
87
|
+
"admin",
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("decodeTokenPayload returns parsed payload object", () => {
|
|
92
|
+
const payload = { sub: "user_1", role: "admin", iat: 1000 };
|
|
93
|
+
const result = decodeTokenPayload(makeToken(payload));
|
|
94
|
+
assert.equal(result.sub, "user_1");
|
|
95
|
+
assert.equal(result.role, "admin");
|
|
96
|
+
assert.equal(result.iat, 1000);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("decodeTokenPayload returns null for non-string input", () => {
|
|
100
|
+
assert.equal(decodeTokenPayload(null), null);
|
|
101
|
+
assert.equal(decodeTokenPayload(undefined), null);
|
|
102
|
+
assert.equal(decodeTokenPayload(42), null);
|
|
103
|
+
assert.equal(decodeTokenPayload(""), null);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("decodeTokenPayload returns null for malformed base64", () => {
|
|
107
|
+
assert.equal(decodeTokenPayload("header.!!!.sig"), null);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("decodeTokenPayload returns null for token with fewer than 2 parts", () => {
|
|
111
|
+
assert.equal(decodeTokenPayload("single-segment"), null);
|
|
112
|
+
});
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for src/program/banner.js — printBanner and printPreReleaseWarning.
|
|
3
|
+
*
|
|
4
|
+
* Coverage tiers addressed:
|
|
5
|
+
* T1 Happy path
|
|
6
|
+
* T2 Edge cases
|
|
7
|
+
* T3 Negative / error paths (void functions — suppression paths covered here)
|
|
8
|
+
* T4 Output fidelity (actual rendered text and ANSI correctness)
|
|
9
|
+
* T5 Concurrency — N/A (pure write functions, no shared state)
|
|
10
|
+
* T6 Integration via runCli ttyOnly flag
|
|
11
|
+
* T7 Regression guards (email, date, version constants pinned)
|
|
12
|
+
* T8 Security — N/A (no user input, no secret handling)
|
|
13
|
+
* T9 DRY — shared helpers from helpers.js
|
|
14
|
+
* T10 Constants / magic values flagged
|
|
15
|
+
* T11 Performance — N/A (simple stream writes, no allocation concern)
|
|
16
|
+
* T12 CLI contract (--version output, --json suppression, VERSION matches package.json)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { describe, test, expect } from "bun:test";
|
|
20
|
+
import { readFileSync } from "node:fs";
|
|
21
|
+
import { makeBufferStream } from "./helpers.js";
|
|
22
|
+
import { printBanner, printPreReleaseWarning } from "../src/program/banner.js";
|
|
23
|
+
import { runCli, VERSION } from "../src/cli.js";
|
|
24
|
+
|
|
25
|
+
// ── T9: helpers — no magic, no copy-paste ─────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
/** Simulate a TTY stderr by setting isTTY = true on the underlying stream. */
|
|
28
|
+
function makeTtyBufferStream() {
|
|
29
|
+
const b = makeBufferStream();
|
|
30
|
+
b.stream.isTTY = true;
|
|
31
|
+
return b;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Strip all ANSI escape sequences from a string for plain-text assertions. */
|
|
35
|
+
function stripAnsi(str) {
|
|
36
|
+
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── T10: constants guard — pin strings that appear in both code paths ─────────
|
|
40
|
+
// If either of these breaks, update the source AND the tests together.
|
|
41
|
+
const CONTACT_EMAIL = "nkishore@megam.io";
|
|
42
|
+
const LAUNCH_DATE = "April 5, 2026";
|
|
43
|
+
const PRE_RELEASE_TAG = "[PRE-RELEASE]";
|
|
44
|
+
|
|
45
|
+
// ── printPreReleaseWarning ─────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
describe("printPreReleaseWarning — T1: happy path", () => {
|
|
48
|
+
test("default opts writes non-empty output (color mode)", () => {
|
|
49
|
+
const out = makeBufferStream();
|
|
50
|
+
printPreReleaseWarning(out.stream, {});
|
|
51
|
+
expect(out.read().length).toBeGreaterThan(0);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("noColor=true writes non-empty plain output", () => {
|
|
55
|
+
const out = makeBufferStream();
|
|
56
|
+
printPreReleaseWarning(out.stream, { noColor: true });
|
|
57
|
+
expect(out.read().length).toBeGreaterThan(0);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("jsonMode=true suppresses all output", () => {
|
|
61
|
+
const out = makeBufferStream();
|
|
62
|
+
printPreReleaseWarning(out.stream, { jsonMode: true });
|
|
63
|
+
expect(out.read()).toBe("");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("ttyOnly=true suppresses all output", () => {
|
|
67
|
+
const out = makeBufferStream();
|
|
68
|
+
printPreReleaseWarning(out.stream, { ttyOnly: true });
|
|
69
|
+
expect(out.read()).toBe("");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("printPreReleaseWarning — T2: edge cases", () => {
|
|
74
|
+
test("no opts argument uses defaults (color mode)", () => {
|
|
75
|
+
const out = makeBufferStream();
|
|
76
|
+
printPreReleaseWarning(out.stream);
|
|
77
|
+
expect(out.read().length).toBeGreaterThan(0);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("empty opts object uses defaults (color mode)", () => {
|
|
81
|
+
const out = makeBufferStream();
|
|
82
|
+
printPreReleaseWarning(out.stream, {});
|
|
83
|
+
expect(out.read().length).toBeGreaterThan(0);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("noColor=false produces colored output (same as default)", () => {
|
|
87
|
+
const out = makeBufferStream();
|
|
88
|
+
printPreReleaseWarning(out.stream, { noColor: false });
|
|
89
|
+
expect(out.read()).toMatch(/\x1b\[/);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("jsonMode=false + noColor=false produces colored output", () => {
|
|
93
|
+
const out = makeBufferStream();
|
|
94
|
+
printPreReleaseWarning(out.stream, { jsonMode: false, noColor: false });
|
|
95
|
+
expect(out.read()).toMatch(/\x1b\[/);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("ttyOnly=true suppresses even when noColor=false", () => {
|
|
99
|
+
const out = makeBufferStream();
|
|
100
|
+
printPreReleaseWarning(out.stream, { ttyOnly: true, noColor: false });
|
|
101
|
+
expect(out.read()).toBe("");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("jsonMode=true takes priority over noColor=true (both suppress-eligible)", () => {
|
|
105
|
+
const out = makeBufferStream();
|
|
106
|
+
printPreReleaseWarning(out.stream, { jsonMode: true, noColor: true });
|
|
107
|
+
expect(out.read()).toBe("");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("ttyOnly=true + jsonMode=true both result in empty output", () => {
|
|
111
|
+
const out = makeBufferStream();
|
|
112
|
+
printPreReleaseWarning(out.stream, { ttyOnly: true, jsonMode: true });
|
|
113
|
+
expect(out.read()).toBe("");
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("printPreReleaseWarning — T3: suppression (negative) paths", () => {
|
|
118
|
+
test("jsonMode suppresses output regardless of noColor value", () => {
|
|
119
|
+
for (const noColor of [true, false]) {
|
|
120
|
+
const out = makeBufferStream();
|
|
121
|
+
printPreReleaseWarning(out.stream, { jsonMode: true, noColor });
|
|
122
|
+
expect(out.read()).toBe("");
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("ttyOnly suppresses output regardless of noColor value", () => {
|
|
127
|
+
for (const noColor of [true, false]) {
|
|
128
|
+
const out = makeBufferStream();
|
|
129
|
+
printPreReleaseWarning(out.stream, { ttyOnly: true, noColor });
|
|
130
|
+
expect(out.read()).toBe("");
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("printPreReleaseWarning — T4: output fidelity (color mode)", () => {
|
|
136
|
+
test("color output contains ANSI escape codes", () => {
|
|
137
|
+
const out = makeBufferStream();
|
|
138
|
+
printPreReleaseWarning(out.stream, {});
|
|
139
|
+
expect(out.read()).toMatch(/\x1b\[/);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("color output contains contact email", () => {
|
|
143
|
+
const out = makeBufferStream();
|
|
144
|
+
printPreReleaseWarning(out.stream, {});
|
|
145
|
+
expect(stripAnsi(out.read())).toContain(CONTACT_EMAIL);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("color output contains launch date", () => {
|
|
149
|
+
const out = makeBufferStream();
|
|
150
|
+
printPreReleaseWarning(out.stream, {});
|
|
151
|
+
expect(stripAnsi(out.read())).toContain(LAUNCH_DATE);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("color output contains warning symbol", () => {
|
|
155
|
+
const out = makeBufferStream();
|
|
156
|
+
printPreReleaseWarning(out.stream, {});
|
|
157
|
+
expect(out.read()).toContain("⚠");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("color output starts with a newline", () => {
|
|
161
|
+
const out = makeBufferStream();
|
|
162
|
+
printPreReleaseWarning(out.stream, {});
|
|
163
|
+
expect(out.read()).toMatch(/^\n/);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("color output ends with double newline", () => {
|
|
167
|
+
const out = makeBufferStream();
|
|
168
|
+
printPreReleaseWarning(out.stream, {});
|
|
169
|
+
expect(out.read()).toMatch(/\n\n$/);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("color output mentions 'Pre-release build'", () => {
|
|
173
|
+
const out = makeBufferStream();
|
|
174
|
+
printPreReleaseWarning(out.stream, {});
|
|
175
|
+
expect(stripAnsi(out.read())).toContain("Pre-release build");
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("printPreReleaseWarning — T4: output fidelity (noColor mode)", () => {
|
|
180
|
+
test("noColor output contains no ANSI escape codes", () => {
|
|
181
|
+
const out = makeBufferStream();
|
|
182
|
+
printPreReleaseWarning(out.stream, { noColor: true });
|
|
183
|
+
expect(out.read()).not.toMatch(/\x1b\[/);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("noColor output contains contact email", () => {
|
|
187
|
+
const out = makeBufferStream();
|
|
188
|
+
printPreReleaseWarning(out.stream, { noColor: true });
|
|
189
|
+
expect(out.read()).toContain(CONTACT_EMAIL);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("noColor output contains launch date", () => {
|
|
193
|
+
const out = makeBufferStream();
|
|
194
|
+
printPreReleaseWarning(out.stream, { noColor: true });
|
|
195
|
+
expect(out.read()).toContain(LAUNCH_DATE);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("noColor output contains [PRE-RELEASE] tag", () => {
|
|
199
|
+
const out = makeBufferStream();
|
|
200
|
+
printPreReleaseWarning(out.stream, { noColor: true });
|
|
201
|
+
expect(out.read()).toContain(PRE_RELEASE_TAG);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("noColor output starts with newline", () => {
|
|
205
|
+
const out = makeBufferStream();
|
|
206
|
+
printPreReleaseWarning(out.stream, { noColor: true });
|
|
207
|
+
expect(out.read()).toMatch(/^\n/);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("noColor output ends with double newline", () => {
|
|
211
|
+
const out = makeBufferStream();
|
|
212
|
+
printPreReleaseWarning(out.stream, { noColor: true });
|
|
213
|
+
expect(out.read()).toMatch(/\n\n$/);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("printPreReleaseWarning — T7: regression guards", () => {
|
|
218
|
+
test("contact email is nkishore@megam.io — pin against accidental change", () => {
|
|
219
|
+
const out = makeBufferStream();
|
|
220
|
+
printPreReleaseWarning(out.stream, { noColor: true });
|
|
221
|
+
expect(out.read()).toContain("nkishore@megam.io");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("launch date is April 5, 2026 — pin against accidental change", () => {
|
|
225
|
+
const out = makeBufferStream();
|
|
226
|
+
printPreReleaseWarning(out.stream, { noColor: true });
|
|
227
|
+
expect(out.read()).toContain("April 5, 2026");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("plain [PRE-RELEASE] tag present in noColor output — regression guard", () => {
|
|
231
|
+
const out = makeBufferStream();
|
|
232
|
+
printPreReleaseWarning(out.stream, { noColor: true });
|
|
233
|
+
expect(out.read()).toContain("[PRE-RELEASE]");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("contact email same in both color and noColor paths — not diverged", () => {
|
|
237
|
+
const color = makeBufferStream();
|
|
238
|
+
const plain = makeBufferStream();
|
|
239
|
+
printPreReleaseWarning(color.stream, {});
|
|
240
|
+
printPreReleaseWarning(plain.stream, { noColor: true });
|
|
241
|
+
expect(stripAnsi(color.read())).toContain(CONTACT_EMAIL);
|
|
242
|
+
expect(plain.read()).toContain(CONTACT_EMAIL);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("launch date same in both color and noColor paths — not diverged", () => {
|
|
246
|
+
const color = makeBufferStream();
|
|
247
|
+
const plain = makeBufferStream();
|
|
248
|
+
printPreReleaseWarning(color.stream, {});
|
|
249
|
+
printPreReleaseWarning(plain.stream, { noColor: true });
|
|
250
|
+
expect(stripAnsi(color.read())).toContain(LAUNCH_DATE);
|
|
251
|
+
expect(plain.read()).toContain(LAUNCH_DATE);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// ── printBanner ────────────────────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
describe("printBanner — T1: happy path", () => {
|
|
258
|
+
test("color mode writes version to stream", () => {
|
|
259
|
+
const out = makeBufferStream();
|
|
260
|
+
printBanner(out.stream, "0.3.0", {});
|
|
261
|
+
expect(stripAnsi(out.read())).toContain("zombiectl v0.3.0");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("noColor mode writes plain version line", () => {
|
|
265
|
+
const out = makeBufferStream();
|
|
266
|
+
printBanner(out.stream, "0.3.0", { noColor: true });
|
|
267
|
+
expect(out.read()).toBe("zombiectl v0.3.0\n");
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("jsonMode suppresses all output", () => {
|
|
271
|
+
const out = makeBufferStream();
|
|
272
|
+
printBanner(out.stream, "0.3.0", { jsonMode: true });
|
|
273
|
+
expect(out.read()).toBe("");
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe("printBanner — T2: edge cases", () => {
|
|
278
|
+
test("empty version string still writes", () => {
|
|
279
|
+
const out = makeBufferStream();
|
|
280
|
+
printBanner(out.stream, "", { noColor: true });
|
|
281
|
+
expect(out.read()).toBe("zombiectl v\n");
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("semver pre-release version string preserved", () => {
|
|
285
|
+
const out = makeBufferStream();
|
|
286
|
+
printBanner(out.stream, "1.2.3-beta.1", { noColor: true });
|
|
287
|
+
expect(out.read()).toContain("1.2.3-beta.1");
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("no opts argument uses defaults (color mode)", () => {
|
|
291
|
+
const out = makeBufferStream();
|
|
292
|
+
printBanner(out.stream, "0.3.0");
|
|
293
|
+
expect(out.read().length).toBeGreaterThan(0);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("empty opts uses defaults (color mode)", () => {
|
|
297
|
+
const out = makeBufferStream();
|
|
298
|
+
printBanner(out.stream, "0.3.0", {});
|
|
299
|
+
expect(out.read().length).toBeGreaterThan(0);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
describe("printBanner — T4: output fidelity", () => {
|
|
304
|
+
test("color mode output contains ANSI escape codes", () => {
|
|
305
|
+
const out = makeBufferStream();
|
|
306
|
+
printBanner(out.stream, "0.3.0", {});
|
|
307
|
+
expect(out.read()).toMatch(/\x1b\[/);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("color mode output contains box-drawing character", () => {
|
|
311
|
+
const out = makeBufferStream();
|
|
312
|
+
printBanner(out.stream, "0.3.0", {});
|
|
313
|
+
expect(out.read()).toContain("\u2500"); // ─ horizontal bar
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("color mode output contains tagline 'autonomous agent cli'", () => {
|
|
317
|
+
const out = makeBufferStream();
|
|
318
|
+
printBanner(out.stream, "0.3.0", {});
|
|
319
|
+
expect(stripAnsi(out.read())).toContain("autonomous agent cli");
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("noColor mode output contains no ANSI codes", () => {
|
|
323
|
+
const out = makeBufferStream();
|
|
324
|
+
printBanner(out.stream, "0.3.0", { noColor: true });
|
|
325
|
+
expect(out.read()).not.toMatch(/\x1b\[/);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("noColor mode output is exactly 'zombiectl v{version}\\n'", () => {
|
|
329
|
+
const out = makeBufferStream();
|
|
330
|
+
printBanner(out.stream, "0.3.0", { noColor: true });
|
|
331
|
+
expect(out.read()).toBe("zombiectl v0.3.0\n");
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
describe("printBanner — T7: regression guards", () => {
|
|
336
|
+
test("noColor plain format pinned — regression guard", () => {
|
|
337
|
+
const out = makeBufferStream();
|
|
338
|
+
printBanner(out.stream, "0.3.0", { noColor: true });
|
|
339
|
+
expect(out.read()).toBe("zombiectl v0.3.0\n");
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("color mode contains zombie emoji 🧟", () => {
|
|
343
|
+
const out = makeBufferStream();
|
|
344
|
+
printBanner(out.stream, "0.3.0", {});
|
|
345
|
+
expect(out.read()).toContain("\u{1F9DF}");
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// ── VERSION constant + ttyOnly integration ─────────────────────────────────────
|
|
350
|
+
|
|
351
|
+
describe("VERSION — T7 + T12: constant matches package.json", () => {
|
|
352
|
+
test("VERSION exported from cli.js matches package.json version", () => {
|
|
353
|
+
const pkg = JSON.parse(
|
|
354
|
+
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
355
|
+
);
|
|
356
|
+
expect(VERSION).toBe(pkg.version);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test("VERSION is 0.3.0", () => {
|
|
360
|
+
expect(VERSION).toBe("0.3.0");
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
describe("ttyOnly flag — T6: integration via runCli", () => {
|
|
365
|
+
test("pre-release warning shown when stderr is a TTY", async () => {
|
|
366
|
+
const out = makeBufferStream();
|
|
367
|
+
const err = makeTtyBufferStream(); // isTTY = true
|
|
368
|
+
const code = await runCli(["--version"], {
|
|
369
|
+
stdout: out.stream,
|
|
370
|
+
stderr: err.stream,
|
|
371
|
+
env: { NO_COLOR: "1" },
|
|
372
|
+
});
|
|
373
|
+
expect(code).toBe(0);
|
|
374
|
+
expect(err.read()).toContain("[PRE-RELEASE]");
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test("pre-release warning suppressed when stderr is not a TTY", async () => {
|
|
378
|
+
const out = makeBufferStream();
|
|
379
|
+
const err = makeBufferStream(); // isTTY is undefined → non-TTY
|
|
380
|
+
const code = await runCli(["--version"], {
|
|
381
|
+
stdout: out.stream,
|
|
382
|
+
stderr: err.stream,
|
|
383
|
+
env: { NO_COLOR: "1" },
|
|
384
|
+
});
|
|
385
|
+
expect(code).toBe(0);
|
|
386
|
+
expect(err.read()).toBe(""); // clean — no banner, no errors
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test("pre-release warning suppressed in --json mode even on TTY stderr", async () => {
|
|
390
|
+
const out = makeBufferStream();
|
|
391
|
+
const err = makeTtyBufferStream();
|
|
392
|
+
const code = await runCli(["--json", "--version"], {
|
|
393
|
+
stdout: out.stream,
|
|
394
|
+
stderr: err.stream,
|
|
395
|
+
env: { ...process.env },
|
|
396
|
+
});
|
|
397
|
+
expect(code).toBe(0);
|
|
398
|
+
expect(err.read()).toBe("");
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("--version stdout unaffected by ttyOnly flag (output always correct)", async () => {
|
|
402
|
+
// Non-TTY path
|
|
403
|
+
const out1 = makeBufferStream();
|
|
404
|
+
await runCli(["--version"], {
|
|
405
|
+
stdout: out1.stream,
|
|
406
|
+
stderr: makeBufferStream().stream,
|
|
407
|
+
env: { NO_COLOR: "1" },
|
|
408
|
+
});
|
|
409
|
+
// TTY path
|
|
410
|
+
const out2 = makeBufferStream();
|
|
411
|
+
await runCli(["--version"], {
|
|
412
|
+
stdout: out2.stream,
|
|
413
|
+
stderr: makeTtyBufferStream().stream,
|
|
414
|
+
env: { NO_COLOR: "1" },
|
|
415
|
+
});
|
|
416
|
+
expect(out1.read()).toContain("zombiectl v0.3.0");
|
|
417
|
+
expect(out2.read()).toContain("zombiectl v0.3.0");
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
describe("ttyOnly flag — T1 + T4: output fidelity via runCli", () => {
|
|
422
|
+
test("--version --json stdout is parseable JSON with correct version", async () => {
|
|
423
|
+
const out = makeBufferStream();
|
|
424
|
+
await runCli(["--json", "--version"], {
|
|
425
|
+
stdout: out.stream,
|
|
426
|
+
stderr: makeBufferStream().stream,
|
|
427
|
+
env: { ...process.env },
|
|
428
|
+
});
|
|
429
|
+
const parsed = JSON.parse(out.read());
|
|
430
|
+
expect(parsed.version).toBe(VERSION);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test("--version NO_COLOR output has no ANSI on stdout", async () => {
|
|
434
|
+
const out = makeBufferStream();
|
|
435
|
+
await runCli(["--version"], {
|
|
436
|
+
stdout: out.stream,
|
|
437
|
+
stderr: makeBufferStream().stream,
|
|
438
|
+
env: { NO_COLOR: "1" },
|
|
439
|
+
});
|
|
440
|
+
expect(out.read()).not.toMatch(/\x1b\[/);
|
|
441
|
+
});
|
|
442
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { test } from "bun:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { resolveBrowserCommand } from "../src/lib/browser.js";
|
|
5
|
+
|
|
6
|
+
test("resolveBrowserCommand disables browser launch when BROWSER=false on darwin", async () => {
|
|
7
|
+
const resolved = await resolveBrowserCommand({ BROWSER: "false" }, "darwin");
|
|
8
|
+
assert.equal(resolved.argv, null);
|
|
9
|
+
assert.equal(resolved.reason, "browser-disabled");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("resolveBrowserCommand disables browser launch when BROWSER=0 on linux", async () => {
|
|
13
|
+
const resolved = await resolveBrowserCommand({ BROWSER: "0", DISPLAY: ":0" }, "linux");
|
|
14
|
+
assert.equal(resolved.argv, null);
|
|
15
|
+
assert.equal(resolved.reason, "browser-disabled");
|
|
16
|
+
});
|