@vellumai/cli 0.3.11 → 0.3.12
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 +8 -8
- package/bun.lock +6 -0
- package/package.json +5 -2
- package/src/__tests__/assistant-config.test.ts +139 -0
- package/src/__tests__/constants.test.ts +48 -0
- package/src/__tests__/health-check.test.ts +64 -0
- package/src/__tests__/random-name.test.ts +19 -0
- package/src/__tests__/retire-archive.test.ts +30 -0
- package/src/__tests__/status-emoji.test.ts +44 -0
- package/src/commands/autonomy.ts +321 -0
- package/src/commands/client.ts +15 -8
- package/src/commands/contacts.ts +265 -0
- package/src/commands/hatch.ts +93 -37
- package/src/commands/login.ts +68 -0
- package/src/commands/ps.ts +7 -3
- package/src/commands/recover.ts +1 -1
- package/src/commands/retire.ts +2 -2
- package/src/commands/skills.ts +355 -0
- package/src/commands/ssh.ts +5 -2
- package/src/components/DefaultMainScreen.tsx +67 -3
- package/src/index.ts +23 -0
- package/src/lib/assistant-config.ts +1 -0
- package/src/lib/gcp.ts +9 -13
- package/src/lib/local.ts +23 -28
- package/src/lib/platform-client.ts +74 -0
- package/src/types/sh.d.ts +4 -0
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ bun run ./src/index.ts <command> [options]
|
|
|
19
19
|
Provision a new assistant instance and bootstrap the Vellum runtime on it.
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
vellum
|
|
22
|
+
vellum hatch [species] [options]
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
#### Species
|
|
@@ -59,19 +59,19 @@ vellum-cli hatch [species] [options]
|
|
|
59
59
|
|
|
60
60
|
```bash
|
|
61
61
|
# Hatch a local assistant (default)
|
|
62
|
-
vellum
|
|
62
|
+
vellum hatch
|
|
63
63
|
|
|
64
64
|
# Hatch a vellum assistant on GCP
|
|
65
|
-
vellum
|
|
65
|
+
vellum hatch vellum --remote gcp
|
|
66
66
|
|
|
67
67
|
# Hatch an openclaw assistant on GCP in detached mode
|
|
68
|
-
vellum
|
|
68
|
+
vellum hatch openclaw --remote gcp -d
|
|
69
69
|
|
|
70
70
|
# Hatch with a specific instance name
|
|
71
|
-
vellum
|
|
71
|
+
vellum hatch --name my-assistant --remote gcp
|
|
72
72
|
|
|
73
73
|
# Hatch on a custom SSH host
|
|
74
|
-
VELLUM_CUSTOM_HOST=user@10.0.0.1 vellum
|
|
74
|
+
VELLUM_CUSTOM_HOST=user@10.0.0.1 vellum hatch --remote custom
|
|
75
75
|
```
|
|
76
76
|
|
|
77
77
|
When hatching on GCP in interactive mode (without `-d`), the CLI displays an animated progress TUI that polls the instance's startup script output in real time. Press `Ctrl+C` to detach -- the instance will continue running in the background.
|
|
@@ -81,7 +81,7 @@ When hatching on GCP in interactive mode (without `-d`), the CLI displays an ani
|
|
|
81
81
|
Delete a provisioned assistant instance. The cloud provider and connection details are automatically resolved from the saved assistant config (written during `hatch`).
|
|
82
82
|
|
|
83
83
|
```bash
|
|
84
|
-
vellum
|
|
84
|
+
vellum retire <name>
|
|
85
85
|
```
|
|
86
86
|
|
|
87
87
|
The CLI looks up the instance by name in `~/.vellum.lock.json` and determines how to retire it based on the saved `cloud` field:
|
|
@@ -95,5 +95,5 @@ The CLI looks up the instance by name in `~/.vellum.lock.json` and determines ho
|
|
|
95
95
|
|
|
96
96
|
```bash
|
|
97
97
|
# Retire an instance (cloud type resolved from config)
|
|
98
|
-
vellum
|
|
98
|
+
vellum retire my-assistant
|
|
99
99
|
```
|
package/bun.lock
CHANGED
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
"name": "@vellumai/cli",
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"ink": "^6.7.0",
|
|
9
|
+
"qrcode-terminal": "^0.12.0",
|
|
9
10
|
"react": "^19.2.4",
|
|
10
11
|
"react-devtools-core": "^6.1.2",
|
|
11
12
|
},
|
|
12
13
|
"devDependencies": {
|
|
13
14
|
"@types/bun": "^1.2.4",
|
|
15
|
+
"@types/qrcode-terminal": "^0.12.2",
|
|
14
16
|
"@types/react": "^19.2.14",
|
|
15
17
|
"eslint": "^10.0.0",
|
|
16
18
|
"typescript": "^5.9.3",
|
|
@@ -53,6 +55,8 @@
|
|
|
53
55
|
|
|
54
56
|
"@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="],
|
|
55
57
|
|
|
58
|
+
"@types/qrcode-terminal": ["@types/qrcode-terminal@0.12.2", "", {}, "sha512-v+RcIEJ+Uhd6ygSQ0u5YYY7ZM+la7GgPbs0V/7l/kFs2uO4S8BcIUEMoP7za4DNIqNnUD5npf0A/7kBhrCKG5Q=="],
|
|
59
|
+
|
|
56
60
|
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
|
|
57
61
|
|
|
58
62
|
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.56.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/type-utils": "8.56.0", "@typescript-eslint/utils": "8.56.0", "@typescript-eslint/visitor-keys": "8.56.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.56.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw=="],
|
|
@@ -217,6 +221,8 @@
|
|
|
217
221
|
|
|
218
222
|
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
|
219
223
|
|
|
224
|
+
"qrcode-terminal": ["qrcode-terminal@0.12.0", "", { "bin": { "qrcode-terminal": "./bin/qrcode-terminal.js" } }, "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ=="],
|
|
225
|
+
|
|
220
226
|
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
|
|
221
227
|
|
|
222
228
|
"react-devtools-core": ["react-devtools-core@6.1.5", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA=="],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vellumai/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.12",
|
|
4
4
|
"description": "CLI tools for vellum-assistant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -11,21 +11,24 @@
|
|
|
11
11
|
"./src/commands/*": "./src/commands/*.ts"
|
|
12
12
|
},
|
|
13
13
|
"bin": {
|
|
14
|
-
"vellum
|
|
14
|
+
"vellum": "./src/index.ts"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
17
|
"lint": "eslint",
|
|
18
|
+
"test": "bun test",
|
|
18
19
|
"typecheck": "bunx tsc --noEmit"
|
|
19
20
|
},
|
|
20
21
|
"author": "Vellum AI",
|
|
21
22
|
"license": "MIT",
|
|
22
23
|
"dependencies": {
|
|
23
24
|
"ink": "^6.7.0",
|
|
25
|
+
"qrcode-terminal": "^0.12.0",
|
|
24
26
|
"react": "^19.2.4",
|
|
25
27
|
"react-devtools-core": "^6.1.2"
|
|
26
28
|
},
|
|
27
29
|
"devDependencies": {
|
|
28
30
|
"@types/bun": "^1.2.4",
|
|
31
|
+
"@types/qrcode-terminal": "^0.12.2",
|
|
29
32
|
"@types/react": "^19.2.14",
|
|
30
33
|
"eslint": "^10.0.0",
|
|
31
34
|
"typescript": "^5.9.3",
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterAll } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
// Point lockfile operations at a temp directory
|
|
7
|
+
const testDir = mkdtempSync(join(tmpdir(), "cli-assistant-config-test-"));
|
|
8
|
+
process.env.BASE_DATA_DIR = testDir;
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
loadLatestAssistant,
|
|
12
|
+
findAssistantByName,
|
|
13
|
+
removeAssistantEntry,
|
|
14
|
+
loadAllAssistants,
|
|
15
|
+
saveAssistantEntry,
|
|
16
|
+
type AssistantEntry,
|
|
17
|
+
} from "../lib/assistant-config.js";
|
|
18
|
+
|
|
19
|
+
afterAll(() => {
|
|
20
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
21
|
+
delete process.env.BASE_DATA_DIR;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
function writeLockfile(data: unknown): void {
|
|
25
|
+
writeFileSync(
|
|
26
|
+
join(testDir, ".vellum.lock.json"),
|
|
27
|
+
JSON.stringify(data, null, 2),
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const makeEntry = (id: string, runtimeUrl = "http://localhost:7821", extra?: Partial<AssistantEntry>): AssistantEntry => ({
|
|
32
|
+
assistantId: id,
|
|
33
|
+
runtimeUrl,
|
|
34
|
+
cloud: "local",
|
|
35
|
+
...extra,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("assistant-config", () => {
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
// Reset lockfile between tests
|
|
41
|
+
try {
|
|
42
|
+
rmSync(join(testDir, ".vellum.lock.json"));
|
|
43
|
+
} catch {
|
|
44
|
+
// file may not exist
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
rmSync(join(testDir, ".vellum.lockfile.json"));
|
|
48
|
+
} catch {
|
|
49
|
+
// file may not exist
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("loadAllAssistants returns empty array when no lockfile exists", () => {
|
|
54
|
+
expect(loadAllAssistants()).toEqual([]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("loadAllAssistants returns empty array for malformed lockfile", () => {
|
|
58
|
+
writeFileSync(join(testDir, ".vellum.lock.json"), "not json");
|
|
59
|
+
expect(loadAllAssistants()).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("loadAllAssistants returns empty array when assistants key is missing", () => {
|
|
63
|
+
writeLockfile({ someOtherKey: true });
|
|
64
|
+
expect(loadAllAssistants()).toEqual([]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("saveAssistantEntry and loadAllAssistants round-trip", () => {
|
|
68
|
+
const entry = makeEntry("test-1");
|
|
69
|
+
saveAssistantEntry(entry);
|
|
70
|
+
const all = loadAllAssistants();
|
|
71
|
+
expect(all).toHaveLength(1);
|
|
72
|
+
expect(all[0].assistantId).toBe("test-1");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("findAssistantByName returns matching entry", () => {
|
|
76
|
+
writeLockfile({
|
|
77
|
+
assistants: [makeEntry("alpha"), makeEntry("beta")],
|
|
78
|
+
});
|
|
79
|
+
const result = findAssistantByName("beta");
|
|
80
|
+
expect(result).not.toBeNull();
|
|
81
|
+
expect(result!.assistantId).toBe("beta");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("findAssistantByName returns null for non-existent name", () => {
|
|
85
|
+
writeLockfile({ assistants: [makeEntry("alpha")] });
|
|
86
|
+
expect(findAssistantByName("missing")).toBeNull();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("removeAssistantEntry removes matching entry", () => {
|
|
90
|
+
writeLockfile({
|
|
91
|
+
assistants: [makeEntry("a"), makeEntry("b"), makeEntry("c")],
|
|
92
|
+
});
|
|
93
|
+
removeAssistantEntry("b");
|
|
94
|
+
const all = loadAllAssistants();
|
|
95
|
+
expect(all).toHaveLength(2);
|
|
96
|
+
expect(all.map((e) => e.assistantId)).toEqual(["a", "c"]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("loadLatestAssistant returns null when empty", () => {
|
|
100
|
+
expect(loadLatestAssistant()).toBeNull();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("loadLatestAssistant returns most recently hatched entry", () => {
|
|
104
|
+
writeLockfile({
|
|
105
|
+
assistants: [
|
|
106
|
+
makeEntry("old", "http://localhost:7821", { hatchedAt: "2024-01-01T00:00:00Z" }),
|
|
107
|
+
makeEntry("new", "http://localhost:7822", { hatchedAt: "2025-06-15T00:00:00Z" }),
|
|
108
|
+
makeEntry("mid", "http://localhost:7823", { hatchedAt: "2024-06-15T00:00:00Z" }),
|
|
109
|
+
],
|
|
110
|
+
});
|
|
111
|
+
const latest = loadLatestAssistant();
|
|
112
|
+
expect(latest).not.toBeNull();
|
|
113
|
+
expect(latest!.assistantId).toBe("new");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("loadLatestAssistant handles entries without hatchedAt", () => {
|
|
117
|
+
writeLockfile({
|
|
118
|
+
assistants: [
|
|
119
|
+
makeEntry("no-date"),
|
|
120
|
+
makeEntry("with-date", "http://localhost:7822", { hatchedAt: "2025-01-01T00:00:00Z" }),
|
|
121
|
+
],
|
|
122
|
+
});
|
|
123
|
+
const latest = loadLatestAssistant();
|
|
124
|
+
expect(latest!.assistantId).toBe("with-date");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("loadAllAssistants filters out entries missing required fields", () => {
|
|
128
|
+
writeLockfile({
|
|
129
|
+
assistants: [
|
|
130
|
+
makeEntry("valid"),
|
|
131
|
+
{ cloud: "local" }, // missing assistantId and runtimeUrl
|
|
132
|
+
{ assistantId: "no-url", cloud: "local" }, // missing runtimeUrl
|
|
133
|
+
],
|
|
134
|
+
});
|
|
135
|
+
const all = loadAllAssistants();
|
|
136
|
+
expect(all).toHaveLength(1);
|
|
137
|
+
expect(all[0].assistantId).toBe("valid");
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
FIREWALL_TAG,
|
|
4
|
+
GATEWAY_PORT,
|
|
5
|
+
VALID_REMOTE_HOSTS,
|
|
6
|
+
VALID_SPECIES,
|
|
7
|
+
SPECIES_CONFIG,
|
|
8
|
+
} from "../lib/constants.js";
|
|
9
|
+
|
|
10
|
+
describe("constants", () => {
|
|
11
|
+
test("FIREWALL_TAG is a non-empty string", () => {
|
|
12
|
+
expect(typeof FIREWALL_TAG).toBe("string");
|
|
13
|
+
expect(FIREWALL_TAG.length).toBeGreaterThan(0);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("GATEWAY_PORT is a valid port number", () => {
|
|
17
|
+
expect(typeof GATEWAY_PORT).toBe("number");
|
|
18
|
+
expect(GATEWAY_PORT).toBeGreaterThan(0);
|
|
19
|
+
expect(GATEWAY_PORT).toBeLessThan(65536);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("VALID_REMOTE_HOSTS includes expected hosts", () => {
|
|
23
|
+
expect(VALID_REMOTE_HOSTS).toContain("local");
|
|
24
|
+
expect(VALID_REMOTE_HOSTS).toContain("gcp");
|
|
25
|
+
expect(VALID_REMOTE_HOSTS).toContain("aws");
|
|
26
|
+
expect(VALID_REMOTE_HOSTS).toContain("custom");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("VALID_SPECIES includes expected species", () => {
|
|
30
|
+
expect(VALID_SPECIES).toContain("openclaw");
|
|
31
|
+
expect(VALID_SPECIES).toContain("vellum");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("SPECIES_CONFIG has entries for all valid species", () => {
|
|
35
|
+
for (const species of VALID_SPECIES) {
|
|
36
|
+
const config = SPECIES_CONFIG[species];
|
|
37
|
+
expect(config).toBeDefined();
|
|
38
|
+
expect(typeof config.color).toBe("string");
|
|
39
|
+
expect(Array.isArray(config.art)).toBe(true);
|
|
40
|
+
expect(config.art.length).toBeGreaterThan(0);
|
|
41
|
+
expect(typeof config.hatchedEmoji).toBe("string");
|
|
42
|
+
expect(Array.isArray(config.waitingMessages)).toBe(true);
|
|
43
|
+
expect(config.waitingMessages.length).toBeGreaterThan(0);
|
|
44
|
+
expect(Array.isArray(config.runningMessages)).toBe(true);
|
|
45
|
+
expect(config.runningMessages.length).toBeGreaterThan(0);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { checkHealth, HEALTH_CHECK_TIMEOUT_MS } from "../lib/health-check.js";
|
|
3
|
+
|
|
4
|
+
describe("checkHealth", () => {
|
|
5
|
+
test("HEALTH_CHECK_TIMEOUT_MS is a positive number", () => {
|
|
6
|
+
expect(typeof HEALTH_CHECK_TIMEOUT_MS).toBe("number");
|
|
7
|
+
expect(HEALTH_CHECK_TIMEOUT_MS).toBeGreaterThan(0);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("returns unreachable for non-existent host", async () => {
|
|
11
|
+
const result = await checkHealth("http://127.0.0.1:1");
|
|
12
|
+
expect(["unreachable", "timeout"]).toContain(result.status);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("returns healthy for a mock healthy endpoint", async () => {
|
|
16
|
+
const server = Bun.serve({
|
|
17
|
+
port: 0,
|
|
18
|
+
fetch() {
|
|
19
|
+
return Response.json({ status: "healthy" });
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const result = await checkHealth(`http://localhost:${server.port}`);
|
|
25
|
+
expect(result.status).toBe("healthy");
|
|
26
|
+
expect(result.detail).toBeNull();
|
|
27
|
+
} finally {
|
|
28
|
+
server.stop(true);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("returns status with detail for non-healthy response", async () => {
|
|
33
|
+
const server = Bun.serve({
|
|
34
|
+
port: 0,
|
|
35
|
+
fetch() {
|
|
36
|
+
return Response.json({ status: "degraded", message: "high latency" });
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const result = await checkHealth(`http://localhost:${server.port}`);
|
|
42
|
+
expect(result.status).toBe("degraded");
|
|
43
|
+
expect(result.detail).toBe("high latency");
|
|
44
|
+
} finally {
|
|
45
|
+
server.stop(true);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("returns error status for non-ok HTTP response", async () => {
|
|
50
|
+
const server = Bun.serve({
|
|
51
|
+
port: 0,
|
|
52
|
+
fetch() {
|
|
53
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const result = await checkHealth(`http://localhost:${server.port}`);
|
|
59
|
+
expect(result.status).toBe("error (500)");
|
|
60
|
+
} finally {
|
|
61
|
+
server.stop(true);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { generateRandomSuffix } from "../lib/random-name.js";
|
|
3
|
+
|
|
4
|
+
describe("generateRandomSuffix", () => {
|
|
5
|
+
test("returns a string in adjective-noun format", () => {
|
|
6
|
+
const result = generateRandomSuffix();
|
|
7
|
+
expect(result).toMatch(/^[a-z]+-[a-z]+$/);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("produces varying results across multiple calls", () => {
|
|
11
|
+
const results = new Set<string>();
|
|
12
|
+
for (let i = 0; i < 20; i++) {
|
|
13
|
+
results.add(generateRandomSuffix());
|
|
14
|
+
}
|
|
15
|
+
// With 62 adjectives * 62 nouns = 3844 combos, 20 calls should produce
|
|
16
|
+
// at least a few unique values
|
|
17
|
+
expect(results.size).toBeGreaterThan(1);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { validateAssistantName } from "../lib/retire-archive.js";
|
|
3
|
+
|
|
4
|
+
describe("validateAssistantName", () => {
|
|
5
|
+
test("accepts valid names", () => {
|
|
6
|
+
expect(() => validateAssistantName("my-assistant")).not.toThrow();
|
|
7
|
+
expect(() => validateAssistantName("test123")).not.toThrow();
|
|
8
|
+
expect(() => validateAssistantName("a")).not.toThrow();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("rejects empty string", () => {
|
|
12
|
+
expect(() => validateAssistantName("")).toThrow("Invalid assistant name");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("rejects names with forward slashes", () => {
|
|
16
|
+
expect(() => validateAssistantName("foo/bar")).toThrow("Invalid assistant name");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("rejects names with backslashes", () => {
|
|
20
|
+
expect(() => validateAssistantName("foo\\bar")).toThrow("Invalid assistant name");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("rejects dot-dot traversal", () => {
|
|
24
|
+
expect(() => validateAssistantName("..")).toThrow("Invalid assistant name");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("rejects single dot", () => {
|
|
28
|
+
expect(() => validateAssistantName(".")).toThrow("Invalid assistant name");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { statusEmoji, withStatusEmoji } from "../lib/status-emoji.js";
|
|
3
|
+
|
|
4
|
+
describe("statusEmoji", () => {
|
|
5
|
+
test("returns green for running/healthy/ok statuses", () => {
|
|
6
|
+
expect(statusEmoji("running")).toBe("\u{1F7E2}");
|
|
7
|
+
expect(statusEmoji("healthy")).toBe("\u{1F7E2}");
|
|
8
|
+
expect(statusEmoji("ok")).toBe("\u{1F7E2}");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("returns green for statuses starting with 'up '", () => {
|
|
12
|
+
expect(statusEmoji("up 5 minutes")).toBe("\u{1F7E2}");
|
|
13
|
+
expect(statusEmoji("up 1d")).toBe("\u{1F7E2}");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("returns red for error/unreachable/exited statuses", () => {
|
|
17
|
+
expect(statusEmoji("error")).toBe("\u{1F534}");
|
|
18
|
+
expect(statusEmoji("error (500)")).toBe("\u{1F534}");
|
|
19
|
+
expect(statusEmoji("unreachable")).toBe("\u{1F534}");
|
|
20
|
+
expect(statusEmoji("exited")).toBe("\u{1F534}");
|
|
21
|
+
expect(statusEmoji("exited (1)")).toBe("\u{1F534}");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("returns yellow for unknown statuses", () => {
|
|
25
|
+
expect(statusEmoji("starting")).toBe("\u{1F7E1}");
|
|
26
|
+
expect(statusEmoji("pending")).toBe("\u{1F7E1}");
|
|
27
|
+
expect(statusEmoji("unknown")).toBe("\u{1F7E1}");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("is case-insensitive", () => {
|
|
31
|
+
expect(statusEmoji("Running")).toBe("\u{1F7E2}");
|
|
32
|
+
expect(statusEmoji("HEALTHY")).toBe("\u{1F7E2}");
|
|
33
|
+
expect(statusEmoji("ERROR")).toBe("\u{1F534}");
|
|
34
|
+
expect(statusEmoji("Unreachable")).toBe("\u{1F534}");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("withStatusEmoji", () => {
|
|
39
|
+
test("prepends emoji to status string", () => {
|
|
40
|
+
expect(withStatusEmoji("running")).toBe("\u{1F7E2} running");
|
|
41
|
+
expect(withStatusEmoji("error")).toBe("\u{1F534} error");
|
|
42
|
+
expect(withStatusEmoji("pending")).toBe("\u{1F7E1} pending");
|
|
43
|
+
});
|
|
44
|
+
});
|