@mycadet/cli 0.12.10 → 0.13.0-dev.202610171617.9ea363e
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 +13 -0
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/channels.ts +0 -1
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/channels.ts +0 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
- package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
- package/node_modules/@mycadet/service-contracts/src/channels.ts +0 -1
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
- package/package.json +8 -3
- package/src/__tests__/assistant-client-cloud-auth.test.ts +2 -2
- package/src/__tests__/backup-ops-matching.test.ts +15 -13
- package/src/__tests__/backup-ops.test.ts +105 -0
- package/src/__tests__/backup.test.ts +114 -673
- package/src/__tests__/cloud-login-poll.test.ts +69 -0
- package/src/__tests__/cloud-releases.test.ts +82 -0
- package/src/__tests__/commands-mock.ts +101 -0
- package/src/__tests__/debug.test.ts +17 -91
- package/src/__tests__/devices.test.ts +64 -2
- package/src/__tests__/ensure-binary.test.ts +52 -0
- package/src/__tests__/gateway-token.test.ts +267 -0
- package/src/__tests__/hatch-next-steps.test.ts +38 -0
- package/src/__tests__/host-teleport-docker.test.ts +371 -0
- package/src/__tests__/host-teleport.test.ts +405 -0
- package/src/__tests__/multi-local.test.ts +3 -3
- package/src/__tests__/nginx-ingress.test.ts +587 -36
- package/src/__tests__/orphan-detection.test.ts +1 -1
- package/src/__tests__/paired-lifecycle.test.ts +2 -2
- package/src/__tests__/ps-cloud-status.test.ts +8 -7
- package/src/__tests__/root-backup-docker.test.ts +294 -0
- package/src/__tests__/root-backup.test.ts +527 -0
- package/src/__tests__/root-restore-command.test.ts +147 -0
- package/src/__tests__/root-signing-key.test.ts +50 -0
- package/src/__tests__/setup.test.ts +17 -7
- package/src/__tests__/shell-mode.test.ts +83 -0
- package/src/__tests__/sleep.test.ts +11 -0
- package/src/__tests__/teleport.test.ts +68 -2996
- package/src/__tests__/tunnel-startup-summary.test.ts +110 -0
- package/src/__tests__/tunnel-stop.test.ts +226 -0
- package/src/__tests__/tunnel.test.ts +22 -4
- package/src/__tests__/upgrade-local.test.ts +21 -11
- package/src/__tests__/upgrade-replay-env.test.ts +2 -2
- package/src/__tests__/upgrade-root-recovery.test.ts +185 -0
- package/src/__tests__/wake.test.ts +4 -1
- package/src/__tests__/windows-lifecycle.test.ts +25 -0
- package/src/commands/backup.ts +44 -399
- package/src/commands/client.ts +3 -3
- package/src/commands/completions.ts +0 -2
- package/src/commands/debug.ts +75 -131
- package/src/commands/devices.ts +117 -47
- package/src/commands/gateway/token.ts +77 -23
- package/src/commands/hatch.ts +10 -52
- package/src/commands/pair.ts +9 -3
- package/src/commands/ps.ts +4 -2
- package/src/commands/restore.ts +77 -444
- package/src/commands/retire.ts +2 -3
- package/src/commands/sleep.ts +15 -2
- package/src/commands/teleport.ts +47 -1559
- package/src/commands/tunnel.ts +91 -11
- package/src/commands/upgrade.ts +84 -157
- package/src/commands/wake.ts +1 -1
- package/src/index.ts +0 -4
- package/src/lib/__tests__/http-client-version.test.ts +29 -0
- package/src/lib/__tests__/runtime-url.test.ts +3 -75
- package/src/lib/__tests__/web-dist.test.ts +27 -1
- package/src/lib/assistant-client.ts +1 -1
- package/src/lib/assistant-config.ts +43 -23
- package/src/lib/backup-ops.ts +37 -251
- package/src/lib/cloud-client.ts +10 -13
- package/src/lib/cloud-login-poll.ts +75 -0
- package/src/lib/cloud-login.ts +5 -2
- package/src/lib/cloud-releases.ts +54 -10
- package/src/lib/cloud-values.ts +1 -1
- package/src/lib/confirm-action.ts +2 -2
- package/src/lib/docker.ts +65 -34
- package/src/lib/ensure-binary.ts +65 -6
- package/src/lib/github-repository.ts +2 -0
- package/src/lib/guardian-token.ts +1 -23
- package/src/lib/hatch-local.ts +18 -52
- package/src/lib/hatch-next-steps.ts +15 -2
- package/src/lib/host-teleport.ts +506 -0
- package/src/lib/http-client.ts +25 -0
- package/src/lib/ingress-config.ts +31 -0
- package/src/lib/live-voice/tunnel.test.ts +1 -1
- package/src/lib/live-voice/tunnel.ts +3 -3
- package/src/lib/local.ts +10 -8
- package/src/lib/nginx-ingress.ts +572 -95
- package/src/lib/process.ts +13 -0
- package/src/lib/root-backup.ts +917 -0
- package/src/lib/root-signing-key.ts +31 -0
- package/src/lib/runtime-url.ts +0 -29
- package/src/lib/teleport-backup.ts +3 -16
- package/src/lib/tunnel.ts +87 -34
- package/src/lib/tunnels/cadet.ts +12 -1
- package/src/lib/tunnels/cloudflare.ts +36 -1
- package/src/lib/tunnels/ngrok.ts +16 -2
- package/src/lib/tunnels/process-lifecycle.ts +26 -0
- package/src/lib/tunnels/tailscale.ts +79 -1
- package/src/lib/upgrade-lifecycle.ts +63 -141
- package/src/lib/web-dist.ts +79 -1
- package/src/lib/xdg-log.ts +11 -0
- package/src/__tests__/bundle-staging.test.ts +0 -268
- package/src/__tests__/unpair.test.ts +0 -160
- package/src/commands/rollback.ts +0 -479
- package/src/commands/unpair.ts +0 -122
- package/src/lib/__tests__/job-polling.test.ts +0 -278
- package/src/lib/__tests__/local-runtime-client.test.ts +0 -666
- package/src/lib/bundle-staging.ts +0 -220
- package/src/lib/job-polling.ts +0 -195
- package/src/lib/local-runtime-client.ts +0 -374
|
@@ -26,6 +26,7 @@ import { z } from "zod";
|
|
|
26
26
|
|
|
27
27
|
import * as httpClient from "../lib/http-client.js";
|
|
28
28
|
import type { AssistantEntry } from "../lib/assistant-config.js";
|
|
29
|
+
import { saveShellMode } from "../lib/ingress-config.js";
|
|
29
30
|
|
|
30
31
|
const realChildProcess = { ...childProcess };
|
|
31
32
|
const realFs = { ...fsModule };
|
|
@@ -51,6 +52,10 @@ mock.module("node:fs", () => ({
|
|
|
51
52
|
readFileSync: readFileSyncMock,
|
|
52
53
|
}));
|
|
53
54
|
|
|
55
|
+
const fetchCurrentVersionMock = mock<typeof httpClient.fetchCurrentVersion>(
|
|
56
|
+
async () => "1.2.3",
|
|
57
|
+
);
|
|
58
|
+
|
|
54
59
|
const waitForDaemonReadyMock = mock<typeof httpClient.waitForDaemonReady>(
|
|
55
60
|
async () => true,
|
|
56
61
|
);
|
|
@@ -58,6 +63,7 @@ const waitForDaemonReadyMock = mock<typeof httpClient.waitForDaemonReady>(
|
|
|
58
63
|
mock.module("../lib/http-client.js", () => ({
|
|
59
64
|
...httpClient,
|
|
60
65
|
waitForDaemonReady: waitForDaemonReadyMock,
|
|
66
|
+
fetchCurrentVersion: fetchCurrentVersionMock,
|
|
61
67
|
}));
|
|
62
68
|
|
|
63
69
|
// Restore the real modules once this file finishes so the mocks do not leak
|
|
@@ -72,10 +78,11 @@ import {
|
|
|
72
78
|
buildIngressNginxConfig,
|
|
73
79
|
hasIpv6Loopback,
|
|
74
80
|
buildRemoteWebIndexHtml,
|
|
75
|
-
cloudWebHubUrl,
|
|
76
81
|
MAX_PRESERVED_MODULE_PRELOADS,
|
|
77
82
|
ensureTunnelEdge,
|
|
78
83
|
EDGE_TEMPLATE_VERSION,
|
|
84
|
+
findAppDistDir,
|
|
85
|
+
findWebDistDir,
|
|
79
86
|
startRemoteWebIngress,
|
|
80
87
|
stopContainerTunnelEdge,
|
|
81
88
|
stopIngressNginx,
|
|
@@ -123,6 +130,8 @@ afterEach(() => {
|
|
|
123
130
|
existsSyncMock.mockImplementation(realFs.existsSync);
|
|
124
131
|
readFileSyncMock.mockReset();
|
|
125
132
|
readFileSyncMock.mockImplementation(realFs.readFileSync);
|
|
133
|
+
fetchCurrentVersionMock.mockReset();
|
|
134
|
+
fetchCurrentVersionMock.mockImplementation(async () => "1.2.3");
|
|
126
135
|
waitForDaemonReadyMock.mockReset();
|
|
127
136
|
waitForDaemonReadyMock.mockImplementation(async () => true);
|
|
128
137
|
for (const dir of workspaces.splice(0)) {
|
|
@@ -306,7 +315,7 @@ describe("buildIngressNginxConfig", () => {
|
|
|
306
315
|
).toEqual([]);
|
|
307
316
|
});
|
|
308
317
|
|
|
309
|
-
test("revalidates the shell and forbids storing
|
|
318
|
+
test("revalidates the shell and forbids storing inline config", () => {
|
|
310
319
|
const blocks = locationBlocks(remoteConf);
|
|
311
320
|
const cacheControlOf = (matcher: string) =>
|
|
312
321
|
blocks
|
|
@@ -317,11 +326,8 @@ describe("buildIngressNginxConfig", () => {
|
|
|
317
326
|
expect(cacheControlOf("^~ /assistant/assets/")).toBe(
|
|
318
327
|
"public, max-age=31536000, immutable",
|
|
319
328
|
);
|
|
320
|
-
//
|
|
321
|
-
// refuses storage. A second no-store means a revalidating location
|
|
322
|
-
// regressed to re-sending its whole body on every load.
|
|
329
|
+
// Inline config has no validator and must not be stored.
|
|
323
330
|
expect(cacheControlOf("= /assistant/__config")).toBe("no-store");
|
|
324
|
-
expect(remoteConf.match(/no-store/g)).toHaveLength(1);
|
|
325
331
|
});
|
|
326
332
|
|
|
327
333
|
test("serves remote web config for the SPA", () => {
|
|
@@ -456,9 +462,7 @@ describe("buildRemoteWebIndexHtml", () => {
|
|
|
456
462
|
expect(result).toContain(
|
|
457
463
|
'window.__CADET_CONFIG__={"remote":"docker","apiBaseUrl":"/v1"}',
|
|
458
464
|
);
|
|
459
|
-
expect(result.indexOf('"webUrl"')).toBeLessThan(
|
|
460
|
-
result.indexOf('"docker"'),
|
|
461
|
-
);
|
|
465
|
+
expect(result.indexOf('"webUrl"')).toBeLessThan(result.indexOf('"docker"'));
|
|
462
466
|
});
|
|
463
467
|
|
|
464
468
|
test("escapes config JSON before embedding it in a script tag", () => {
|
|
@@ -528,26 +532,6 @@ describe("buildRemoteWebIndexHtml", () => {
|
|
|
528
532
|
});
|
|
529
533
|
});
|
|
530
534
|
|
|
531
|
-
describe("cloudWebHubUrl", () => {
|
|
532
|
-
test("maps build environments to their cloud SPA base", () => {
|
|
533
|
-
expect(cloudWebHubUrl("production")).toBe(
|
|
534
|
-
"https://mycadet.ai/assistant",
|
|
535
|
-
);
|
|
536
|
-
expect(cloudWebHubUrl("staging")).toBe(
|
|
537
|
-
"https://staging.mycadet.ai/assistant",
|
|
538
|
-
);
|
|
539
|
-
expect(cloudWebHubUrl("dev")).toBe(
|
|
540
|
-
"https://dev.mycadet.ai/assistant",
|
|
541
|
-
);
|
|
542
|
-
expect(cloudWebHubUrl("local")).toBe(
|
|
543
|
-
"https://dev.mycadet.ai/assistant",
|
|
544
|
-
);
|
|
545
|
-
expect(cloudWebHubUrl(undefined)).toBe(
|
|
546
|
-
"https://dev.mycadet.ai/assistant",
|
|
547
|
-
);
|
|
548
|
-
});
|
|
549
|
-
});
|
|
550
|
-
|
|
551
535
|
describe("nginx ingress process state", () => {
|
|
552
536
|
function writeIngressState(workspaceDir: string, listenPort: number): void {
|
|
553
537
|
writeFileSync(
|
|
@@ -755,17 +739,19 @@ function mockWebDistMissing(): void {
|
|
|
755
739
|
}
|
|
756
740
|
|
|
757
741
|
/** Force findWebDistDir() to resolve a dist dir, regardless of checkout state. */
|
|
758
|
-
function mockWebDistPresent(): void {
|
|
742
|
+
function mockWebDistPresent(version: string | null = "1.2.3"): void {
|
|
759
743
|
existsSyncMock.mockImplementation((path) =>
|
|
760
744
|
String(path).endsWith(WEB_INDEX_SUFFIX) ? true : realFs.existsSync(path),
|
|
761
745
|
);
|
|
762
746
|
readFileSyncMock.mockImplementation(((path, options) =>
|
|
763
747
|
String(path).endsWith(WEB_INDEX_SUFFIX)
|
|
764
748
|
? FAKE_INDEX_HTML
|
|
765
|
-
:
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
749
|
+
: String(path).endsWith("release.json")
|
|
750
|
+
? JSON.stringify(version === null ? {} : { version })
|
|
751
|
+
: realFs.readFileSync(
|
|
752
|
+
path as never,
|
|
753
|
+
options as never,
|
|
754
|
+
)) as typeof readFileSync);
|
|
769
755
|
}
|
|
770
756
|
|
|
771
757
|
/** Record a running edge: ingress state, live pidfile, matching ps output. */
|
|
@@ -846,14 +832,19 @@ const PRODUCTION_HUB_URL = "https://mycadet.ai/assistant";
|
|
|
846
832
|
|
|
847
833
|
/**
|
|
848
834
|
* Mirror of the SPA config fingerprint the edge records in its ingress state
|
|
849
|
-
* (sha256 over the
|
|
835
|
+
* (sha256 over the template, injected config, bundle directory and index). Pins
|
|
850
836
|
* both the injected config shape and the hash format; assumes the
|
|
851
837
|
* production-pinned environment. The version itself is imported rather than
|
|
852
838
|
* mirrored: bumping it is the correct response to any template edit, so a
|
|
853
839
|
* copy here would fail every such edit without naming a defect.
|
|
854
840
|
*/
|
|
855
841
|
function spaConfigHash(
|
|
856
|
-
opts: {
|
|
842
|
+
opts: {
|
|
843
|
+
assistantName?: string;
|
|
844
|
+
assistantId?: string;
|
|
845
|
+
indexHtml?: string;
|
|
846
|
+
webDistDir?: string;
|
|
847
|
+
} = {},
|
|
857
848
|
): string {
|
|
858
849
|
return createHash("sha256")
|
|
859
850
|
.update(
|
|
@@ -866,6 +857,12 @@ function spaConfigHash(
|
|
|
866
857
|
...(opts.assistantId ? { assistantId: opts.assistantId } : {}),
|
|
867
858
|
hubUrl: PRODUCTION_HUB_URL,
|
|
868
859
|
},
|
|
860
|
+
webDistDir: opts.webDistDir ?? findWebDistDir(),
|
|
861
|
+
version: "1.2.3",
|
|
862
|
+
// Web mode is the default shell; the fingerprint covers the mode so
|
|
863
|
+
// an edge built for the other shell is never mistaken for this one.
|
|
864
|
+
shellMode: "web",
|
|
865
|
+
indexHtml: opts.indexHtml ?? FAKE_INDEX_HTML,
|
|
869
866
|
}),
|
|
870
867
|
)
|
|
871
868
|
.digest("hex");
|
|
@@ -890,6 +887,70 @@ function pinProductionEnvironment(): void {
|
|
|
890
887
|
describe("startRemoteWebIngress", () => {
|
|
891
888
|
pinProductionEnvironment();
|
|
892
889
|
|
|
890
|
+
for (const [webVersion, runtimeVersion, ready] of [
|
|
891
|
+
["1.2.3", "1.2.3", true],
|
|
892
|
+
["1.2.3", "v1.2.3", true],
|
|
893
|
+
["1.2.2", "1.2.3", false],
|
|
894
|
+
["1.2.3", undefined, false],
|
|
895
|
+
[null, "1.2.3", false],
|
|
896
|
+
] as const) {
|
|
897
|
+
test(`release resolution: UI ${webVersion}, assistant ${runtimeVersion}`, async () => {
|
|
898
|
+
const ws = makeWorkspace();
|
|
899
|
+
mockNginxInstalled();
|
|
900
|
+
mockNginxSpawn();
|
|
901
|
+
mockWebDistPresent(webVersion);
|
|
902
|
+
fetchCurrentVersionMock.mockResolvedValue(runtimeVersion);
|
|
903
|
+
const result = await startRemoteWebIngress({
|
|
904
|
+
workspaceDir: ws,
|
|
905
|
+
gatewayPort: 7830,
|
|
906
|
+
listenPort: 7845,
|
|
907
|
+
assistantId: "assistant-1",
|
|
908
|
+
});
|
|
909
|
+
expect(result.status).toBe("started");
|
|
910
|
+
expect(fetchCurrentVersionMock).toHaveBeenCalledWith(
|
|
911
|
+
"http://127.0.0.1:7830",
|
|
912
|
+
);
|
|
913
|
+
const conf = realFs.readFileSync(ingressConfPath(ws), "utf8");
|
|
914
|
+
const detail = conf
|
|
915
|
+
.split('location = "/v1/assistants/assistant-1/" {')[1]
|
|
916
|
+
?.split("\n }")[0];
|
|
917
|
+
expect(detail).toContain(`return ${ready ? 200 : 409} `);
|
|
918
|
+
if (ready) {
|
|
919
|
+
expect(detail).toContain('\\"version\\":\\"1.2.3\\"');
|
|
920
|
+
} else {
|
|
921
|
+
expect(detail).not.toContain('\\"url\\"');
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
test("restarts an edge when its verified assistant/UI release changes", async () => {
|
|
927
|
+
const ws = makeWorkspace();
|
|
928
|
+
mockNginxInstalled();
|
|
929
|
+
mockNginxSpawn();
|
|
930
|
+
mockWebDistPresent("1.2.3");
|
|
931
|
+
fetchCurrentVersionMock.mockResolvedValue("1.2.4");
|
|
932
|
+
const pid = mockRunningEdge(ws, {
|
|
933
|
+
listenPort: 7845,
|
|
934
|
+
includeWebApp: true,
|
|
935
|
+
gatewayPort: 7830,
|
|
936
|
+
remoteWebConfigHash: spaConfigHash({ assistantId: "assistant-1" }),
|
|
937
|
+
});
|
|
938
|
+
const edge = mockKillableNginx(pid);
|
|
939
|
+
const result = await startRemoteWebIngress({
|
|
940
|
+
workspaceDir: ws,
|
|
941
|
+
gatewayPort: 7830,
|
|
942
|
+
listenPort: 7845,
|
|
943
|
+
assistantId: "assistant-1",
|
|
944
|
+
});
|
|
945
|
+
expect(result.status).toBe("started");
|
|
946
|
+
expect(edge.killed()).toBe(true);
|
|
947
|
+
const conf = realFs.readFileSync(ingressConfPath(ws), "utf8");
|
|
948
|
+
const detail = conf
|
|
949
|
+
.split('location = "/v1/assistants/assistant-1/" {')[1]
|
|
950
|
+
?.split("\n }")[0];
|
|
951
|
+
expect(detail).toContain("return 409 ");
|
|
952
|
+
});
|
|
953
|
+
|
|
893
954
|
test("webhooks-only mode starts the denylist+proxy edge without a web dist", async () => {
|
|
894
955
|
const ws = makeWorkspace();
|
|
895
956
|
mockNginxInstalled();
|
|
@@ -1247,6 +1308,7 @@ describe("startRemoteWebIngress", () => {
|
|
|
1247
1308
|
const ws = makeWorkspace();
|
|
1248
1309
|
mockNginxInstalled();
|
|
1249
1310
|
mockNginxSpawn();
|
|
1311
|
+
mockWebDistPresent();
|
|
1250
1312
|
const pid = mockRunningEdge(ws, {
|
|
1251
1313
|
listenPort: 7845,
|
|
1252
1314
|
gatewayPort: 7830,
|
|
@@ -1300,6 +1362,44 @@ describe("startRemoteWebIngress", () => {
|
|
|
1300
1362
|
expect(spawnMock).not.toHaveBeenCalled();
|
|
1301
1363
|
});
|
|
1302
1364
|
|
|
1365
|
+
for (const drift of ["indexHtml", "webDistDir"] as const) {
|
|
1366
|
+
test(`restarts a detached edge when the web bundle ${drift} changes`, async () => {
|
|
1367
|
+
const ws = makeWorkspace();
|
|
1368
|
+
mockNginxInstalled();
|
|
1369
|
+
mockNginxSpawn();
|
|
1370
|
+
mockWebDistPresent();
|
|
1371
|
+
const pid = mockRunningEdge(ws, {
|
|
1372
|
+
listenPort: 7845,
|
|
1373
|
+
includeWebApp: true,
|
|
1374
|
+
gatewayPort: 7830,
|
|
1375
|
+
remoteWebConfigHash: spaConfigHash({
|
|
1376
|
+
...(drift === "indexHtml"
|
|
1377
|
+
? {
|
|
1378
|
+
indexHtml:
|
|
1379
|
+
'<html><head><script src="/assistant/assets/old.js"></script></head></html>',
|
|
1380
|
+
}
|
|
1381
|
+
: { webDistDir: "/old-release/dist" }),
|
|
1382
|
+
}),
|
|
1383
|
+
});
|
|
1384
|
+
const edge = mockKillableNginx(pid);
|
|
1385
|
+
const result = await startRemoteWebIngress({
|
|
1386
|
+
workspaceDir: ws,
|
|
1387
|
+
gatewayPort: 7830,
|
|
1388
|
+
});
|
|
1389
|
+
expect(result).toMatchObject({ status: "started", listenPort: 7845 });
|
|
1390
|
+
expect(edge.killed()).toBe(true);
|
|
1391
|
+
expect(readConfig(ws).ingress?.nginx?.remoteWebConfigHash).toBe(
|
|
1392
|
+
spaConfigHash(),
|
|
1393
|
+
);
|
|
1394
|
+
expect(
|
|
1395
|
+
realFs.readFileSync(
|
|
1396
|
+
join(ws, "data", "ingress", "assistant-index.html"),
|
|
1397
|
+
"utf-8",
|
|
1398
|
+
),
|
|
1399
|
+
).not.toContain("old.js");
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1303
1403
|
test("restarts the edge when the injected SPA config drifts from the recorded one", async () => {
|
|
1304
1404
|
const ws = makeWorkspace();
|
|
1305
1405
|
mockNginxInstalled();
|
|
@@ -1801,6 +1901,7 @@ describe("ensureTunnelEdge", () => {
|
|
|
1801
1901
|
const ws = makeWorkspace();
|
|
1802
1902
|
mockNginxInstalled();
|
|
1803
1903
|
mockNginxSpawn();
|
|
1904
|
+
mockWebDistPresent();
|
|
1804
1905
|
const pid = mockRunningEdge(ws, {
|
|
1805
1906
|
listenPort: 7845,
|
|
1806
1907
|
includeWebApp: true,
|
|
@@ -1819,6 +1920,7 @@ describe("ensureTunnelEdge", () => {
|
|
|
1819
1920
|
port: 7845,
|
|
1820
1921
|
started: false,
|
|
1821
1922
|
includesWebApp: true,
|
|
1923
|
+
shellMode: "web",
|
|
1822
1924
|
});
|
|
1823
1925
|
expect(edge.killed()).toBe(false);
|
|
1824
1926
|
expect(spawnMock).not.toHaveBeenCalled();
|
|
@@ -1972,6 +2074,7 @@ describe("ensureTunnelEdge", () => {
|
|
|
1972
2074
|
port: REQUESTED_PORT,
|
|
1973
2075
|
started: true,
|
|
1974
2076
|
includesWebApp: true,
|
|
2077
|
+
shellMode: "web",
|
|
1975
2078
|
});
|
|
1976
2079
|
expect(edge.killed()).toBe(true);
|
|
1977
2080
|
const conf = realFs.readFileSync(ingressConfPath(ws), "utf-8");
|
|
@@ -1996,6 +2099,7 @@ describe("ensureTunnelEdge", () => {
|
|
|
1996
2099
|
port: REQUESTED_PORT,
|
|
1997
2100
|
started: true,
|
|
1998
2101
|
includesWebApp: true,
|
|
2102
|
+
shellMode: "web",
|
|
1999
2103
|
});
|
|
2000
2104
|
expect(edge.killed()).toBe(true);
|
|
2001
2105
|
const conf = realFs.readFileSync(ingressConfPath(ws), "utf-8");
|
|
@@ -2018,6 +2122,7 @@ describe("ensureTunnelEdge", () => {
|
|
|
2018
2122
|
port: REQUESTED_PORT,
|
|
2019
2123
|
started: true,
|
|
2020
2124
|
includesWebApp: true,
|
|
2125
|
+
shellMode: "web",
|
|
2021
2126
|
});
|
|
2022
2127
|
const conf = realFs.readFileSync(ingressConfPath(ws), "utf-8");
|
|
2023
2128
|
expect(conf).toContain("location ^~ /assistant/ {");
|
|
@@ -2073,6 +2178,7 @@ describe("ensureTunnelEdge", () => {
|
|
|
2073
2178
|
port: REQUESTED_PORT,
|
|
2074
2179
|
started: true,
|
|
2075
2180
|
includesWebApp: true,
|
|
2181
|
+
shellMode: "web",
|
|
2076
2182
|
});
|
|
2077
2183
|
const conf = realFs.readFileSync(ingressConfPath(ws), "utf-8");
|
|
2078
2184
|
expect(conf).toContain("location ^~ /assistant/ {");
|
|
@@ -2223,3 +2329,448 @@ describe("stopContainerTunnelEdge", () => {
|
|
|
2223
2329
|
expect(await stopContainerTunnelEdge(containerEntry(7930))).toBe(false);
|
|
2224
2330
|
});
|
|
2225
2331
|
});
|
|
2332
|
+
|
|
2333
|
+
describe("assistant metadata API", () => {
|
|
2334
|
+
function config(id = "assistant-1") {
|
|
2335
|
+
return buildIngressNginxConfig({
|
|
2336
|
+
gatewayPort: 7830,
|
|
2337
|
+
listenPort: 7840,
|
|
2338
|
+
remoteWebIngress: {
|
|
2339
|
+
webDistDir: "/tmp/web",
|
|
2340
|
+
version: "1.2.3",
|
|
2341
|
+
assistantId: id,
|
|
2342
|
+
assistantName: "My assistant",
|
|
2343
|
+
config: { appAllowedOrigins: ["https://parent.example"] },
|
|
2344
|
+
},
|
|
2345
|
+
});
|
|
2346
|
+
}
|
|
2347
|
+
function body(conf: string, path: string, status = 200): unknown {
|
|
2348
|
+
const location = conf
|
|
2349
|
+
.split(`location = "${path}" {`)[1]
|
|
2350
|
+
?.split("\n }")[0];
|
|
2351
|
+
const value = location?.match(/return (200|409) ("(?:\\.|[^"\\])*");/);
|
|
2352
|
+
expect(Number(value?.[1])).toBe(status);
|
|
2353
|
+
if (!value) {
|
|
2354
|
+
throw new Error(`Missing metadata for ${path}`);
|
|
2355
|
+
}
|
|
2356
|
+
return JSON.parse(JSON.parse(value[2]));
|
|
2357
|
+
}
|
|
2358
|
+
test("lists only this host's assistant and returns its actual UI version and URL", () => {
|
|
2359
|
+
const conf = config();
|
|
2360
|
+
expect(body(conf, "/v1/assistants/")).toEqual({
|
|
2361
|
+
results: [{ id: "assistant-1", name: "My assistant" }],
|
|
2362
|
+
next: null,
|
|
2363
|
+
});
|
|
2364
|
+
expect(body(conf, "/v1/assistants/assistant-1/")).toEqual({
|
|
2365
|
+
id: "assistant-1",
|
|
2366
|
+
name: "My assistant",
|
|
2367
|
+
status: "active",
|
|
2368
|
+
version: "1.2.3",
|
|
2369
|
+
url: "assistant/",
|
|
2370
|
+
});
|
|
2371
|
+
expect(conf).not.toContain('location = "/v1/assistants/other/"');
|
|
2372
|
+
});
|
|
2373
|
+
test("metadata supports both slash forms and only read methods", () => {
|
|
2374
|
+
const conf = config();
|
|
2375
|
+
for (const path of [
|
|
2376
|
+
"/v1/assistants",
|
|
2377
|
+
"/v1/assistants/",
|
|
2378
|
+
"/v1/assistants/assistant-1",
|
|
2379
|
+
"/v1/assistants/assistant-1/",
|
|
2380
|
+
]) {
|
|
2381
|
+
expect(body(conf, path)).toBeDefined();
|
|
2382
|
+
}
|
|
2383
|
+
expect(conf).toContain(
|
|
2384
|
+
"if ($request_method !~ ^(GET|HEAD)$) { return 405; }",
|
|
2385
|
+
);
|
|
2386
|
+
expect(conf).toContain(
|
|
2387
|
+
'"https://parent.example" "https://parent.example";',
|
|
2388
|
+
);
|
|
2389
|
+
expect(conf).toContain(
|
|
2390
|
+
"add_header Access-Control-Allow-Origin $cadet_app_origin always;",
|
|
2391
|
+
);
|
|
2392
|
+
});
|
|
2393
|
+
test("unknown identity yields an empty list without inventing an assistant ID", () => {
|
|
2394
|
+
const conf = buildIngressNginxConfig({
|
|
2395
|
+
gatewayPort: 7830,
|
|
2396
|
+
listenPort: 7840,
|
|
2397
|
+
remoteWebIngress: { webDistDir: "/tmp/web" },
|
|
2398
|
+
});
|
|
2399
|
+
expect(body(conf, "/v1/assistants/")).toEqual({ results: [], next: null });
|
|
2400
|
+
});
|
|
2401
|
+
test("a known assistant without a UI bundle returns 409 on both detail paths", () => {
|
|
2402
|
+
const conf = buildIngressNginxConfig({
|
|
2403
|
+
gatewayPort: 7830,
|
|
2404
|
+
listenPort: 7840,
|
|
2405
|
+
remoteWebIngress: { webDistDir: "/tmp/web", assistantId: "assistant-1" },
|
|
2406
|
+
});
|
|
2407
|
+
expect(body(conf, "/v1/assistants/")).toEqual({
|
|
2408
|
+
results: [{ id: "assistant-1" }],
|
|
2409
|
+
next: null,
|
|
2410
|
+
});
|
|
2411
|
+
for (const path of [
|
|
2412
|
+
"/v1/assistants/assistant-1",
|
|
2413
|
+
"/v1/assistants/assistant-1/",
|
|
2414
|
+
]) {
|
|
2415
|
+
expect(body(conf, path, 409)).toEqual({
|
|
2416
|
+
detail: "assistant is not ready",
|
|
2417
|
+
});
|
|
2418
|
+
}
|
|
2419
|
+
});
|
|
2420
|
+
test("invalid browser origins fail before nginx starts", () => {
|
|
2421
|
+
expect(() =>
|
|
2422
|
+
buildIngressNginxConfig({
|
|
2423
|
+
gatewayPort: 7830,
|
|
2424
|
+
listenPort: 7840,
|
|
2425
|
+
remoteWebIngress: {
|
|
2426
|
+
webDistDir: "/tmp/web",
|
|
2427
|
+
config: { appAllowedOrigins: ["https://parent.example/path"] },
|
|
2428
|
+
},
|
|
2429
|
+
}),
|
|
2430
|
+
).toThrow("App origins");
|
|
2431
|
+
});
|
|
2432
|
+
});
|
|
2433
|
+
|
|
2434
|
+
const APP_INDEX_SUFFIX = join("standalone", "index.html");
|
|
2435
|
+
const FAKE_APP_INDEX_HTML =
|
|
2436
|
+
"<!doctype html><html><head><title>app</title></head><body>app-shell</body></html>";
|
|
2437
|
+
|
|
2438
|
+
/**
|
|
2439
|
+
* Force findAppDistDir() to resolve a standalone bundle. The web-dist mock
|
|
2440
|
+
* below always pairs with it: app mode serves both bundles.
|
|
2441
|
+
*/
|
|
2442
|
+
function mockAppDistPresent(): void {
|
|
2443
|
+
const previousExists = existsSyncMock.getMockImplementation();
|
|
2444
|
+
existsSyncMock.mockImplementation((path) =>
|
|
2445
|
+
String(path).endsWith(APP_INDEX_SUFFIX)
|
|
2446
|
+
? true
|
|
2447
|
+
: previousExists
|
|
2448
|
+
? previousExists(path)
|
|
2449
|
+
: realFs.existsSync(path),
|
|
2450
|
+
);
|
|
2451
|
+
const previousRead = readFileSyncMock.getMockImplementation();
|
|
2452
|
+
readFileSyncMock.mockImplementation(((path, options) =>
|
|
2453
|
+
String(path).endsWith(APP_INDEX_SUFFIX)
|
|
2454
|
+
? FAKE_APP_INDEX_HTML
|
|
2455
|
+
: previousRead
|
|
2456
|
+
? previousRead(path as never, options as never)
|
|
2457
|
+
: realFs.readFileSync(
|
|
2458
|
+
path as never,
|
|
2459
|
+
options as never,
|
|
2460
|
+
)) as typeof readFileSync);
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
/** The fingerprint the edge must record for an app-mode SPA config. */
|
|
2464
|
+
function appConfigHash(
|
|
2465
|
+
opts: {
|
|
2466
|
+
assistantName?: string;
|
|
2467
|
+
assistantId?: string;
|
|
2468
|
+
} = {},
|
|
2469
|
+
): string {
|
|
2470
|
+
return createHash("sha256")
|
|
2471
|
+
.update(
|
|
2472
|
+
JSON.stringify({
|
|
2473
|
+
template: EDGE_TEMPLATE_VERSION,
|
|
2474
|
+
config: {
|
|
2475
|
+
remote: "custom",
|
|
2476
|
+
apiBaseUrl: "/v1",
|
|
2477
|
+
...(opts.assistantName ? { assistantName: opts.assistantName } : {}),
|
|
2478
|
+
...(opts.assistantId ? { assistantId: opts.assistantId } : {}),
|
|
2479
|
+
hubUrl: PRODUCTION_HUB_URL,
|
|
2480
|
+
},
|
|
2481
|
+
webDistDir: findWebDistDir(),
|
|
2482
|
+
version: "1.2.3",
|
|
2483
|
+
shellMode: "app",
|
|
2484
|
+
appDistDir: findAppDistDir(),
|
|
2485
|
+
appIndexHtml: FAKE_APP_INDEX_HTML,
|
|
2486
|
+
indexHtml: FAKE_INDEX_HTML,
|
|
2487
|
+
}),
|
|
2488
|
+
)
|
|
2489
|
+
.digest("hex");
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
describe("app shell mode", () => {
|
|
2493
|
+
pinProductionEnvironment();
|
|
2494
|
+
|
|
2495
|
+
function appConf() {
|
|
2496
|
+
return buildIngressNginxConfig({
|
|
2497
|
+
gatewayPort: 7830,
|
|
2498
|
+
listenPort: 7840,
|
|
2499
|
+
remoteWebIngress: {
|
|
2500
|
+
webDistDir: "/tmp/cadet web/dist",
|
|
2501
|
+
appDistDir: "/tmp/cadet app/dist/standalone",
|
|
2502
|
+
version: "1.2.3",
|
|
2503
|
+
assistantId: "assistant-1",
|
|
2504
|
+
config: {},
|
|
2505
|
+
},
|
|
2506
|
+
});
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
test("serves the app bundle as the outer shell from the origin root", () => {
|
|
2510
|
+
const conf = appConf();
|
|
2511
|
+
// The injected app document is internal-only and every shell route
|
|
2512
|
+
// (/, /account/*, /logout, and the /assistant/* host routes) falls back
|
|
2513
|
+
// to it through the named location.
|
|
2514
|
+
expect(conf).toContain("location = /__cadet-app-index.html {");
|
|
2515
|
+
// Without an explicit app index path, the config aliases the bundle's own
|
|
2516
|
+
// index; the edge start path points this at the ingress dir (below).
|
|
2517
|
+
expect(conf).toContain('alias "/tmp/cadet app/dist/standalone/index.html"');
|
|
2518
|
+
expect(conf).toContain("location @cadet_app_shell {");
|
|
2519
|
+
expect(conf).toContain("rewrite ^ /__cadet-app-index.html last;");
|
|
2520
|
+
expect(conf).toContain(
|
|
2521
|
+
"location = / {\n rewrite ^ /__cadet-app-index.html last;\n }",
|
|
2522
|
+
);
|
|
2523
|
+
expect(conf).toContain('alias "/tmp/cadet app/dist/standalone/assets/";');
|
|
2524
|
+
expect(conf).toContain('alias "/tmp/cadet app/dist/standalone/";');
|
|
2525
|
+
// The shell owns the /assistant/* host routes; the exact mounts load the
|
|
2526
|
+
// app document rather than the web SPA.
|
|
2527
|
+
expect(conf).toContain(
|
|
2528
|
+
"location = /assistant {\n rewrite ^ /__cadet-app-index.html last;\n }",
|
|
2529
|
+
);
|
|
2530
|
+
expect(conf).toContain(
|
|
2531
|
+
"location = /assistant/ {\n rewrite ^ /__cadet-app-index.html last;\n }",
|
|
2532
|
+
);
|
|
2533
|
+
expect(conf).toContain("try_files $uri @cadet_app_shell;");
|
|
2534
|
+
// No recursive loading: the web SPA's exact mount is gone from the shell.
|
|
2535
|
+
expect(conf).not.toContain(
|
|
2536
|
+
"location = /assistant/ {\n rewrite ^ /assistant/__remote-index.html last;\n }",
|
|
2537
|
+
);
|
|
2538
|
+
});
|
|
2539
|
+
|
|
2540
|
+
test("iframes the web UI from the distinct /web/assistant namespace", () => {
|
|
2541
|
+
const conf = appConf();
|
|
2542
|
+
expect(conf).toContain('location = "/v1/assistants/assistant-1/"');
|
|
2543
|
+
expect(conf).toContain('\\"url\\":\\"web/assistant/\\"');
|
|
2544
|
+
expect(conf).toContain('\\"version\\":\\"1.2.3\\"');
|
|
2545
|
+
expect(conf).toContain(
|
|
2546
|
+
"location = /web/assistant/ {\n rewrite ^ /web/assistant/__remote-index.html last;\n }",
|
|
2547
|
+
);
|
|
2548
|
+
expect(conf).toContain("location = /web/assistant/__remote-index.html {");
|
|
2549
|
+
expect(conf).toContain('alias "/tmp/cadet web/dist/index.html"');
|
|
2550
|
+
expect(conf).toContain("location = /web/assistant/__config {");
|
|
2551
|
+
expect(conf).toContain("location ^~ /web/assistant/ {");
|
|
2552
|
+
expect(conf).toContain(
|
|
2553
|
+
"try_files $uri $uri/ /web/assistant/__remote-index.html;",
|
|
2554
|
+
);
|
|
2555
|
+
// The web bundle's asset URLs keep their baked /assistant/ base.
|
|
2556
|
+
expect(conf).toContain(
|
|
2557
|
+
'location ^~ /assistant/assets/ {\n gzip on;\n alias "/tmp/cadet web/dist/assets/"',
|
|
2558
|
+
);
|
|
2559
|
+
// Prefixed API paths the web UI builds from its serving prefix.
|
|
2560
|
+
expect(conf).toContain("location ^~ /web/v1/ {");
|
|
2561
|
+
expect(conf).toContain("proxy_pass http://127.0.0.1:7830/v1/;");
|
|
2562
|
+
// Nothing else under /web exists, so the namespace cannot serve the shell
|
|
2563
|
+
// (or anything else) recursively.
|
|
2564
|
+
expect(conf).toContain("location ^~ /web/ {\n return 404;\n }");
|
|
2565
|
+
});
|
|
2566
|
+
|
|
2567
|
+
test("blocks the sensitive gateway routes under both prefixes", () => {
|
|
2568
|
+
const conf = appConf();
|
|
2569
|
+
for (const route of ["/v1/pair", "/v1/devices", "/v1/guardian/init"]) {
|
|
2570
|
+
expect(conf).toContain(`location = ${route} { return 404; }`);
|
|
2571
|
+
expect(conf).toContain(`location = /web${route} { return 404; }`);
|
|
2572
|
+
}
|
|
2573
|
+
expect(conf).toContain(
|
|
2574
|
+
"location ^~ /web/assistant/__local/ { return 404; }",
|
|
2575
|
+
);
|
|
2576
|
+
});
|
|
2577
|
+
|
|
2578
|
+
test("proxies runtime API paths verbatim for both surfaces", () => {
|
|
2579
|
+
const conf = appConf();
|
|
2580
|
+
expect(conf).toContain(
|
|
2581
|
+
"location ^~ /v1/ {\n proxy_pass http://127.0.0.1:7830;",
|
|
2582
|
+
);
|
|
2583
|
+
expect(conf).toContain(
|
|
2584
|
+
"location ^~ /webhooks/ {\n proxy_pass http://127.0.0.1:7830;",
|
|
2585
|
+
);
|
|
2586
|
+
expect(conf).toContain(
|
|
2587
|
+
"location = /healthz {\n proxy_pass http://127.0.0.1:7830;",
|
|
2588
|
+
);
|
|
2589
|
+
});
|
|
2590
|
+
|
|
2591
|
+
test("app mode starts, injects both documents, and records the app fingerprint", async () => {
|
|
2592
|
+
const ws = makeWorkspace();
|
|
2593
|
+
mockNginxInstalled();
|
|
2594
|
+
mockNginxSpawn();
|
|
2595
|
+
mockWebDistPresent();
|
|
2596
|
+
mockAppDistPresent();
|
|
2597
|
+
fetchCurrentVersionMock.mockResolvedValue("1.2.3");
|
|
2598
|
+
|
|
2599
|
+
const result = await startRemoteWebIngress({
|
|
2600
|
+
workspaceDir: ws,
|
|
2601
|
+
gatewayPort: 7830,
|
|
2602
|
+
listenPort: 7845,
|
|
2603
|
+
shellMode: "app",
|
|
2604
|
+
assistantId: "assistant-1",
|
|
2605
|
+
});
|
|
2606
|
+
|
|
2607
|
+
expect(result.status).toBe("started");
|
|
2608
|
+
const ingressDir = join(ws, "data", "ingress");
|
|
2609
|
+
const appIndex = realFs.readFileSync(
|
|
2610
|
+
join(ingressDir, "app-index.html"),
|
|
2611
|
+
"utf-8",
|
|
2612
|
+
);
|
|
2613
|
+
expect(appIndex).toContain("__CADET_CONFIG__");
|
|
2614
|
+
expect(appIndex).toContain(`"assistantId":"assistant-1"`);
|
|
2615
|
+
const conf = realFs.readFileSync(ingressConfPath(ws), "utf-8");
|
|
2616
|
+
expect(conf).toContain("location = /__cadet-app-index.html {");
|
|
2617
|
+
expect(conf).toContain('\\"url\\":\\"web/assistant/\\"');
|
|
2618
|
+
expect(readConfig(ws).ingress?.nginx).toMatchObject({
|
|
2619
|
+
listenPort: 7845,
|
|
2620
|
+
includeWebApp: true,
|
|
2621
|
+
gatewayPort: 7830,
|
|
2622
|
+
remoteWebConfigHash: appConfigHash({ assistantId: "assistant-1" }),
|
|
2623
|
+
});
|
|
2624
|
+
});
|
|
2625
|
+
|
|
2626
|
+
test("fails fast when the app bundle is not built", async () => {
|
|
2627
|
+
const ws = makeWorkspace();
|
|
2628
|
+
mockNginxInstalled();
|
|
2629
|
+
mockNginxSpawn();
|
|
2630
|
+
mockWebDistPresent();
|
|
2631
|
+
// A real build in the checkout would satisfy the lookup; the failure
|
|
2632
|
+
// path needs the lookup to resolve nothing, regardless of checkout state.
|
|
2633
|
+
// Web dist stays present so the app-dist check is the one that fires.
|
|
2634
|
+
const previousExists = existsSyncMock.getMockImplementation();
|
|
2635
|
+
existsSyncMock.mockImplementation((path) =>
|
|
2636
|
+
String(path).endsWith(APP_INDEX_SUFFIX)
|
|
2637
|
+
? false
|
|
2638
|
+
: previousExists
|
|
2639
|
+
? previousExists(path)
|
|
2640
|
+
: realFs.existsSync(path),
|
|
2641
|
+
);
|
|
2642
|
+
|
|
2643
|
+
const result = await startRemoteWebIngress({
|
|
2644
|
+
workspaceDir: ws,
|
|
2645
|
+
gatewayPort: 7830,
|
|
2646
|
+
listenPort: 7845,
|
|
2647
|
+
shellMode: "app",
|
|
2648
|
+
});
|
|
2649
|
+
|
|
2650
|
+
expect(result).toEqual({ status: "app-dist-missing" });
|
|
2651
|
+
expect(realFs.existsSync(ingressConfPath(ws))).toBe(false);
|
|
2652
|
+
});
|
|
2653
|
+
|
|
2654
|
+
test("restarting an edge between shell modes rotates onto the requested mode", async () => {
|
|
2655
|
+
const ws = makeWorkspace();
|
|
2656
|
+
mockNginxInstalled();
|
|
2657
|
+
mockNginxSpawn();
|
|
2658
|
+
mockWebDistPresent();
|
|
2659
|
+
mockAppDistPresent();
|
|
2660
|
+
// A running web-mode edge whose fingerprint predates app mode.
|
|
2661
|
+
mockRunningEdge(ws, {
|
|
2662
|
+
listenPort: 7845,
|
|
2663
|
+
includeWebApp: true,
|
|
2664
|
+
gatewayPort: 7830,
|
|
2665
|
+
remoteWebConfigHash: spaConfigHash({ assistantId: "assistant-1" }),
|
|
2666
|
+
});
|
|
2667
|
+
const edge = mockKillableNginx(123_460);
|
|
2668
|
+
|
|
2669
|
+
const result = await startRemoteWebIngress({
|
|
2670
|
+
workspaceDir: ws,
|
|
2671
|
+
gatewayPort: 7830,
|
|
2672
|
+
listenPort: 7845,
|
|
2673
|
+
shellMode: "app",
|
|
2674
|
+
assistantId: "assistant-1",
|
|
2675
|
+
});
|
|
2676
|
+
|
|
2677
|
+
expect(result.status).toBe("started");
|
|
2678
|
+
expect(edge.killed()).toBe(true);
|
|
2679
|
+
expect(readConfig(ws).ingress?.nginx).toMatchObject({
|
|
2680
|
+
remoteWebConfigHash: appConfigHash({ assistantId: "assistant-1" }),
|
|
2681
|
+
});
|
|
2682
|
+
});
|
|
2683
|
+
|
|
2684
|
+
test("reuses an already-running app-mode edge", async () => {
|
|
2685
|
+
const ws = makeWorkspace();
|
|
2686
|
+
mockNginxInstalled();
|
|
2687
|
+
mockNginxSpawn();
|
|
2688
|
+
mockWebDistPresent();
|
|
2689
|
+
mockAppDistPresent();
|
|
2690
|
+
mockRunningEdge(ws, {
|
|
2691
|
+
listenPort: 7845,
|
|
2692
|
+
includeWebApp: true,
|
|
2693
|
+
gatewayPort: 7830,
|
|
2694
|
+
remoteWebConfigHash: appConfigHash({ assistantId: "assistant-1" }),
|
|
2695
|
+
});
|
|
2696
|
+
const edge = mockKillableNginx(123_460);
|
|
2697
|
+
|
|
2698
|
+
const result = await startRemoteWebIngress({
|
|
2699
|
+
workspaceDir: ws,
|
|
2700
|
+
gatewayPort: 7830,
|
|
2701
|
+
listenPort: 7845,
|
|
2702
|
+
shellMode: "app",
|
|
2703
|
+
assistantId: "assistant-1",
|
|
2704
|
+
});
|
|
2705
|
+
|
|
2706
|
+
expect(result).toMatchObject({
|
|
2707
|
+
status: "already-running",
|
|
2708
|
+
listenPort: 7845,
|
|
2709
|
+
});
|
|
2710
|
+
expect(edge.killed()).toBe(false);
|
|
2711
|
+
expect(spawnMock).not.toHaveBeenCalled();
|
|
2712
|
+
});
|
|
2713
|
+
|
|
2714
|
+
test("ensureTunnelEdge reads the persisted shell mode from the lockfile", async () => {
|
|
2715
|
+
const ws = makeWorkspace();
|
|
2716
|
+
mockNginxInstalled();
|
|
2717
|
+
mockNginxSpawn();
|
|
2718
|
+
mockWebDistPresent();
|
|
2719
|
+
mockAppDistPresent();
|
|
2720
|
+
const originalLockfileDir = process.env.CADET_LOCKFILE_DIR;
|
|
2721
|
+
const lockfileDir = join(ws, "lockfile");
|
|
2722
|
+
mkdirSync(lockfileDir, { recursive: true });
|
|
2723
|
+
process.env.CADET_LOCKFILE_DIR = lockfileDir;
|
|
2724
|
+
try {
|
|
2725
|
+
saveShellMode("app");
|
|
2726
|
+
|
|
2727
|
+
const result = await ensureTunnelEdge({
|
|
2728
|
+
assistantId: "assistant-1",
|
|
2729
|
+
workspaceDir: ws,
|
|
2730
|
+
gatewayPort: 7830,
|
|
2731
|
+
});
|
|
2732
|
+
|
|
2733
|
+
expect(result.shellMode).toBe("app");
|
|
2734
|
+
} finally {
|
|
2735
|
+
if (originalLockfileDir === undefined) {
|
|
2736
|
+
delete process.env.CADET_LOCKFILE_DIR;
|
|
2737
|
+
} else {
|
|
2738
|
+
process.env.CADET_LOCKFILE_DIR = originalLockfileDir;
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
const conf = realFs.readFileSync(ingressConfPath(ws), "utf-8");
|
|
2742
|
+
expect(conf).toContain("location = /__cadet-app-index.html {");
|
|
2743
|
+
expect(conf).toContain('\\"url\\":\\"web/assistant/\\"');
|
|
2744
|
+
});
|
|
2745
|
+
|
|
2746
|
+
test("ensureTunnelEdge defaults to the web shell", async () => {
|
|
2747
|
+
const ws = makeWorkspace();
|
|
2748
|
+
mockNginxInstalled();
|
|
2749
|
+
mockNginxSpawn();
|
|
2750
|
+
mockWebDistPresent();
|
|
2751
|
+
// Hermetic lockfile dir: no edgeShell key means the web default, even if
|
|
2752
|
+
// the machine running the tests has its own shell selection persisted.
|
|
2753
|
+
const originalLockfileDir = process.env.CADET_LOCKFILE_DIR;
|
|
2754
|
+
const lockfileDir = join(ws, "lockfile");
|
|
2755
|
+
mkdirSync(lockfileDir, { recursive: true });
|
|
2756
|
+
process.env.CADET_LOCKFILE_DIR = lockfileDir;
|
|
2757
|
+
try {
|
|
2758
|
+
const result = await ensureTunnelEdge({
|
|
2759
|
+
assistantId: "assistant-1",
|
|
2760
|
+
workspaceDir: ws,
|
|
2761
|
+
gatewayPort: 7830,
|
|
2762
|
+
});
|
|
2763
|
+
|
|
2764
|
+
expect(result.shellMode).toBe("web");
|
|
2765
|
+
} finally {
|
|
2766
|
+
if (originalLockfileDir === undefined) {
|
|
2767
|
+
delete process.env.CADET_LOCKFILE_DIR;
|
|
2768
|
+
} else {
|
|
2769
|
+
process.env.CADET_LOCKFILE_DIR = originalLockfileDir;
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
const conf = realFs.readFileSync(ingressConfPath(ws), "utf-8");
|
|
2773
|
+
expect(conf).not.toContain("__cadet-app-index");
|
|
2774
|
+
expect(conf).toContain('\\"url\\":\\"assistant/\\"');
|
|
2775
|
+
});
|
|
2776
|
+
});
|