switchroom 0.8.1 → 0.10.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 +49 -57
- package/bin/timezone-hook.sh +9 -7
- package/dist/agent-scheduler/index.js +285 -45
- package/dist/auth-broker/index.js +13932 -0
- package/dist/cli/switchroom.js +15931 -12778
- package/dist/host-control/main.js +582 -43
- package/dist/vault/approvals/kernel-server.js +276 -47
- package/dist/vault/broker/server.js +333 -69
- package/examples/minimal.yaml +63 -0
- package/examples/personal-google-workspace-mcp/.env.example +34 -0
- package/examples/personal-google-workspace-mcp/README.md +194 -0
- package/examples/personal-google-workspace-mcp/compose.yaml +66 -0
- package/examples/switchroom.yaml +220 -0
- package/package.json +6 -4
- package/profiles/_base/start.sh.hbs +3 -3
- package/profiles/_shared/agent-self-service.md.hbs +126 -0
- package/profiles/default/CLAUDE.md +10 -0
- package/profiles/default/CLAUDE.md.hbs +16 -0
- package/skills/buildkite-agent-infrastructure/SKILL.md +30 -11
- package/skills/buildkite-agent-runtime/SKILL.md +44 -11
- package/skills/buildkite-api/SKILL.md +31 -8
- package/skills/buildkite-cli/SKILL.md +27 -9
- package/skills/buildkite-migration/SKILL.md +22 -9
- package/skills/buildkite-pipelines/SKILL.md +26 -9
- package/skills/buildkite-secure-delivery/SKILL.md +23 -9
- package/skills/buildkite-test-engine/SKILL.md +25 -8
- package/skills/docx/SKILL.md +1 -1
- package/skills/file-bug/SKILL.md +34 -6
- package/skills/humanizer/SKILL.md +15 -0
- package/skills/humanizer-calibrate/SKILL.md +7 -1
- package/skills/mcp-builder/SKILL.md +1 -1
- package/skills/pdf/SKILL.md +1 -1
- package/skills/pptx/SKILL.md +1 -1
- package/skills/skill-creator/SKILL.md +21 -1
- package/skills/skill-creator/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/generate_report.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/improve_description.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_eval.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_loop.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/utils.cpython-313.pyc +0 -0
- package/skills/switchroom-cli/SKILL.md +63 -64
- package/skills/switchroom-health/SKILL.md +23 -10
- package/skills/switchroom-install/SKILL.md +3 -3
- package/skills/switchroom-manage/SKILL.md +26 -19
- package/skills/switchroom-runtime/SKILL.md +67 -15
- package/skills/switchroom-status/SKILL.md +26 -1
- package/skills/telegram-test-harness/SKILL.md +3 -0
- package/skills/webapp-testing/SKILL.md +31 -1
- package/skills/xlsx/SKILL.md +1 -1
- package/telegram-plugin/admin-commands/index.ts +7 -5
- package/telegram-plugin/dist/gateway/gateway.js +13042 -12844
- package/telegram-plugin/gateway/auth-add-flow.ts +326 -0
- package/telegram-plugin/gateway/auth-broker-client.ts +75 -0
- package/telegram-plugin/gateway/auth-command.ts +794 -0
- package/telegram-plugin/gateway/auth-line.ts +123 -0
- package/telegram-plugin/gateway/boot-card.ts +22 -36
- package/telegram-plugin/gateway/boot-probes.ts +3 -3
- package/telegram-plugin/gateway/gateway.ts +313 -798
- package/telegram-plugin/gateway/hostd-dispatch.ts +117 -0
- package/telegram-plugin/hooks/tool-label-pretool.mjs +11 -0
- package/telegram-plugin/hooks/wedge-detect-posttool.mjs +303 -0
- package/telegram-plugin/permission-title.ts +56 -0
- package/telegram-plugin/quota-check.ts +19 -41
- package/telegram-plugin/scripts/build.mjs +0 -1
- package/telegram-plugin/shared/bot-runtime.ts +5 -4
- package/telegram-plugin/tests/auth-add-flow.test.ts +559 -0
- package/telegram-plugin/tests/auth-code-redact.test.ts +8 -4
- package/telegram-plugin/tests/auth-command-vernacular.test.ts +531 -0
- package/telegram-plugin/tests/boot-probes.test.ts +11 -4
- package/telegram-plugin/tests/hostd-dispatch.test.ts +129 -0
- package/telegram-plugin/tests/permission-title.test.ts +31 -0
- package/telegram-plugin/tests/quota-check.test.ts +5 -35
- package/telegram-plugin/uat/SETUP.md +31 -1
- package/telegram-plugin/uat/runners/agent-self-sufficiency.ts +457 -0
- package/telegram-plugin/uat/runners/paraphrases.ts +231 -0
- package/telegram-plugin/uat/runners/report.ts +150 -0
- package/telegram-plugin/uat/runners/run-agent-self-sufficiency.sh +50 -0
- package/telegram-plugin/uat/runners/scorer.test.ts +196 -0
- package/telegram-plugin/uat/runners/scorer.ts +106 -0
- package/telegram-plugin/uat/runners/skill-coverage.test.ts +100 -0
- package/telegram-plugin/uat/runners/skill-coverage.ts +620 -0
- package/telegram-plugin/uat/scenarios/jtbd-interrupt-marker-dm.test.ts +7 -1
- package/telegram-plugin/uat/scenarios/jtbd-rapid-followup-dm.test.ts +7 -1
- package/telegram-plugin/auth-dashboard.ts +0 -1104
- package/telegram-plugin/auth-slot-parser.ts +0 -497
- package/telegram-plugin/dist/foreman/foreman.js +0 -31358
- package/telegram-plugin/foreman/foreman-create-flow.ts +0 -202
- package/telegram-plugin/foreman/foreman-handlers.ts +0 -493
- package/telegram-plugin/foreman/foreman.ts +0 -1165
- package/telegram-plugin/foreman/setup-flow.ts +0 -345
- package/telegram-plugin/foreman/setup-state.ts +0 -239
- package/telegram-plugin/foreman/state.ts +0 -203
- package/telegram-plugin/tests/auth-account-identity-surface.test.ts +0 -118
- package/telegram-plugin/tests/auth-dashboard-edge-cases.test.ts +0 -260
- package/telegram-plugin/tests/auth-dashboard-restart-flow.test.ts +0 -140
- package/telegram-plugin/tests/auth-dashboard-v3b.test.ts +0 -559
- package/telegram-plugin/tests/auth-dashboard.test.ts +0 -1045
- package/telegram-plugin/tests/auth-slot-commands.test.ts +0 -640
- package/telegram-plugin/tests/boot-card-account-quota.test.ts +0 -137
- package/telegram-plugin/tests/foreman-create-flow.test.ts +0 -359
- package/telegram-plugin/tests/foreman-handlers.test.ts +0 -347
- package/telegram-plugin/tests/foreman-state.test.ts +0 -164
- package/telegram-plugin/tests/foreman-write-ops.test.ts +0 -214
- package/telegram-plugin/tests/setup-flow.test.ts +0 -510
- package/telegram-plugin/tests/setup-state.test.ts +0 -146
|
@@ -1,640 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
assertSafeSlotName,
|
|
4
|
-
parseAuthSubCommand,
|
|
5
|
-
checkRemoveSafety,
|
|
6
|
-
formatSlotList,
|
|
7
|
-
splitFlags,
|
|
8
|
-
usageText,
|
|
9
|
-
AUTH_VERBS,
|
|
10
|
-
type SlotListingFromCli,
|
|
11
|
-
type AuthIntent,
|
|
12
|
-
} from "../auth-slot-parser";
|
|
13
|
-
|
|
14
|
-
describe("assertSafeSlotName", () => {
|
|
15
|
-
it("accepts valid slot names", () => {
|
|
16
|
-
for (const name of ["default", "personal-1", "work_acct", "a", "A".repeat(32)]) {
|
|
17
|
-
expect(() => assertSafeSlotName(name)).not.toThrow();
|
|
18
|
-
}
|
|
19
|
-
});
|
|
20
|
-
it("rejects empty string", () => {
|
|
21
|
-
expect(() => assertSafeSlotName("")).toThrow(/invalid slot name/);
|
|
22
|
-
});
|
|
23
|
-
it("rejects names > 32 chars", () => {
|
|
24
|
-
expect(() => assertSafeSlotName("a".repeat(33))).toThrow(/invalid slot name/);
|
|
25
|
-
});
|
|
26
|
-
it("rejects shell metacharacters and spaces", () => {
|
|
27
|
-
for (const bad of ["foo bar", "foo;ls", "foo/bar", "foo.bar", "foo$var", "foo|bar"]) {
|
|
28
|
-
expect(() => assertSafeSlotName(bad)).toThrow(/invalid slot name/);
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
describe("parseAuthSubCommand — existing verbs pass-through", () => {
|
|
34
|
-
it("defaults to status with no args", () => {
|
|
35
|
-
const intent = parseAuthSubCommand([], "clerk");
|
|
36
|
-
expect(intent.kind).toBe("status");
|
|
37
|
-
if (intent.kind === "status") expect(intent.cliArgs).toEqual(["auth", "status"]);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("login with no agent defaults to currentAgent", () => {
|
|
41
|
-
const intent = parseAuthSubCommand(["login"], "clerk");
|
|
42
|
-
expect(intent.kind).toBe("login");
|
|
43
|
-
if (intent.kind === "login") {
|
|
44
|
-
expect(intent.agent).toBe("clerk");
|
|
45
|
-
expect(intent.cliArgs).toEqual(["auth", "login", "clerk"]);
|
|
46
|
-
expect(intent.registerReauth).toBe(true);
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("login with explicit agent", () => {
|
|
51
|
-
const intent = parseAuthSubCommand(["login", "klanker"], "clerk");
|
|
52
|
-
expect(intent.kind).toBe("login");
|
|
53
|
-
if (intent.kind === "login") expect(intent.agent).toBe("klanker");
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("code requires a browser code", () => {
|
|
57
|
-
const intent = parseAuthSubCommand(["code"], "clerk");
|
|
58
|
-
expect(intent.kind).toBe("usage");
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("code treats 2 args as agent-is-current + code", () => {
|
|
62
|
-
const intent = parseAuthSubCommand(["code", "ABC123"], "clerk");
|
|
63
|
-
expect(intent.kind).toBe("code");
|
|
64
|
-
if (intent.kind === "code") {
|
|
65
|
-
expect(intent.agent).toBe("clerk");
|
|
66
|
-
expect(intent.code).toBe("ABC123");
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("code treats 3 args as agent + code", () => {
|
|
71
|
-
const intent = parseAuthSubCommand(["code", "klanker", "ABC123"], "clerk");
|
|
72
|
-
expect(intent.kind).toBe("code");
|
|
73
|
-
if (intent.kind === "code") {
|
|
74
|
-
expect(intent.agent).toBe("klanker");
|
|
75
|
-
expect(intent.code).toBe("ABC123");
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
describe("parseAuthSubCommand — /auth add", () => {
|
|
81
|
-
it("defaults agent to currentAgent", () => {
|
|
82
|
-
const intent = parseAuthSubCommand(["add"], "clerk");
|
|
83
|
-
expect(intent.kind).toBe("add");
|
|
84
|
-
if (intent.kind === "add") {
|
|
85
|
-
expect(intent.agent).toBe("clerk");
|
|
86
|
-
expect(intent.slot).toBeUndefined();
|
|
87
|
-
expect(intent.cliArgs).toEqual(["auth", "add", "clerk"]);
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("takes explicit agent", () => {
|
|
92
|
-
const intent = parseAuthSubCommand(["add", "klanker"], "clerk");
|
|
93
|
-
expect(intent.kind).toBe("add");
|
|
94
|
-
if (intent.kind === "add") {
|
|
95
|
-
expect(intent.agent).toBe("klanker");
|
|
96
|
-
expect(intent.cliArgs).toEqual(["auth", "add", "klanker"]);
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("accepts --slot value", () => {
|
|
101
|
-
const intent = parseAuthSubCommand(["add", "clerk", "--slot", "personal"], "clerk");
|
|
102
|
-
expect(intent.kind).toBe("add");
|
|
103
|
-
if (intent.kind === "add") {
|
|
104
|
-
expect(intent.slot).toBe("personal");
|
|
105
|
-
expect(intent.cliArgs).toEqual(["auth", "add", "clerk", "--slot", "personal"]);
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("rejects invalid slot name", () => {
|
|
110
|
-
const intent = parseAuthSubCommand(["add", "clerk", "--slot", "bad name"], "clerk");
|
|
111
|
-
expect(intent.kind).toBe("error");
|
|
112
|
-
if (intent.kind === "error") expect(intent.message).toMatch(/slot name/i);
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
describe("parseAuthSubCommand — /auth use", () => {
|
|
117
|
-
it("requires a slot arg", () => {
|
|
118
|
-
const intent = parseAuthSubCommand(["use"], "clerk");
|
|
119
|
-
expect(intent.kind).toBe("usage");
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it("1 positional arg = slot (agent defaults)", () => {
|
|
123
|
-
const intent = parseAuthSubCommand(["use", "personal"], "clerk");
|
|
124
|
-
expect(intent.kind).toBe("use");
|
|
125
|
-
if (intent.kind === "use") {
|
|
126
|
-
expect(intent.agent).toBe("clerk");
|
|
127
|
-
expect(intent.slot).toBe("personal");
|
|
128
|
-
expect(intent.force).toBe(false);
|
|
129
|
-
expect(intent.cliArgs).toEqual(["auth", "use", "clerk", "personal"]);
|
|
130
|
-
expect(intent.restartAgentAfter).toBe(true);
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("2 positional args = agent + slot", () => {
|
|
135
|
-
const intent = parseAuthSubCommand(["use", "klanker", "personal"], "clerk");
|
|
136
|
-
expect(intent.kind).toBe("use");
|
|
137
|
-
if (intent.kind === "use") {
|
|
138
|
-
expect(intent.agent).toBe("klanker");
|
|
139
|
-
expect(intent.slot).toBe("personal");
|
|
140
|
-
expect(intent.force).toBe(false);
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("--force sets force=true (#421)", () => {
|
|
145
|
-
const intent = parseAuthSubCommand(["use", "personal", "--force"], "clerk");
|
|
146
|
-
expect(intent.kind).toBe("use");
|
|
147
|
-
if (intent.kind === "use") {
|
|
148
|
-
expect(intent.slot).toBe("personal");
|
|
149
|
-
expect(intent.force).toBe(true);
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("--force with explicit agent + slot", () => {
|
|
154
|
-
const intent = parseAuthSubCommand(["use", "klanker", "personal", "--force"], "clerk");
|
|
155
|
-
expect(intent.kind).toBe("use");
|
|
156
|
-
if (intent.kind === "use") {
|
|
157
|
-
expect(intent.agent).toBe("klanker");
|
|
158
|
-
expect(intent.slot).toBe("personal");
|
|
159
|
-
expect(intent.force).toBe(true);
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("rejects invalid slot name with clear error", () => {
|
|
164
|
-
const intent = parseAuthSubCommand(["use", "bad slot"], "clerk");
|
|
165
|
-
expect(intent.kind).toBe("error");
|
|
166
|
-
if (intent.kind === "error") expect(intent.message).toMatch(/slot name/i);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it("rejects invalid agent name", () => {
|
|
170
|
-
const intent = parseAuthSubCommand(["use", "bad;agent", "slot"], "clerk");
|
|
171
|
-
expect(intent.kind).toBe("error");
|
|
172
|
-
if (intent.kind === "error") expect(intent.message).toMatch(/agent name/i);
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
describe("parseAuthSubCommand — /auth list", () => {
|
|
177
|
-
it("defaults agent to currentAgent, requests JSON from CLI", () => {
|
|
178
|
-
const intent = parseAuthSubCommand(["list"], "clerk");
|
|
179
|
-
expect(intent.kind).toBe("list");
|
|
180
|
-
if (intent.kind === "list") {
|
|
181
|
-
expect(intent.agent).toBe("clerk");
|
|
182
|
-
expect(intent.cliArgs).toEqual(["auth", "list", "clerk", "--json"]);
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it("takes explicit agent", () => {
|
|
187
|
-
const intent = parseAuthSubCommand(["list", "klanker"], "clerk");
|
|
188
|
-
expect(intent.kind).toBe("list");
|
|
189
|
-
if (intent.kind === "list") expect(intent.agent).toBe("klanker");
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
describe("parseAuthSubCommand — /auth rm", () => {
|
|
194
|
-
it("requires a slot", () => {
|
|
195
|
-
const intent = parseAuthSubCommand(["rm"], "clerk");
|
|
196
|
-
expect(intent.kind).toBe("usage");
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it("1 positional arg = slot, no --force", () => {
|
|
200
|
-
const intent = parseAuthSubCommand(["rm", "personal"], "clerk");
|
|
201
|
-
expect(intent.kind).toBe("rm");
|
|
202
|
-
if (intent.kind === "rm") {
|
|
203
|
-
expect(intent.agent).toBe("clerk");
|
|
204
|
-
expect(intent.slot).toBe("personal");
|
|
205
|
-
expect(intent.force).toBe(false);
|
|
206
|
-
expect(intent.cliArgs).toEqual(["auth", "rm", "clerk", "personal"]);
|
|
207
|
-
}
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it("--force sets force=true", () => {
|
|
211
|
-
const intent = parseAuthSubCommand(["rm", "personal", "--force"], "clerk");
|
|
212
|
-
expect(intent.kind).toBe("rm");
|
|
213
|
-
if (intent.kind === "rm") expect(intent.force).toBe(true);
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
it("2 positional args = agent + slot", () => {
|
|
217
|
-
const intent = parseAuthSubCommand(["rm", "klanker", "personal"], "clerk");
|
|
218
|
-
expect(intent.kind).toBe("rm");
|
|
219
|
-
if (intent.kind === "rm") {
|
|
220
|
-
expect(intent.agent).toBe("klanker");
|
|
221
|
-
expect(intent.slot).toBe("personal");
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it("rejects invalid slot name", () => {
|
|
226
|
-
const intent = parseAuthSubCommand(["rm", "bad slot"], "clerk");
|
|
227
|
-
expect(intent.kind).toBe("error");
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
describe("parseAuthSubCommand — unknown verb", () => {
|
|
232
|
-
it("returns usage with full verb list", () => {
|
|
233
|
-
const intent = parseAuthSubCommand(["foo"], "clerk");
|
|
234
|
-
expect(intent.kind).toBe("usage");
|
|
235
|
-
if (intent.kind === "usage") {
|
|
236
|
-
// Primary verbs should appear in the usage text.
|
|
237
|
-
// ("link" is an alias of "login" and not listed separately;
|
|
238
|
-
// "status" is the bare /auth with no args.)
|
|
239
|
-
const visibleVerbs = AUTH_VERBS.filter(v => v !== "link" && v !== "status");
|
|
240
|
-
for (const v of visibleVerbs) {
|
|
241
|
-
expect(intent.message).toContain(`/auth ${v}`);
|
|
242
|
-
}
|
|
243
|
-
// sanity: the 4 new verbs definitely appear
|
|
244
|
-
expect(intent.message).toContain("/auth add");
|
|
245
|
-
expect(intent.message).toContain("/auth use");
|
|
246
|
-
expect(intent.message).toContain("/auth list");
|
|
247
|
-
expect(intent.message).toContain("/auth rm");
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
describe("checkRemoveSafety", () => {
|
|
253
|
-
const baseListing: SlotListingFromCli = {
|
|
254
|
-
agent: "clerk",
|
|
255
|
-
slots: [
|
|
256
|
-
{ slot: "default", active: true, health: "healthy", expires_at: null, quota_exhausted_until: null },
|
|
257
|
-
{ slot: "personal", active: false, health: "healthy", expires_at: null, quota_exhausted_until: null },
|
|
258
|
-
],
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
it("allows removing inactive slot without force", () => {
|
|
262
|
-
expect(checkRemoveSafety(baseListing, "personal", false)).toBeNull();
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
it("blocks removing active slot without force", () => {
|
|
266
|
-
const err = checkRemoveSafety(baseListing, "default", false);
|
|
267
|
-
expect(err).toMatch(/active slot/i);
|
|
268
|
-
expect(err).toMatch(/--force/);
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it("blocks removing only slot without force", () => {
|
|
272
|
-
const only: SlotListingFromCli = {
|
|
273
|
-
agent: "clerk",
|
|
274
|
-
slots: [{ slot: "default", active: true, health: "healthy", expires_at: null, quota_exhausted_until: null }],
|
|
275
|
-
};
|
|
276
|
-
const err = checkRemoveSafety(only, "default", false);
|
|
277
|
-
expect(err).toMatch(/only account slot/i);
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
it("--force bypasses all safety checks", () => {
|
|
281
|
-
expect(checkRemoveSafety(baseListing, "default", true)).toBeNull();
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
it("returns null for unknown slot (CLI will produce its own error)", () => {
|
|
285
|
-
expect(checkRemoveSafety(baseListing, "ghost", false)).toBeNull();
|
|
286
|
-
});
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
describe("formatSlotList", () => {
|
|
290
|
-
const now = Date.now();
|
|
291
|
-
|
|
292
|
-
it("shows active marker on active slot", () => {
|
|
293
|
-
const out = formatSlotList({
|
|
294
|
-
agent: "clerk",
|
|
295
|
-
slots: [
|
|
296
|
-
{ slot: "default", active: true, health: "healthy", expires_at: null, quota_exhausted_until: null },
|
|
297
|
-
{ slot: "personal", active: false, health: "healthy", expires_at: null, quota_exhausted_until: null },
|
|
298
|
-
],
|
|
299
|
-
});
|
|
300
|
-
expect(out).toContain("<b>Slots for clerk</b>");
|
|
301
|
-
expect(out).toContain("●"); // active marker
|
|
302
|
-
expect(out).toContain("<code>default</code>");
|
|
303
|
-
expect(out).toContain("<code>personal</code>");
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
it("shows 'no slots' message when empty", () => {
|
|
307
|
-
const out = formatSlotList({ agent: "clerk", slots: [] });
|
|
308
|
-
expect(out).toMatch(/no slots/i);
|
|
309
|
-
expect(out).toContain("/auth add");
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it("surfaces quota-exhausted with resets-in tail", () => {
|
|
313
|
-
const until = now + 30 * 60_000;
|
|
314
|
-
const out = formatSlotList({
|
|
315
|
-
agent: "clerk",
|
|
316
|
-
slots: [{ slot: "default", active: true, health: "quota-exhausted", expires_at: null, quota_exhausted_until: until }],
|
|
317
|
-
});
|
|
318
|
-
expect(out).toMatch(/resets in ~\d+m/);
|
|
319
|
-
expect(out).toContain("⚠️");
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
it("surfaces expired with reauth hint", () => {
|
|
323
|
-
const out = formatSlotList({
|
|
324
|
-
agent: "clerk",
|
|
325
|
-
slots: [{ slot: "default", active: true, health: "expired", expires_at: null, quota_exhausted_until: null }],
|
|
326
|
-
});
|
|
327
|
-
expect(out).toMatch(/\/auth reauth/);
|
|
328
|
-
expect(out).toContain("⌛");
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
it("escapes HTML in agent name", () => {
|
|
332
|
-
const out = formatSlotList({ agent: "<evil>", slots: [] });
|
|
333
|
-
expect(out).toContain("<evil>");
|
|
334
|
-
expect(out).not.toContain("<evil>");
|
|
335
|
-
});
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
describe("splitFlags", () => {
|
|
339
|
-
it("extracts value flag + leaves positional", () => {
|
|
340
|
-
const { flags, positional } = splitFlags(["agent", "--slot", "personal"], ["--slot"]);
|
|
341
|
-
expect(flags).toEqual({ "--slot": "personal" });
|
|
342
|
-
expect(positional).toEqual(["agent"]);
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
it("treats value flag with no value as boolean", () => {
|
|
346
|
-
const { flags, positional } = splitFlags(["--slot"], ["--slot"]);
|
|
347
|
-
expect(flags["--slot"]).toBe(true);
|
|
348
|
-
expect(positional).toEqual([]);
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
it("bare --flag becomes boolean", () => {
|
|
352
|
-
const { flags, positional } = splitFlags(["slot", "--force"], []);
|
|
353
|
-
expect(flags["--force"]).toBe(true);
|
|
354
|
-
expect(positional).toEqual(["slot"]);
|
|
355
|
-
});
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
describe("usageText", () => {
|
|
359
|
-
it("lists all ten public sub-verbs", () => {
|
|
360
|
-
const u = usageText();
|
|
361
|
-
for (const v of ["login", "reauth", "code", "cancel", "add", "use", "list", "rm"]) {
|
|
362
|
-
expect(u).toContain(`/auth ${v}`);
|
|
363
|
-
}
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
it("lists the new account-shaped verbs", () => {
|
|
367
|
-
const u = usageText();
|
|
368
|
-
expect(u).toContain("/auth account add");
|
|
369
|
-
expect(u).toContain("/auth account list");
|
|
370
|
-
expect(u).toContain("/auth account rm");
|
|
371
|
-
expect(u).toContain("/auth enable");
|
|
372
|
-
expect(u).toContain("/auth disable");
|
|
373
|
-
});
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
describe("parseAuthSubCommand — account-shaped verbs", () => {
|
|
377
|
-
it("/auth account add <label> defaults --from-agent to currentAgent", () => {
|
|
378
|
-
const intent = parseAuthSubCommand(["account", "add", "work-pro"], "clerk");
|
|
379
|
-
expect(intent.kind).toBe("account-add");
|
|
380
|
-
if (intent.kind === "account-add") {
|
|
381
|
-
expect(intent.account).toBe("work-pro");
|
|
382
|
-
expect(intent.fromAgent).toBe("clerk");
|
|
383
|
-
expect(intent.cliArgs).toEqual(["auth", "account", "add", "work-pro", "--from-agent", "clerk"]);
|
|
384
|
-
}
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
it("/auth account add <label> --from-agent <name> uses explicit agent", () => {
|
|
388
|
-
const intent = parseAuthSubCommand(
|
|
389
|
-
["account", "add", "work-pro", "--from-agent", "klanker"],
|
|
390
|
-
"clerk",
|
|
391
|
-
);
|
|
392
|
-
expect(intent.kind).toBe("account-add");
|
|
393
|
-
if (intent.kind === "account-add") {
|
|
394
|
-
expect(intent.fromAgent).toBe("klanker");
|
|
395
|
-
expect(intent.cliArgs).toContain("--from-agent");
|
|
396
|
-
expect(intent.cliArgs).toContain("klanker");
|
|
397
|
-
}
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
it("/auth account add without label is a usage error", () => {
|
|
401
|
-
const intent = parseAuthSubCommand(["account", "add"], "clerk");
|
|
402
|
-
expect(intent.kind).toBe("usage");
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
it("/auth account add rejects invalid labels", () => {
|
|
406
|
-
const intent = parseAuthSubCommand(["account", "add", "../etc"], "clerk");
|
|
407
|
-
expect(intent.kind).toBe("error");
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
it("/auth account add accepts dotted account labels (email-style)", () => {
|
|
411
|
-
const intent = parseAuthSubCommand(
|
|
412
|
-
["account", "add", "ken.example.com"],
|
|
413
|
-
"clerk",
|
|
414
|
-
);
|
|
415
|
-
expect(intent.kind).toBe("account-add");
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
it("/auth account add rejects --from-agent with shell metacharacters", () => {
|
|
419
|
-
const intent = parseAuthSubCommand(
|
|
420
|
-
["account", "add", "work", "--from-agent", "foo;ls"],
|
|
421
|
-
"clerk",
|
|
422
|
-
);
|
|
423
|
-
expect(intent.kind).toBe("error");
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
it("/auth account list maps to the CLI list verb", () => {
|
|
427
|
-
const intent = parseAuthSubCommand(["account", "list"], "clerk");
|
|
428
|
-
expect(intent.kind).toBe("account-list");
|
|
429
|
-
if (intent.kind === "account-list") {
|
|
430
|
-
expect(intent.cliArgs).toEqual(["auth", "account", "list"]);
|
|
431
|
-
}
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
it("/auth account rm <label>", () => {
|
|
435
|
-
const intent = parseAuthSubCommand(["account", "rm", "old-account"], "clerk");
|
|
436
|
-
expect(intent.kind).toBe("account-rm");
|
|
437
|
-
if (intent.kind === "account-rm") {
|
|
438
|
-
expect(intent.account).toBe("old-account");
|
|
439
|
-
expect(intent.cliArgs).toEqual(["auth", "account", "rm", "old-account"]);
|
|
440
|
-
}
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
it("/auth account rm without label is a usage error", () => {
|
|
444
|
-
const intent = parseAuthSubCommand(["account", "rm"], "clerk");
|
|
445
|
-
expect(intent.kind).toBe("usage");
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
it("/auth account with no subverb defaults to list", () => {
|
|
449
|
-
const intent = parseAuthSubCommand(["account"], "clerk");
|
|
450
|
-
expect(intent.kind).toBe("account-list");
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
it("/auth account rename <old> <new> produces account-rename intent with both labels", () => {
|
|
454
|
-
const intent = parseAuthSubCommand(
|
|
455
|
-
["account", "rename", "ken-pro", "work-pro"],
|
|
456
|
-
"clerk",
|
|
457
|
-
);
|
|
458
|
-
expect(intent.kind).toBe("account-rename");
|
|
459
|
-
if (intent.kind === "account-rename") {
|
|
460
|
-
expect(intent.oldAccount).toBe("ken-pro");
|
|
461
|
-
expect(intent.newAccount).toBe("work-pro");
|
|
462
|
-
expect(intent.cliArgs).toEqual([
|
|
463
|
-
"auth",
|
|
464
|
-
"account",
|
|
465
|
-
"rename",
|
|
466
|
-
"ken-pro",
|
|
467
|
-
"work-pro",
|
|
468
|
-
]);
|
|
469
|
-
}
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
it("/auth account rename without both labels is a usage error", () => {
|
|
473
|
-
expect(parseAuthSubCommand(["account", "rename"], "clerk").kind).toBe("usage");
|
|
474
|
-
expect(parseAuthSubCommand(["account", "rename", "only-one"], "clerk").kind).toBe(
|
|
475
|
-
"usage",
|
|
476
|
-
);
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
it("/auth account rename rejects same-name no-op as an error (not silent)", () => {
|
|
480
|
-
const intent = parseAuthSubCommand(
|
|
481
|
-
["account", "rename", "same", "same"],
|
|
482
|
-
"clerk",
|
|
483
|
-
);
|
|
484
|
-
expect(intent.kind).toBe("error");
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
it("/auth account rename rejects invalid labels", () => {
|
|
488
|
-
expect(
|
|
489
|
-
parseAuthSubCommand(["account", "rename", "../etc", "ok"], "clerk").kind,
|
|
490
|
-
).toBe("error");
|
|
491
|
-
expect(
|
|
492
|
-
parseAuthSubCommand(["account", "rename", "ok", "foo bar"], "clerk").kind,
|
|
493
|
-
).toBe("error");
|
|
494
|
-
});
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
describe("parseAuthSubCommand — enable / disable", () => {
|
|
498
|
-
it("/auth enable <label> defaults agents to currentAgent", () => {
|
|
499
|
-
const intent = parseAuthSubCommand(["enable", "work-pro"], "clerk");
|
|
500
|
-
expect(intent.kind).toBe("enable");
|
|
501
|
-
if (intent.kind === "enable") {
|
|
502
|
-
expect(intent.account).toBe("work-pro");
|
|
503
|
-
expect(intent.agents).toEqual(["clerk"]);
|
|
504
|
-
expect(intent.cliArgs).toEqual(["auth", "enable", "work-pro", "clerk"]);
|
|
505
|
-
expect(intent.restartAgentsAfter).toBe(true);
|
|
506
|
-
}
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
it("/auth enable <label> <a> <b> wires multiple agents", () => {
|
|
510
|
-
const intent = parseAuthSubCommand(
|
|
511
|
-
["enable", "work-pro", "foo", "bar"],
|
|
512
|
-
"clerk",
|
|
513
|
-
);
|
|
514
|
-
expect(intent.kind).toBe("enable");
|
|
515
|
-
if (intent.kind === "enable") {
|
|
516
|
-
expect(intent.agents).toEqual(["foo", "bar"]);
|
|
517
|
-
expect(intent.cliArgs).toEqual(["auth", "enable", "work-pro", "foo", "bar"]);
|
|
518
|
-
}
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
it("/auth enable without label is a usage error", () => {
|
|
522
|
-
const intent = parseAuthSubCommand(["enable"], "clerk");
|
|
523
|
-
expect(intent.kind).toBe("usage");
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
it("/auth enable rejects invalid agent names", () => {
|
|
527
|
-
const intent = parseAuthSubCommand(
|
|
528
|
-
["enable", "work-pro", "foo;ls"],
|
|
529
|
-
"clerk",
|
|
530
|
-
);
|
|
531
|
-
expect(intent.kind).toBe("error");
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
it("/auth disable <label> mirrors enable shape", () => {
|
|
535
|
-
const intent = parseAuthSubCommand(["disable", "work-pro"], "clerk");
|
|
536
|
-
expect(intent.kind).toBe("disable");
|
|
537
|
-
if (intent.kind === "disable") {
|
|
538
|
-
expect(intent.account).toBe("work-pro");
|
|
539
|
-
expect(intent.agents).toEqual(["clerk"]);
|
|
540
|
-
expect(intent.cliArgs).toEqual(["auth", "disable", "work-pro", "clerk"]);
|
|
541
|
-
}
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
it("/auth disable <label> <a> <b> for explicit agents", () => {
|
|
545
|
-
const intent = parseAuthSubCommand(
|
|
546
|
-
["disable", "work-pro", "foo", "bar"],
|
|
547
|
-
"clerk",
|
|
548
|
-
);
|
|
549
|
-
expect(intent.kind).toBe("disable");
|
|
550
|
-
if (intent.kind === "disable") {
|
|
551
|
-
expect(intent.agents).toEqual(["foo", "bar"]);
|
|
552
|
-
}
|
|
553
|
-
});
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
describe("AUTH_VERBS includes the new account-shaped verbs", () => {
|
|
557
|
-
it("exports account / enable / disable in the verb list", () => {
|
|
558
|
-
expect(AUTH_VERBS).toContain("account");
|
|
559
|
-
expect(AUTH_VERBS).toContain("enable");
|
|
560
|
-
expect(AUTH_VERBS).toContain("disable");
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
it("exports share in the verb list", () => {
|
|
564
|
-
expect(AUTH_VERBS).toContain("share");
|
|
565
|
-
});
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
describe("parseAuthSubCommand — `all` keyword for enable/disable", () => {
|
|
569
|
-
it("/auth enable <label> all parses with agents=['all']", () => {
|
|
570
|
-
const intent = parseAuthSubCommand(["enable", "work-pro", "all"], "clerk");
|
|
571
|
-
expect(intent.kind).toBe("enable");
|
|
572
|
-
if (intent.kind === "enable") {
|
|
573
|
-
expect(intent.agents).toEqual(["all"]);
|
|
574
|
-
expect(intent.cliArgs).toEqual(["auth", "enable", "work-pro", "all"]);
|
|
575
|
-
expect(intent.restartAgentsAfter).toBe(true);
|
|
576
|
-
}
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
it("/auth disable <label> all is symmetric", () => {
|
|
580
|
-
const intent = parseAuthSubCommand(["disable", "work-pro", "all"], "clerk");
|
|
581
|
-
expect(intent.kind).toBe("disable");
|
|
582
|
-
if (intent.kind === "disable") {
|
|
583
|
-
expect(intent.agents).toEqual(["all"]);
|
|
584
|
-
expect(intent.cliArgs).toEqual(["auth", "disable", "work-pro", "all"]);
|
|
585
|
-
}
|
|
586
|
-
});
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
describe("parseAuthSubCommand — /auth share", () => {
|
|
590
|
-
it("/auth share <label> defaults --from-agent to currentAgent", () => {
|
|
591
|
-
const intent = parseAuthSubCommand(["share", "work-pro"], "clerk");
|
|
592
|
-
expect(intent.kind).toBe("share");
|
|
593
|
-
if (intent.kind === "share") {
|
|
594
|
-
expect(intent.account).toBe("work-pro");
|
|
595
|
-
expect(intent.fromAgent).toBe("clerk");
|
|
596
|
-
expect(intent.cliArgs).toEqual(["auth", "share", "work-pro", "--from-agent", "clerk"]);
|
|
597
|
-
expect(intent.restartAgentsAfter).toBe(true);
|
|
598
|
-
}
|
|
599
|
-
});
|
|
600
|
-
|
|
601
|
-
it("/auth share <label> --from-agent <name> honors explicit value", () => {
|
|
602
|
-
const intent = parseAuthSubCommand(
|
|
603
|
-
["share", "work-pro", "--from-agent", "klanker"],
|
|
604
|
-
"clerk",
|
|
605
|
-
);
|
|
606
|
-
expect(intent.kind).toBe("share");
|
|
607
|
-
if (intent.kind === "share") {
|
|
608
|
-
expect(intent.fromAgent).toBe("klanker");
|
|
609
|
-
expect(intent.cliArgs).toEqual([
|
|
610
|
-
"auth", "share", "work-pro", "--from-agent", "klanker",
|
|
611
|
-
]);
|
|
612
|
-
}
|
|
613
|
-
});
|
|
614
|
-
|
|
615
|
-
it("/auth share without label is a usage error", () => {
|
|
616
|
-
const intent = parseAuthSubCommand(["share"], "clerk");
|
|
617
|
-
expect(intent.kind).toBe("usage");
|
|
618
|
-
});
|
|
619
|
-
|
|
620
|
-
it("/auth share rejects invalid labels", () => {
|
|
621
|
-
const intent = parseAuthSubCommand(["share", "../etc"], "clerk");
|
|
622
|
-
expect(intent.kind).toBe("error");
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
it("/auth share rejects --from-agent with shell metacharacters", () => {
|
|
626
|
-
const intent = parseAuthSubCommand(
|
|
627
|
-
["share", "work-pro", "--from-agent", "foo;ls"],
|
|
628
|
-
"clerk",
|
|
629
|
-
);
|
|
630
|
-
expect(intent.kind).toBe("error");
|
|
631
|
-
});
|
|
632
|
-
});
|
|
633
|
-
|
|
634
|
-
describe("usageText — `all` keyword and share verb", () => {
|
|
635
|
-
it("documents the `all` keyword on enable/disable and `/auth share`", () => {
|
|
636
|
-
const u = usageText();
|
|
637
|
-
expect(u).toContain("/auth share");
|
|
638
|
-
expect(u).toMatch(/all/);
|
|
639
|
-
});
|
|
640
|
-
});
|