@openparachute/hub 0.3.0-rc.1
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/LICENSE +661 -0
- package/README.md +284 -0
- package/package.json +31 -0
- package/src/__tests__/auth.test.ts +101 -0
- package/src/__tests__/auto-wire.test.ts +283 -0
- package/src/__tests__/cli.test.ts +192 -0
- package/src/__tests__/cloudflare-config.test.ts +54 -0
- package/src/__tests__/cloudflare-detect.test.ts +68 -0
- package/src/__tests__/cloudflare-state.test.ts +92 -0
- package/src/__tests__/cloudflare-tunnel.test.ts +207 -0
- package/src/__tests__/config.test.ts +18 -0
- package/src/__tests__/env-file.test.ts +125 -0
- package/src/__tests__/expose-auth-preflight.test.ts +201 -0
- package/src/__tests__/expose-cloudflare.test.ts +484 -0
- package/src/__tests__/expose-interactive.test.ts +703 -0
- package/src/__tests__/expose-last-provider.test.ts +113 -0
- package/src/__tests__/expose-off-auto.test.ts +269 -0
- package/src/__tests__/expose-state.test.ts +101 -0
- package/src/__tests__/expose.test.ts +1581 -0
- package/src/__tests__/hub-control.test.ts +346 -0
- package/src/__tests__/hub-server.test.ts +157 -0
- package/src/__tests__/hub.test.ts +116 -0
- package/src/__tests__/install.test.ts +1145 -0
- package/src/__tests__/lifecycle.test.ts +608 -0
- package/src/__tests__/migrate.test.ts +422 -0
- package/src/__tests__/notes-serve.test.ts +135 -0
- package/src/__tests__/port-assign.test.ts +178 -0
- package/src/__tests__/process-state.test.ts +140 -0
- package/src/__tests__/scribe-config.test.ts +193 -0
- package/src/__tests__/scribe-provider-interactive.test.ts +361 -0
- package/src/__tests__/services-manifest.test.ts +177 -0
- package/src/__tests__/status.test.ts +347 -0
- package/src/__tests__/tailscale-commands.test.ts +111 -0
- package/src/__tests__/tailscale-detect.test.ts +64 -0
- package/src/__tests__/vault-auth-status.test.ts +164 -0
- package/src/__tests__/vault-tokens-create-interactive.test.ts +183 -0
- package/src/__tests__/well-known.test.ts +214 -0
- package/src/auto-wire.ts +184 -0
- package/src/cli.ts +482 -0
- package/src/cloudflare/config.ts +58 -0
- package/src/cloudflare/detect.ts +58 -0
- package/src/cloudflare/state.ts +96 -0
- package/src/cloudflare/tunnel.ts +135 -0
- package/src/commands/auth.ts +69 -0
- package/src/commands/expose-auth-preflight.ts +217 -0
- package/src/commands/expose-cloudflare.ts +329 -0
- package/src/commands/expose-interactive.ts +428 -0
- package/src/commands/expose-off-auto.ts +199 -0
- package/src/commands/expose.ts +522 -0
- package/src/commands/install.ts +422 -0
- package/src/commands/lifecycle.ts +324 -0
- package/src/commands/migrate.ts +253 -0
- package/src/commands/scribe-provider-interactive.ts +269 -0
- package/src/commands/status.ts +238 -0
- package/src/commands/vault-tokens-create-interactive.ts +137 -0
- package/src/commands/vault.ts +17 -0
- package/src/config.ts +16 -0
- package/src/env-file.ts +76 -0
- package/src/expose-last-provider.ts +71 -0
- package/src/expose-state.ts +125 -0
- package/src/help.ts +279 -0
- package/src/hub-control.ts +254 -0
- package/src/hub-origin.ts +44 -0
- package/src/hub-server.ts +113 -0
- package/src/hub.ts +674 -0
- package/src/notes-serve.ts +135 -0
- package/src/port-assign.ts +125 -0
- package/src/process-state.ts +111 -0
- package/src/scribe-config.ts +149 -0
- package/src/service-spec.ts +296 -0
- package/src/services-manifest.ts +171 -0
- package/src/tailscale/commands.ts +41 -0
- package/src/tailscale/detect.ts +107 -0
- package/src/tailscale/run.ts +28 -0
- package/src/vault/auth-status.ts +179 -0
- package/src/well-known.ts +127 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { assignPort, assignServicePort } from "../port-assign.ts";
|
|
6
|
+
import { CANONICAL_PORT_MAX, CANONICAL_PORT_MIN } from "../service-spec.ts";
|
|
7
|
+
|
|
8
|
+
function makeTempDir(): { dir: string; cleanup: () => void } {
|
|
9
|
+
const dir = mkdtempSync(join(tmpdir(), "pcli-port-assign-"));
|
|
10
|
+
return { dir, cleanup: () => rmSync(dir, { recursive: true, force: true }) };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("assignPort (pure)", () => {
|
|
14
|
+
test("returns the canonical slot when free", () => {
|
|
15
|
+
const result = assignPort(1940, []);
|
|
16
|
+
expect(result.port).toBe(1940);
|
|
17
|
+
expect(result.source).toBe("canonical");
|
|
18
|
+
expect(result.warning).toBeUndefined();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("returns canonical even when other unrelated ports are taken", () => {
|
|
22
|
+
const result = assignPort(1940, [1939, 1942, 1943, 5173]);
|
|
23
|
+
expect(result.port).toBe(1940);
|
|
24
|
+
expect(result.source).toBe("canonical");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("walks the unassigned reservation range when canonical is occupied", () => {
|
|
28
|
+
// 1940 is taken; canonical reserved range starts at 1944 → first hit.
|
|
29
|
+
const result = assignPort(1940, [1940]);
|
|
30
|
+
expect(result.port).toBe(1944);
|
|
31
|
+
expect(result.source).toBe("fallback-in-range");
|
|
32
|
+
expect(result.warning).toMatch(/canonical port 1940 is in use/);
|
|
33
|
+
expect(result.warning).toMatch(/1944/);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("skips reservations that are also occupied", () => {
|
|
37
|
+
// Canonical 1940 + the first three reserved slots are all in use.
|
|
38
|
+
const result = assignPort(1940, [1940, 1944, 1945, 1946]);
|
|
39
|
+
expect(result.port).toBe(1947);
|
|
40
|
+
expect(result.source).toBe("fallback-in-range");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("falls outside the range with a warning when reservations are exhausted", () => {
|
|
44
|
+
const occupied = [];
|
|
45
|
+
for (let p = CANONICAL_PORT_MIN; p <= CANONICAL_PORT_MAX; p++) occupied.push(p);
|
|
46
|
+
const result = assignPort(1940, occupied);
|
|
47
|
+
expect(result.port).toBe(CANONICAL_PORT_MAX + 1);
|
|
48
|
+
expect(result.source).toBe("fallback-out-of-range");
|
|
49
|
+
expect(result.warning).toMatch(/canonical range/);
|
|
50
|
+
expect(result.warning).toMatch(/1950/);
|
|
51
|
+
expect(result.warning).toMatch(/may conflict/);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("walks past out-of-range collisions too", () => {
|
|
55
|
+
const occupied = [];
|
|
56
|
+
for (let p = CANONICAL_PORT_MIN; p <= CANONICAL_PORT_MAX + 2; p++) occupied.push(p);
|
|
57
|
+
const result = assignPort(1940, occupied);
|
|
58
|
+
expect(result.port).toBe(CANONICAL_PORT_MAX + 3);
|
|
59
|
+
expect(result.source).toBe("fallback-out-of-range");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("third-party (no canonical slot) jumps straight to the reservation range", () => {
|
|
63
|
+
const result = assignPort(undefined, []);
|
|
64
|
+
expect(result.port).toBe(1944);
|
|
65
|
+
expect(result.source).toBe("fallback-in-range");
|
|
66
|
+
expect(result.warning).toMatch(/no canonical slot/);
|
|
67
|
+
expect(result.warning).toMatch(/1944/);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("third-party with reservations occupied walks further in the range", () => {
|
|
71
|
+
const result = assignPort(undefined, [1944, 1945]);
|
|
72
|
+
expect(result.port).toBe(1946);
|
|
73
|
+
expect(result.source).toBe("fallback-in-range");
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("assignServicePort (.env round-trip)", () => {
|
|
78
|
+
test("preserves an existing PORT in .env (idempotent re-install)", () => {
|
|
79
|
+
const { dir, cleanup } = makeTempDir();
|
|
80
|
+
try {
|
|
81
|
+
const envPath = join(dir, ".env");
|
|
82
|
+
writeFileSync(envPath, "PORT=1944\nOTHER=keepme\n");
|
|
83
|
+
const result = assignServicePort({
|
|
84
|
+
envPath,
|
|
85
|
+
canonical: 1940,
|
|
86
|
+
// Even though canonical is free, the existing .env wins.
|
|
87
|
+
occupied: [],
|
|
88
|
+
});
|
|
89
|
+
expect(result.port).toBe(1944);
|
|
90
|
+
expect(result.source).toBe("preserved");
|
|
91
|
+
expect(result.written).toBe(false);
|
|
92
|
+
// File untouched — no rewrite means OTHER stays as-is.
|
|
93
|
+
const text = readFileSync(envPath, "utf8");
|
|
94
|
+
expect(text).toContain("PORT=1944");
|
|
95
|
+
expect(text).toContain("OTHER=keepme");
|
|
96
|
+
} finally {
|
|
97
|
+
cleanup();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("writes PORT into a fresh .env when canonical is free", () => {
|
|
102
|
+
const { dir, cleanup } = makeTempDir();
|
|
103
|
+
try {
|
|
104
|
+
const envPath = join(dir, "subdir", ".env");
|
|
105
|
+
const result = assignServicePort({
|
|
106
|
+
envPath,
|
|
107
|
+
canonical: 1940,
|
|
108
|
+
occupied: [],
|
|
109
|
+
});
|
|
110
|
+
expect(result.port).toBe(1940);
|
|
111
|
+
expect(result.source).toBe("canonical");
|
|
112
|
+
expect(result.written).toBe(true);
|
|
113
|
+
expect(result.warning).toBeUndefined();
|
|
114
|
+
expect(existsSync(envPath)).toBe(true);
|
|
115
|
+
expect(readFileSync(envPath, "utf8")).toContain("PORT=1940");
|
|
116
|
+
} finally {
|
|
117
|
+
cleanup();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("writes a fallback PORT and surfaces the warning when canonical is occupied", () => {
|
|
122
|
+
const { dir, cleanup } = makeTempDir();
|
|
123
|
+
try {
|
|
124
|
+
const envPath = join(dir, ".env");
|
|
125
|
+
const result = assignServicePort({
|
|
126
|
+
envPath,
|
|
127
|
+
canonical: 1940,
|
|
128
|
+
occupied: [1940],
|
|
129
|
+
});
|
|
130
|
+
expect(result.port).toBe(1944);
|
|
131
|
+
expect(result.source).toBe("fallback-in-range");
|
|
132
|
+
expect(result.written).toBe(true);
|
|
133
|
+
expect(result.warning).toMatch(/canonical port 1940 is in use/);
|
|
134
|
+
expect(readFileSync(envPath, "utf8")).toContain("PORT=1944");
|
|
135
|
+
} finally {
|
|
136
|
+
cleanup();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("ignores a non-numeric PORT and assigns a fresh one", () => {
|
|
141
|
+
const { dir, cleanup } = makeTempDir();
|
|
142
|
+
try {
|
|
143
|
+
const envPath = join(dir, ".env");
|
|
144
|
+
writeFileSync(envPath, "PORT=garbage\n");
|
|
145
|
+
const result = assignServicePort({
|
|
146
|
+
envPath,
|
|
147
|
+
canonical: 1940,
|
|
148
|
+
occupied: [],
|
|
149
|
+
});
|
|
150
|
+
expect(result.port).toBe(1940);
|
|
151
|
+
expect(result.written).toBe(true);
|
|
152
|
+
// The garbage value got upserted to a real number.
|
|
153
|
+
expect(readFileSync(envPath, "utf8")).toContain("PORT=1940");
|
|
154
|
+
} finally {
|
|
155
|
+
cleanup();
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("preserves surrounding lines on rewrite", () => {
|
|
160
|
+
const { dir, cleanup } = makeTempDir();
|
|
161
|
+
try {
|
|
162
|
+
const envPath = join(dir, ".env");
|
|
163
|
+
writeFileSync(envPath, "FOO=bar\nBAZ=qux\n");
|
|
164
|
+
const result = assignServicePort({
|
|
165
|
+
envPath,
|
|
166
|
+
canonical: 1940,
|
|
167
|
+
occupied: [],
|
|
168
|
+
});
|
|
169
|
+
expect(result.written).toBe(true);
|
|
170
|
+
const text = readFileSync(envPath, "utf8");
|
|
171
|
+
expect(text).toContain("FOO=bar");
|
|
172
|
+
expect(text).toContain("BAZ=qux");
|
|
173
|
+
expect(text).toContain("PORT=1940");
|
|
174
|
+
} finally {
|
|
175
|
+
cleanup();
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
clearPid,
|
|
7
|
+
defaultAlive,
|
|
8
|
+
ensureLogPath,
|
|
9
|
+
formatUptime,
|
|
10
|
+
logPath,
|
|
11
|
+
pidPath,
|
|
12
|
+
processState,
|
|
13
|
+
readPid,
|
|
14
|
+
writePid,
|
|
15
|
+
} from "../process-state.ts";
|
|
16
|
+
|
|
17
|
+
function makeTempConfig(): { dir: string; cleanup: () => void } {
|
|
18
|
+
const dir = mkdtempSync(join(tmpdir(), "pcli-proc-"));
|
|
19
|
+
return { dir, cleanup: () => rmSync(dir, { recursive: true, force: true }) };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("process-state paths", () => {
|
|
23
|
+
test("pidPath / logPath land under <configDir>/<svc>/{run,logs}", () => {
|
|
24
|
+
expect(pidPath("vault", "/cfg")).toBe("/cfg/vault/run/vault.pid");
|
|
25
|
+
expect(logPath("notes", "/cfg")).toBe("/cfg/notes/logs/notes.log");
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("writePid / readPid / clearPid", () => {
|
|
30
|
+
test("round-trips through the filesystem", () => {
|
|
31
|
+
const { dir, cleanup } = makeTempConfig();
|
|
32
|
+
try {
|
|
33
|
+
expect(readPid("vault", dir)).toBeUndefined();
|
|
34
|
+
writePid("vault", 12345, dir);
|
|
35
|
+
expect(readPid("vault", dir)).toBe(12345);
|
|
36
|
+
clearPid("vault", dir);
|
|
37
|
+
expect(readPid("vault", dir)).toBeUndefined();
|
|
38
|
+
} finally {
|
|
39
|
+
cleanup();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("readPid ignores garbage pid files", () => {
|
|
44
|
+
const { dir, cleanup } = makeTempConfig();
|
|
45
|
+
try {
|
|
46
|
+
const p = pidPath("vault", dir);
|
|
47
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
48
|
+
writeFileSync(p, "not-a-number\n");
|
|
49
|
+
expect(readPid("vault", dir)).toBeUndefined();
|
|
50
|
+
} finally {
|
|
51
|
+
cleanup();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("processState", () => {
|
|
57
|
+
test("no pid file → unknown (externally-managed is possible)", () => {
|
|
58
|
+
const { dir, cleanup } = makeTempConfig();
|
|
59
|
+
try {
|
|
60
|
+
expect(processState("vault", dir).status).toBe("unknown");
|
|
61
|
+
} finally {
|
|
62
|
+
cleanup();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("pid file + alive pid → running with pid + startedAt", () => {
|
|
67
|
+
const { dir, cleanup } = makeTempConfig();
|
|
68
|
+
try {
|
|
69
|
+
writePid("vault", 4242, dir);
|
|
70
|
+
const state = processState("vault", dir, () => true);
|
|
71
|
+
expect(state.status).toBe("running");
|
|
72
|
+
expect(state.pid).toBe(4242);
|
|
73
|
+
expect(state.startedAt).toBeInstanceOf(Date);
|
|
74
|
+
} finally {
|
|
75
|
+
cleanup();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("pid file + dead pid → stopped (known-dead, not unknown)", () => {
|
|
80
|
+
const { dir, cleanup } = makeTempConfig();
|
|
81
|
+
try {
|
|
82
|
+
writePid("vault", 4242, dir);
|
|
83
|
+
const state = processState("vault", dir, () => false);
|
|
84
|
+
expect(state.status).toBe("stopped");
|
|
85
|
+
expect(state.pid).toBe(4242);
|
|
86
|
+
} finally {
|
|
87
|
+
cleanup();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("defaultAlive", () => {
|
|
93
|
+
test("current process is alive", () => {
|
|
94
|
+
expect(defaultAlive(process.pid)).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("absurd pid is not alive", () => {
|
|
98
|
+
// PIDs above 2^22 don't exist on mainstream OSes.
|
|
99
|
+
expect(defaultAlive(99_999_999)).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("formatUptime", () => {
|
|
104
|
+
test("sub-minute → seconds", () => {
|
|
105
|
+
const now = new Date("2026-04-19T12:00:45Z");
|
|
106
|
+
const start = new Date("2026-04-19T12:00:00Z");
|
|
107
|
+
expect(formatUptime(start, now)).toBe("45s");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("sub-hour → minutes", () => {
|
|
111
|
+
const now = new Date("2026-04-19T12:13:00Z");
|
|
112
|
+
const start = new Date("2026-04-19T12:00:00Z");
|
|
113
|
+
expect(formatUptime(start, now)).toBe("13m");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("sub-day → h+m", () => {
|
|
117
|
+
const now = new Date("2026-04-19T14:13:00Z");
|
|
118
|
+
const start = new Date("2026-04-19T12:00:00Z");
|
|
119
|
+
expect(formatUptime(start, now)).toBe("2h 13m");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("multi-day → d+h", () => {
|
|
123
|
+
const now = new Date("2026-04-23T18:00:00Z");
|
|
124
|
+
const start = new Date("2026-04-19T12:00:00Z");
|
|
125
|
+
expect(formatUptime(start, now)).toBe("4d 6h");
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("ensureLogPath", () => {
|
|
130
|
+
test("creates logs dir and returns the log-file path", () => {
|
|
131
|
+
const { dir, cleanup } = makeTempConfig();
|
|
132
|
+
try {
|
|
133
|
+
const p = ensureLogPath("vault", dir);
|
|
134
|
+
expect(p).toBe(logPath("vault", dir));
|
|
135
|
+
expect(existsSync(join(dir, "vault", "logs"))).toBe(true);
|
|
136
|
+
} finally {
|
|
137
|
+
cleanup();
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
SCRIBE_DEFAULT_PROVIDER,
|
|
7
|
+
SCRIBE_PROVIDERS,
|
|
8
|
+
apiKeyEnvFor,
|
|
9
|
+
isKnownScribeProvider,
|
|
10
|
+
readScribeProviderState,
|
|
11
|
+
scribeConfigPath,
|
|
12
|
+
scribeEnvPath,
|
|
13
|
+
writeScribeApiKey,
|
|
14
|
+
writeScribeProvider,
|
|
15
|
+
} from "../scribe-config.ts";
|
|
16
|
+
|
|
17
|
+
function makeHarness(): { dir: string; cleanup: () => void } {
|
|
18
|
+
const dir = mkdtempSync(join(tmpdir(), "pcli-scribecfg-"));
|
|
19
|
+
return { dir, cleanup: () => rmSync(dir, { recursive: true, force: true }) };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("provider catalog", () => {
|
|
23
|
+
test("default provider is in the catalog", () => {
|
|
24
|
+
expect(SCRIBE_PROVIDERS.some((p) => p.key === SCRIBE_DEFAULT_PROVIDER)).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("isKnownScribeProvider matches every catalog key", () => {
|
|
28
|
+
for (const p of SCRIBE_PROVIDERS) {
|
|
29
|
+
expect(isKnownScribeProvider(p.key)).toBe(true);
|
|
30
|
+
}
|
|
31
|
+
expect(isKnownScribeProvider("not-a-provider")).toBe(false);
|
|
32
|
+
expect(isKnownScribeProvider("cloudflare")).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("apiKeyEnvFor maps cloud providers to env keys, locals to undefined", () => {
|
|
36
|
+
expect(apiKeyEnvFor("groq")).toBe("GROQ_API_KEY");
|
|
37
|
+
expect(apiKeyEnvFor("openai")).toBe("OPENAI_API_KEY");
|
|
38
|
+
expect(apiKeyEnvFor("parakeet-mlx")).toBeUndefined();
|
|
39
|
+
expect(apiKeyEnvFor("onnx-asr")).toBeUndefined();
|
|
40
|
+
expect(apiKeyEnvFor("whisper")).toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("readScribeProviderState", () => {
|
|
45
|
+
test("missing file: configExists false, no provider", () => {
|
|
46
|
+
const h = makeHarness();
|
|
47
|
+
try {
|
|
48
|
+
const state = readScribeProviderState(h.dir);
|
|
49
|
+
expect(state.configExists).toBe(false);
|
|
50
|
+
expect(state.provider).toBeUndefined();
|
|
51
|
+
} finally {
|
|
52
|
+
h.cleanup();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("file exists without transcribe block: configExists true, no provider", () => {
|
|
57
|
+
const h = makeHarness();
|
|
58
|
+
try {
|
|
59
|
+
mkdirSync(join(h.dir, "scribe"), { recursive: true });
|
|
60
|
+
writeFileSync(scribeConfigPath(h.dir), JSON.stringify({ auth: { required_token: "x" } }));
|
|
61
|
+
const state = readScribeProviderState(h.dir);
|
|
62
|
+
expect(state.configExists).toBe(true);
|
|
63
|
+
expect(state.provider).toBeUndefined();
|
|
64
|
+
} finally {
|
|
65
|
+
h.cleanup();
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("transcribe.provider set: returns it", () => {
|
|
70
|
+
const h = makeHarness();
|
|
71
|
+
try {
|
|
72
|
+
mkdirSync(join(h.dir, "scribe"), { recursive: true });
|
|
73
|
+
writeFileSync(
|
|
74
|
+
scribeConfigPath(h.dir),
|
|
75
|
+
JSON.stringify({ transcribe: { provider: "groq" }, auth: { required_token: "x" } }),
|
|
76
|
+
);
|
|
77
|
+
const state = readScribeProviderState(h.dir);
|
|
78
|
+
expect(state.provider).toBe("groq");
|
|
79
|
+
} finally {
|
|
80
|
+
h.cleanup();
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("malformed JSON: configExists true, provider undefined (no throw)", () => {
|
|
85
|
+
const h = makeHarness();
|
|
86
|
+
try {
|
|
87
|
+
mkdirSync(join(h.dir, "scribe"), { recursive: true });
|
|
88
|
+
writeFileSync(scribeConfigPath(h.dir), "{ not valid json");
|
|
89
|
+
const state = readScribeProviderState(h.dir);
|
|
90
|
+
expect(state.configExists).toBe(true);
|
|
91
|
+
expect(state.provider).toBeUndefined();
|
|
92
|
+
} finally {
|
|
93
|
+
h.cleanup();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("writeScribeProvider", () => {
|
|
99
|
+
test("creates new config with transcribe.provider when none exists", () => {
|
|
100
|
+
const h = makeHarness();
|
|
101
|
+
try {
|
|
102
|
+
writeScribeProvider(h.dir, "groq");
|
|
103
|
+
const parsed = JSON.parse(readFileSync(scribeConfigPath(h.dir), "utf8"));
|
|
104
|
+
expect(parsed).toEqual({ transcribe: { provider: "groq" } });
|
|
105
|
+
} finally {
|
|
106
|
+
h.cleanup();
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("preserves auth.required_token written by auto-wire", () => {
|
|
111
|
+
const h = makeHarness();
|
|
112
|
+
try {
|
|
113
|
+
mkdirSync(join(h.dir, "scribe"), { recursive: true });
|
|
114
|
+
writeFileSync(
|
|
115
|
+
scribeConfigPath(h.dir),
|
|
116
|
+
JSON.stringify({ auth: { required_token: "secret-token" } }),
|
|
117
|
+
);
|
|
118
|
+
writeScribeProvider(h.dir, "openai");
|
|
119
|
+
const parsed = JSON.parse(readFileSync(scribeConfigPath(h.dir), "utf8"));
|
|
120
|
+
expect(parsed.auth).toEqual({ required_token: "secret-token" });
|
|
121
|
+
expect(parsed.transcribe).toEqual({ provider: "openai" });
|
|
122
|
+
} finally {
|
|
123
|
+
h.cleanup();
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("merges into an existing transcribe block", () => {
|
|
128
|
+
const h = makeHarness();
|
|
129
|
+
try {
|
|
130
|
+
mkdirSync(join(h.dir, "scribe"), { recursive: true });
|
|
131
|
+
writeFileSync(
|
|
132
|
+
scribeConfigPath(h.dir),
|
|
133
|
+
JSON.stringify({ transcribe: { provider: "parakeet-mlx", language: "en" } }),
|
|
134
|
+
);
|
|
135
|
+
writeScribeProvider(h.dir, "groq");
|
|
136
|
+
const parsed = JSON.parse(readFileSync(scribeConfigPath(h.dir), "utf8"));
|
|
137
|
+
expect(parsed.transcribe).toEqual({ provider: "groq", language: "en" });
|
|
138
|
+
} finally {
|
|
139
|
+
h.cleanup();
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("overwrites malformed config (does not throw)", () => {
|
|
144
|
+
const h = makeHarness();
|
|
145
|
+
try {
|
|
146
|
+
mkdirSync(join(h.dir, "scribe"), { recursive: true });
|
|
147
|
+
writeFileSync(scribeConfigPath(h.dir), "{ broken");
|
|
148
|
+
writeScribeProvider(h.dir, "whisper");
|
|
149
|
+
const parsed = JSON.parse(readFileSync(scribeConfigPath(h.dir), "utf8"));
|
|
150
|
+
expect(parsed).toEqual({ transcribe: { provider: "whisper" } });
|
|
151
|
+
} finally {
|
|
152
|
+
h.cleanup();
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("writeScribeApiKey", () => {
|
|
158
|
+
test("creates scribe/.env with the key when missing", () => {
|
|
159
|
+
const h = makeHarness();
|
|
160
|
+
try {
|
|
161
|
+
writeScribeApiKey(h.dir, "GROQ_API_KEY", "gsk_test_123");
|
|
162
|
+
expect(readFileSync(scribeEnvPath(h.dir), "utf8")).toBe("GROQ_API_KEY=gsk_test_123\n");
|
|
163
|
+
} finally {
|
|
164
|
+
h.cleanup();
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("upserts in place when the key is already present", () => {
|
|
169
|
+
const h = makeHarness();
|
|
170
|
+
try {
|
|
171
|
+
mkdirSync(join(h.dir, "scribe"), { recursive: true });
|
|
172
|
+
writeFileSync(scribeEnvPath(h.dir), "OTHER=keep\nGROQ_API_KEY=old_value\nLAST=tail\n");
|
|
173
|
+
writeScribeApiKey(h.dir, "GROQ_API_KEY", "new_value");
|
|
174
|
+
const text = readFileSync(scribeEnvPath(h.dir), "utf8");
|
|
175
|
+
expect(text).toBe("OTHER=keep\nGROQ_API_KEY=new_value\nLAST=tail\n");
|
|
176
|
+
} finally {
|
|
177
|
+
h.cleanup();
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("preserves unrelated lines on first-time write", () => {
|
|
182
|
+
const h = makeHarness();
|
|
183
|
+
try {
|
|
184
|
+
mkdirSync(join(h.dir, "scribe"), { recursive: true });
|
|
185
|
+
writeFileSync(scribeEnvPath(h.dir), "EXISTING=value\n");
|
|
186
|
+
writeScribeApiKey(h.dir, "OPENAI_API_KEY", "sk-test");
|
|
187
|
+
const text = readFileSync(scribeEnvPath(h.dir), "utf8");
|
|
188
|
+
expect(text).toBe("EXISTING=value\nOPENAI_API_KEY=sk-test\n");
|
|
189
|
+
} finally {
|
|
190
|
+
h.cleanup();
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
});
|