@vellumai/cli 0.11.3 → 0.11.4-dev.202608190019.b94dbf2
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/node_modules/@vellumai/local-mode/src/__tests__/unpair.test.ts +33 -0
- package/node_modules/@vellumai/local-mode/src/index.ts +3 -0
- package/node_modules/@vellumai/local-mode/src/lockfile-lock.test.ts +165 -0
- package/node_modules/@vellumai/local-mode/src/lockfile-lock.ts +156 -0
- package/node_modules/@vellumai/local-mode/src/lockfile.test.ts +249 -8
- package/node_modules/@vellumai/local-mode/src/lockfile.ts +186 -68
- package/node_modules/@vellumai/local-mode/src/unpair.ts +18 -0
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
- package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/package.json +1 -1
- package/src/__tests__/assistant-config.test.ts +35 -0
- package/src/__tests__/nginx-ingress-command.test.ts +4 -23
- package/src/__tests__/nginx-ingress.test.ts +59 -215
- package/src/__tests__/pair.test.ts +11 -197
- package/src/__tests__/retire-archive.test.ts +13 -1
- package/src/__tests__/retire-local.test.ts +58 -4
- package/src/__tests__/tunnel.test.ts +0 -28
- package/src/__tests__/wake.test.ts +91 -69
- package/src/__tests__/windows-lifecycle.test.ts +157 -0
- package/src/commands/client.ts +9 -31
- package/src/commands/nginx-ingress.ts +0 -15
- package/src/commands/pair.ts +0 -39
- package/src/commands/wake.ts +39 -12
- package/src/lib/__tests__/web-dist.test.ts +86 -0
- package/src/lib/assistant-config.ts +64 -27
- package/src/lib/local.ts +60 -20
- package/src/lib/nginx-ingress.ts +35 -108
- package/src/lib/orphan-detection.test.ts +3 -0
- package/src/lib/orphan-detection.ts +33 -11
- package/src/lib/pgrep.ts +20 -2
- package/src/lib/process.ts +191 -18
- package/src/lib/retire-archive.ts +38 -8
- package/src/lib/retire-local.ts +75 -9
- package/src/lib/tunnel-edge.ts +14 -22
- package/src/lib/web-dist.ts +48 -0
- package/src/lib/feature-flags.test.ts +0 -157
- package/src/lib/feature-flags.ts +0 -38
|
@@ -382,14 +382,6 @@ describe("pair command", () => {
|
|
|
382
382
|
const origFetch = globalThis.fetch;
|
|
383
383
|
globalThis.fetch = (async (url: string, init?: RequestInit) => {
|
|
384
384
|
calls.push([url, init]);
|
|
385
|
-
if (url === `${LOCAL_URL}/v1/assistants/pair-test/feature-flags`) {
|
|
386
|
-
return new Response(
|
|
387
|
-
JSON.stringify({
|
|
388
|
-
flags: [{ key: "web-remote-ingress", enabled: true }],
|
|
389
|
-
}),
|
|
390
|
-
{ status: 200, headers: { "content-type": "application/json" } },
|
|
391
|
-
);
|
|
392
|
-
}
|
|
393
385
|
if (url === `${LOCAL_URL}/v1/remote-web/pairing-challenge`) {
|
|
394
386
|
return new Response(
|
|
395
387
|
JSON.stringify({
|
|
@@ -430,9 +422,9 @@ describe("pair command", () => {
|
|
|
430
422
|
globalThis.fetch = origFetch;
|
|
431
423
|
}
|
|
432
424
|
|
|
433
|
-
expect(calls).toHaveLength(
|
|
434
|
-
expect(calls[
|
|
435
|
-
expect(JSON.parse(calls[
|
|
425
|
+
expect(calls).toHaveLength(1);
|
|
426
|
+
expect(calls[0][0]).toBe(`${LOCAL_URL}/v1/remote-web/pairing-challenge`);
|
|
427
|
+
expect(JSON.parse(calls[0][1]?.body as string)).toEqual({
|
|
436
428
|
publicBaseUrl: "https://abc123.ngrok.app/assistant-123",
|
|
437
429
|
});
|
|
438
430
|
|
|
@@ -449,58 +441,6 @@ describe("pair command", () => {
|
|
|
449
441
|
expect(logs.join("\n")).not.toContain("refresh");
|
|
450
442
|
});
|
|
451
443
|
|
|
452
|
-
test("--web refuses when the web remote ingress feature flag is off", async () => {
|
|
453
|
-
const calls: Array<[string, RequestInit | undefined]> = [];
|
|
454
|
-
const origFetch = globalThis.fetch;
|
|
455
|
-
globalThis.fetch = (async (url: string, init?: RequestInit) => {
|
|
456
|
-
calls.push([url, init]);
|
|
457
|
-
return new Response(
|
|
458
|
-
JSON.stringify({
|
|
459
|
-
flags: [{ key: "web-remote-ingress", enabled: false }],
|
|
460
|
-
}),
|
|
461
|
-
{ status: 200, headers: { "content-type": "application/json" } },
|
|
462
|
-
);
|
|
463
|
-
}) as unknown as typeof fetch;
|
|
464
|
-
|
|
465
|
-
const errors: string[] = [];
|
|
466
|
-
const errSpy = spyOn(console, "error").mockImplementation(
|
|
467
|
-
(...a: unknown[]) => {
|
|
468
|
-
errors.push(a.join(" "));
|
|
469
|
-
},
|
|
470
|
-
);
|
|
471
|
-
const exitSpy = spyOn(process, "exit").mockImplementation(((
|
|
472
|
-
code?: number,
|
|
473
|
-
) => {
|
|
474
|
-
throw new Error(`exit:${code}`);
|
|
475
|
-
}) as never);
|
|
476
|
-
|
|
477
|
-
process.argv = [
|
|
478
|
-
"bun",
|
|
479
|
-
"vellum",
|
|
480
|
-
"pair",
|
|
481
|
-
"--web",
|
|
482
|
-
"--url",
|
|
483
|
-
"https://abc123.ngrok.app",
|
|
484
|
-
];
|
|
485
|
-
let exited = false;
|
|
486
|
-
try {
|
|
487
|
-
await pair();
|
|
488
|
-
} catch (e) {
|
|
489
|
-
exited = (e as Error).message === "exit:1";
|
|
490
|
-
} finally {
|
|
491
|
-
errSpy.mockRestore();
|
|
492
|
-
exitSpy.mockRestore();
|
|
493
|
-
globalThis.fetch = origFetch;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
expect(exited).toBe(true);
|
|
497
|
-
expect(errors.join("\n")).toContain("web-remote-ingress");
|
|
498
|
-
expect(calls).toHaveLength(1);
|
|
499
|
-
expect(calls[0][0]).toBe(
|
|
500
|
-
`${LOCAL_URL}/v1/assistants/pair-test/feature-flags`,
|
|
501
|
-
);
|
|
502
|
-
});
|
|
503
|
-
|
|
504
444
|
test("--web-approve approves a browser pairing code over loopback", async () => {
|
|
505
445
|
writeFileSync(
|
|
506
446
|
join(testDir, ".vellum.lock.json"),
|
|
@@ -521,14 +461,6 @@ describe("pair command", () => {
|
|
|
521
461
|
const origFetch = globalThis.fetch;
|
|
522
462
|
globalThis.fetch = (async (url: string, init?: RequestInit) => {
|
|
523
463
|
calls.push([url, init]);
|
|
524
|
-
if (url === `${LOCAL_URL}/v1/assistants/pair-test/feature-flags`) {
|
|
525
|
-
return new Response(
|
|
526
|
-
JSON.stringify({
|
|
527
|
-
flags: [{ key: "web-remote-ingress", enabled: true }],
|
|
528
|
-
}),
|
|
529
|
-
{ status: 200, headers: { "content-type": "application/json" } },
|
|
530
|
-
);
|
|
531
|
-
}
|
|
532
464
|
if (url === `${LOCAL_URL}/v1/remote-web/pairing-verification`) {
|
|
533
465
|
return new Response(
|
|
534
466
|
JSON.stringify({
|
|
@@ -564,9 +496,9 @@ describe("pair command", () => {
|
|
|
564
496
|
globalThis.fetch = origFetch;
|
|
565
497
|
}
|
|
566
498
|
|
|
567
|
-
expect(calls).toHaveLength(
|
|
568
|
-
expect(calls[
|
|
569
|
-
expect(JSON.parse(calls[
|
|
499
|
+
expect(calls).toHaveLength(1);
|
|
500
|
+
expect(calls[0][0]).toBe(`${LOCAL_URL}/v1/remote-web/pairing-verification`);
|
|
501
|
+
expect(JSON.parse(calls[0][1]?.body as string)).toEqual({
|
|
570
502
|
userCode: "ABCD-EFGH",
|
|
571
503
|
});
|
|
572
504
|
expect(JSON.parse(logs.join("\n"))).toEqual({
|
|
@@ -581,14 +513,6 @@ describe("pair command", () => {
|
|
|
581
513
|
const origFetch = globalThis.fetch;
|
|
582
514
|
globalThis.fetch = (async (url: string, init?: RequestInit) => {
|
|
583
515
|
calls.push([url, init]);
|
|
584
|
-
if (url === `${LOCAL_URL}/v1/assistants/pair-test/feature-flags`) {
|
|
585
|
-
return new Response(
|
|
586
|
-
JSON.stringify({
|
|
587
|
-
flags: [{ key: "web-remote-ingress", enabled: true }],
|
|
588
|
-
}),
|
|
589
|
-
{ status: 200, headers: { "content-type": "application/json" } },
|
|
590
|
-
);
|
|
591
|
-
}
|
|
592
516
|
if (url === `${LOCAL_URL}/v1/remote-web/pairing-challenge`) {
|
|
593
517
|
return new Response(
|
|
594
518
|
JSON.stringify({
|
|
@@ -637,18 +561,17 @@ describe("pair command", () => {
|
|
|
637
561
|
globalThis.fetch = origFetch;
|
|
638
562
|
}
|
|
639
563
|
|
|
640
|
-
//
|
|
564
|
+
// Create challenge → approve it, all over loopback. The
|
|
641
565
|
// approval is the whole point: running the CLI on the host IS the approval,
|
|
642
566
|
// so the scan alone completes pairing.
|
|
643
567
|
expect(calls.map((c) => c[0])).toEqual([
|
|
644
|
-
`${LOCAL_URL}/v1/assistants/pair-test/feature-flags`,
|
|
645
568
|
`${LOCAL_URL}/v1/remote-web/pairing-challenge`,
|
|
646
569
|
`${LOCAL_URL}/v1/remote-web/pairing-verification`,
|
|
647
570
|
]);
|
|
648
|
-
expect(JSON.parse(calls[
|
|
571
|
+
expect(JSON.parse(calls[0][1]?.body as string)).toEqual({
|
|
649
572
|
publicBaseUrl: "https://pair.example.ts.net",
|
|
650
573
|
});
|
|
651
|
-
expect(JSON.parse(calls[
|
|
574
|
+
expect(JSON.parse(calls[1][1]?.body as string)).toEqual({
|
|
652
575
|
userCode: "ABCD-EFGH",
|
|
653
576
|
});
|
|
654
577
|
|
|
@@ -669,14 +592,6 @@ describe("pair command", () => {
|
|
|
669
592
|
test("--qr renders a QR and prints the fallback URL and expiry", async () => {
|
|
670
593
|
const origFetch = globalThis.fetch;
|
|
671
594
|
globalThis.fetch = (async (url: string) => {
|
|
672
|
-
if (url === `${LOCAL_URL}/v1/assistants/pair-test/feature-flags`) {
|
|
673
|
-
return new Response(
|
|
674
|
-
JSON.stringify({
|
|
675
|
-
flags: [{ key: "web-remote-ingress", enabled: true }],
|
|
676
|
-
}),
|
|
677
|
-
{ status: 200, headers: { "content-type": "application/json" } },
|
|
678
|
-
);
|
|
679
|
-
}
|
|
680
595
|
if (url === `${LOCAL_URL}/v1/remote-web/pairing-challenge`) {
|
|
681
596
|
return new Response(
|
|
682
597
|
JSON.stringify({
|
|
@@ -903,59 +818,6 @@ describe("pair command", () => {
|
|
|
903
818
|
expect(fetchCalled).toBe(false);
|
|
904
819
|
});
|
|
905
820
|
|
|
906
|
-
test("--qr refuses when the web remote ingress feature flag is off", async () => {
|
|
907
|
-
const calls: Array<[string, RequestInit | undefined]> = [];
|
|
908
|
-
const origFetch = globalThis.fetch;
|
|
909
|
-
globalThis.fetch = (async (url: string, init?: RequestInit) => {
|
|
910
|
-
calls.push([url, init]);
|
|
911
|
-
return new Response(
|
|
912
|
-
JSON.stringify({
|
|
913
|
-
flags: [{ key: "web-remote-ingress", enabled: false }],
|
|
914
|
-
}),
|
|
915
|
-
{ status: 200, headers: { "content-type": "application/json" } },
|
|
916
|
-
);
|
|
917
|
-
}) as unknown as typeof fetch;
|
|
918
|
-
|
|
919
|
-
const errors: string[] = [];
|
|
920
|
-
const errSpy = spyOn(console, "error").mockImplementation(
|
|
921
|
-
(...a: unknown[]) => {
|
|
922
|
-
errors.push(a.join(" "));
|
|
923
|
-
},
|
|
924
|
-
);
|
|
925
|
-
const exitSpy = spyOn(process, "exit").mockImplementation(((
|
|
926
|
-
code?: number,
|
|
927
|
-
) => {
|
|
928
|
-
throw new Error(`exit:${code}`);
|
|
929
|
-
}) as never);
|
|
930
|
-
|
|
931
|
-
process.argv = [
|
|
932
|
-
"bun",
|
|
933
|
-
"vellum",
|
|
934
|
-
"pair",
|
|
935
|
-
"--qr",
|
|
936
|
-
"--url",
|
|
937
|
-
"https://pair.example.ts.net",
|
|
938
|
-
];
|
|
939
|
-
let exited = false;
|
|
940
|
-
try {
|
|
941
|
-
await pair();
|
|
942
|
-
} catch (e) {
|
|
943
|
-
exited = (e as Error).message === "exit:1";
|
|
944
|
-
} finally {
|
|
945
|
-
errSpy.mockRestore();
|
|
946
|
-
exitSpy.mockRestore();
|
|
947
|
-
globalThis.fetch = origFetch;
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
// Only the flag check runs; no challenge is minted when the flag is off.
|
|
951
|
-
expect(exited).toBe(true);
|
|
952
|
-
expect(errors.join("\n")).toContain("web-remote-ingress");
|
|
953
|
-
expect(calls).toHaveLength(1);
|
|
954
|
-
expect(calls[0][0]).toBe(
|
|
955
|
-
`${LOCAL_URL}/v1/assistants/pair-test/feature-flags`,
|
|
956
|
-
);
|
|
957
|
-
});
|
|
958
|
-
|
|
959
821
|
test("buildAppConnectUrl composes and encodes the connect link", () => {
|
|
960
822
|
expect(
|
|
961
823
|
buildAppConnectUrl(
|
|
@@ -1004,14 +866,6 @@ describe("pair command", () => {
|
|
|
1004
866
|
test("--qr --app emits an app connect URL alongside the browser URL", async () => {
|
|
1005
867
|
const origFetch = globalThis.fetch;
|
|
1006
868
|
globalThis.fetch = (async (url: string) => {
|
|
1007
|
-
if (url === `${LOCAL_URL}/v1/assistants/pair-test/feature-flags`) {
|
|
1008
|
-
return new Response(
|
|
1009
|
-
JSON.stringify({
|
|
1010
|
-
flags: [{ key: "web-remote-ingress", enabled: true }],
|
|
1011
|
-
}),
|
|
1012
|
-
{ status: 200, headers: { "content-type": "application/json" } },
|
|
1013
|
-
);
|
|
1014
|
-
}
|
|
1015
869
|
if (url === `${LOCAL_URL}/v1/remote-web/pairing-challenge`) {
|
|
1016
870
|
return new Response(
|
|
1017
871
|
JSON.stringify({
|
|
@@ -1079,14 +933,6 @@ describe("pair command", () => {
|
|
|
1079
933
|
test("--qr --app --label overrides the assistant name in the connect link", async () => {
|
|
1080
934
|
const origFetch = globalThis.fetch;
|
|
1081
935
|
globalThis.fetch = (async (url: string) => {
|
|
1082
|
-
if (url === `${LOCAL_URL}/v1/assistants/pair-test/feature-flags`) {
|
|
1083
|
-
return new Response(
|
|
1084
|
-
JSON.stringify({
|
|
1085
|
-
flags: [{ key: "web-remote-ingress", enabled: true }],
|
|
1086
|
-
}),
|
|
1087
|
-
{ status: 200, headers: { "content-type": "application/json" } },
|
|
1088
|
-
);
|
|
1089
|
-
}
|
|
1090
936
|
if (url === `${LOCAL_URL}/v1/remote-web/pairing-challenge`) {
|
|
1091
937
|
return new Response(
|
|
1092
938
|
JSON.stringify({
|
|
@@ -1205,14 +1051,6 @@ describe("pair command", () => {
|
|
|
1205
1051
|
const origFetch = globalThis.fetch;
|
|
1206
1052
|
globalThis.fetch = (async (url: string, init?: RequestInit) => {
|
|
1207
1053
|
calls.push(url);
|
|
1208
|
-
if (url === `${LOCAL_URL}/v1/assistants/pair-test/feature-flags`) {
|
|
1209
|
-
return new Response(
|
|
1210
|
-
JSON.stringify({
|
|
1211
|
-
flags: [{ key: "web-remote-ingress", enabled: true }],
|
|
1212
|
-
}),
|
|
1213
|
-
{ status: 200, headers: { "content-type": "application/json" } },
|
|
1214
|
-
);
|
|
1215
|
-
}
|
|
1216
1054
|
if (url === `${LOCAL_URL}/v1/remote-web/pairing-challenge`) {
|
|
1217
1055
|
// The minted challenge must advertise the entry's recorded URL.
|
|
1218
1056
|
expect(JSON.parse(init?.body as string)).toEqual({
|
|
@@ -1272,14 +1110,6 @@ describe("pair command", () => {
|
|
|
1272
1110
|
|
|
1273
1111
|
const origFetch = globalThis.fetch;
|
|
1274
1112
|
globalThis.fetch = (async (url: string, init?: RequestInit) => {
|
|
1275
|
-
if (url === `${LOCAL_URL}/v1/assistants/pair-test/feature-flags`) {
|
|
1276
|
-
return new Response(
|
|
1277
|
-
JSON.stringify({
|
|
1278
|
-
flags: [{ key: "web-remote-ingress", enabled: true }],
|
|
1279
|
-
}),
|
|
1280
|
-
{ status: 200, headers: { "content-type": "application/json" } },
|
|
1281
|
-
);
|
|
1282
|
-
}
|
|
1283
1113
|
if (url === `${LOCAL_URL}/v1/remote-web/pairing-challenge`) {
|
|
1284
1114
|
expect(JSON.parse(init?.body as string)).toEqual({
|
|
1285
1115
|
publicBaseUrl: "https://explicit.example.ts.net",
|
|
@@ -1338,15 +1168,7 @@ describe("pair command", () => {
|
|
|
1338
1168
|
|
|
1339
1169
|
const origFetch = globalThis.fetch;
|
|
1340
1170
|
let minted = false;
|
|
1341
|
-
globalThis.fetch = (async (
|
|
1342
|
-
if (url === `${LOCAL_URL}/v1/assistants/pair-test/feature-flags`) {
|
|
1343
|
-
return new Response(
|
|
1344
|
-
JSON.stringify({
|
|
1345
|
-
flags: [{ key: "web-remote-ingress", enabled: true }],
|
|
1346
|
-
}),
|
|
1347
|
-
{ status: 200, headers: { "content-type": "application/json" } },
|
|
1348
|
-
);
|
|
1349
|
-
}
|
|
1171
|
+
globalThis.fetch = (async () => {
|
|
1350
1172
|
minted = true;
|
|
1351
1173
|
return new Response("{}", { status: 200 });
|
|
1352
1174
|
}) as unknown as typeof fetch;
|
|
@@ -1387,15 +1209,7 @@ describe("pair command", () => {
|
|
|
1387
1209
|
|
|
1388
1210
|
const origFetch = globalThis.fetch;
|
|
1389
1211
|
let minted = false;
|
|
1390
|
-
globalThis.fetch = (async (
|
|
1391
|
-
if (url === `${LOCAL_URL}/v1/assistants/pair-test/feature-flags`) {
|
|
1392
|
-
return new Response(
|
|
1393
|
-
JSON.stringify({
|
|
1394
|
-
flags: [{ key: "web-remote-ingress", enabled: true }],
|
|
1395
|
-
}),
|
|
1396
|
-
{ status: 200, headers: { "content-type": "application/json" } },
|
|
1397
|
-
);
|
|
1398
|
-
}
|
|
1212
|
+
globalThis.fetch = (async () => {
|
|
1399
1213
|
minted = true;
|
|
1400
1214
|
return new Response("{}", { status: 200 });
|
|
1401
1215
|
}) as unknown as typeof fetch;
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { describe, test, expect } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import { win32 } from "node:path";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
resolveRetiredFilePath,
|
|
6
|
+
validateAssistantName,
|
|
7
|
+
} from "../lib/retire-archive.js";
|
|
3
8
|
|
|
4
9
|
describe("validateAssistantName", () => {
|
|
5
10
|
test("accepts valid names", () => {
|
|
@@ -32,3 +37,10 @@ describe("validateAssistantName", () => {
|
|
|
32
37
|
expect(() => validateAssistantName(".")).toThrow("Invalid assistant name");
|
|
33
38
|
});
|
|
34
39
|
});
|
|
40
|
+
|
|
41
|
+
test("accepts a Windows archive path inside the retired directory", () => {
|
|
42
|
+
const retiredDir = "C:\\Users\\Example User\\AppData\\Local\\Vellum\\retired";
|
|
43
|
+
expect(
|
|
44
|
+
resolveRetiredFilePath("assistant", "tar.gz", retiredDir, "win32"),
|
|
45
|
+
).toBe(win32.join(retiredDir, "assistant.tar.gz"));
|
|
46
|
+
});
|
|
@@ -12,7 +12,12 @@ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
|
12
12
|
import { tmpdir } from "node:os";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
|
|
15
|
-
import
|
|
15
|
+
import {
|
|
16
|
+
getDaemonPidPath,
|
|
17
|
+
type AssistantEntry,
|
|
18
|
+
} from "../lib/assistant-config.js";
|
|
19
|
+
import { getLockfilePath } from "../lib/environments/paths.js";
|
|
20
|
+
import { getCurrentEnvironment } from "../lib/environments/resolve.js";
|
|
16
21
|
|
|
17
22
|
const testDir = mkdtempSync(join(tmpdir(), "retire-local-test-"));
|
|
18
23
|
const originalLockfileDir = process.env.VELLUM_LOCKFILE_DIR;
|
|
@@ -31,7 +36,7 @@ const stopProcessByPidFileMock = mock(
|
|
|
31
36
|
async (_pidFile: string, _label: string): Promise<boolean> => true,
|
|
32
37
|
);
|
|
33
38
|
const stopOrphanedDaemonProcessesMock = mock(
|
|
34
|
-
async (): Promise<boolean> => false,
|
|
39
|
+
async (_excludePids?: ReadonlySet<string>): Promise<boolean> => false,
|
|
35
40
|
);
|
|
36
41
|
const stopIngressNginxMock = mock(async (): Promise<boolean> => false);
|
|
37
42
|
|
|
@@ -60,7 +65,7 @@ afterAll(() => {
|
|
|
60
65
|
mock.module("../lib/retire-archive.js", () => realRetireArchive);
|
|
61
66
|
});
|
|
62
67
|
|
|
63
|
-
import { retireLocal } from "../lib/retire-local.js";
|
|
68
|
+
import { getRetireArchiveCommand, retireLocal } from "../lib/retire-local.js";
|
|
64
69
|
|
|
65
70
|
const instanceDir = join(testDir, "test-instance");
|
|
66
71
|
const vellumDir = join(instanceDir, ".vellum");
|
|
@@ -82,7 +87,7 @@ function makeEntry(assistantId: string): AssistantEntry {
|
|
|
82
87
|
|
|
83
88
|
function writeLockfile(entries: AssistantEntry[]): void {
|
|
84
89
|
writeFileSync(
|
|
85
|
-
|
|
90
|
+
getLockfilePath(getCurrentEnvironment()),
|
|
86
91
|
JSON.stringify({ assistants: entries }, null, 2) + "\n",
|
|
87
92
|
);
|
|
88
93
|
}
|
|
@@ -174,4 +179,53 @@ describe("retireLocal — CES sibling stop", () => {
|
|
|
174
179
|
);
|
|
175
180
|
expect(cesStopCall).toBeDefined();
|
|
176
181
|
});
|
|
182
|
+
|
|
183
|
+
test("protects other assistants during orphan cleanup", async () => {
|
|
184
|
+
const otherInstanceDir = join(testDir, "other-instance");
|
|
185
|
+
const otherEntry = makeEntry("other-assistant");
|
|
186
|
+
otherEntry.resources!.instanceDir = otherInstanceDir;
|
|
187
|
+
const otherPid = "4242";
|
|
188
|
+
const otherPidFile = getDaemonPidPath(otherEntry.resources);
|
|
189
|
+
mkdirSync(join(otherInstanceDir, ".vellum", "workspace"), {
|
|
190
|
+
recursive: true,
|
|
191
|
+
});
|
|
192
|
+
writeFileSync(otherPidFile, otherPid);
|
|
193
|
+
writeLockfile([makeEntry("test-assistant"), otherEntry]);
|
|
194
|
+
stopProcessByPidFileMock.mockImplementation(
|
|
195
|
+
async (_pidFile: string, label: string): Promise<boolean> =>
|
|
196
|
+
label !== "daemon",
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
await retireLocal("test-assistant", makeEntry("test-assistant"), {
|
|
200
|
+
progress: () => {},
|
|
201
|
+
log: () => {},
|
|
202
|
+
warn: () => {},
|
|
203
|
+
error: () => {},
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
expect(stopOrphanedDaemonProcessesMock).toHaveBeenCalledTimes(1);
|
|
207
|
+
const excludedPids = stopOrphanedDaemonProcessesMock.mock.calls[0]?.[0];
|
|
208
|
+
expect(excludedPids?.has(otherPid)).toBeTrue();
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("uses a Windows-native archive process", () => {
|
|
213
|
+
const archivePath =
|
|
214
|
+
"C:\\Users\\Example User\\Vellum\\retired\\assistant & data.tar.gz";
|
|
215
|
+
const stagingDir = `${archivePath}.staging`;
|
|
216
|
+
const archiveCommand = getRetireArchiveCommand(
|
|
217
|
+
archivePath,
|
|
218
|
+
stagingDir,
|
|
219
|
+
"win32",
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
expect(archiveCommand.command).toBe("powershell.exe");
|
|
223
|
+
expect(archiveCommand.args.join(" ")).toContain("tar.exe");
|
|
224
|
+
expect(archiveCommand.args.join(" ")).not.toContain(archivePath);
|
|
225
|
+
expect(archiveCommand.env).toMatchObject({
|
|
226
|
+
VELLUM_RETIRE_ARCHIVE_PATH: archivePath,
|
|
227
|
+
VELLUM_RETIRE_ARCHIVE_PARENT: "C:\\Users\\Example User\\Vellum\\retired",
|
|
228
|
+
VELLUM_RETIRE_STAGING_NAME: "assistant & data.tar.gz.staging",
|
|
229
|
+
VELLUM_RETIRE_STAGING_DIR: stagingDir,
|
|
230
|
+
});
|
|
177
231
|
});
|
|
@@ -238,20 +238,6 @@ describe("tunnel edge targeting", () => {
|
|
|
238
238
|
mock.module("../lib/nginx-ingress.js", () => realNginxIngress);
|
|
239
239
|
});
|
|
240
240
|
|
|
241
|
-
test("does not start ngrok when the edge flag lookup fails", async () => {
|
|
242
|
-
process.argv = ["bun", "vellum", "tunnel", "--provider", "ngrok"];
|
|
243
|
-
ensureTunnelEdgeMock.mockImplementation(realNginxIngress.ensureTunnelEdge);
|
|
244
|
-
|
|
245
|
-
const { exited, errors } = await runTunnelExpectingExit1();
|
|
246
|
-
|
|
247
|
-
expect(exited).toBe(true);
|
|
248
|
-
expect(errors).toContain(
|
|
249
|
-
"Could not verify the `web-remote-ingress` feature flag",
|
|
250
|
-
);
|
|
251
|
-
expect(runNgrokTunnelMock).not.toHaveBeenCalled();
|
|
252
|
-
expect(runCloudflareTunnelMock).not.toHaveBeenCalled();
|
|
253
|
-
});
|
|
254
|
-
|
|
255
241
|
test("targets the edge port returned by ensureTunnelEdge for ngrok", async () => {
|
|
256
242
|
const entry = makeLocalEntry();
|
|
257
243
|
entry.runtimeUrl = "https://stale-tunnel.ngrok-free.dev";
|
|
@@ -660,20 +646,6 @@ describe("tunnel edge targeting", () => {
|
|
|
660
646
|
expect(runTailscaleTunnelMock).not.toHaveBeenCalled();
|
|
661
647
|
});
|
|
662
648
|
|
|
663
|
-
test("does not start cloudflared when the edge flag lookup fails", async () => {
|
|
664
|
-
process.argv = ["bun", "vellum", "tunnel", "--provider", "cloudflare"];
|
|
665
|
-
ensureTunnelEdgeMock.mockImplementation(realNginxIngress.ensureTunnelEdge);
|
|
666
|
-
|
|
667
|
-
const { exited, errors } = await runTunnelExpectingExit1();
|
|
668
|
-
|
|
669
|
-
expect(exited).toBe(true);
|
|
670
|
-
expect(errors).toContain(
|
|
671
|
-
"Could not verify the `web-remote-ingress` feature flag",
|
|
672
|
-
);
|
|
673
|
-
expect(runNgrokTunnelMock).not.toHaveBeenCalled();
|
|
674
|
-
expect(runCloudflareTunnelMock).not.toHaveBeenCalled();
|
|
675
|
-
});
|
|
676
|
-
|
|
677
649
|
test("rejects an unknown --provider with a stale-CLI hint", async () => {
|
|
678
650
|
process.argv = ["bun", "vellum", "tunnel", "--provider", "bogus"];
|
|
679
651
|
|