@openparachute/hub 0.6.2 → 0.6.3-rc.2
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 +87 -35
- package/package.json +1 -1
- package/src/__tests__/api-hub-upgrade.test.ts +690 -0
- package/src/__tests__/api-modules-ops.test.ts +359 -3
- package/src/__tests__/api-modules.test.ts +54 -0
- package/src/__tests__/expose-cloudflare.test.ts +163 -72
- package/src/__tests__/expose-off-auto.test.ts +26 -1
- package/src/__tests__/expose.test.ts +260 -240
- package/src/__tests__/hub-control.test.ts +1 -242
- package/src/__tests__/hub-server.test.ts +64 -0
- package/src/__tests__/hub-unit.test.ts +574 -0
- package/src/__tests__/init.test.ts +219 -2
- package/src/__tests__/lifecycle.test.ts +416 -1448
- package/src/__tests__/managed-unit.test.ts +575 -0
- package/src/__tests__/migrate-cutover.test.ts +840 -0
- package/src/__tests__/migrate-offer.test.ts +240 -0
- package/src/__tests__/migrate.test.ts +132 -0
- package/src/__tests__/module-ops-client.test.ts +556 -0
- package/src/__tests__/port-probe.test.ts +23 -0
- package/src/__tests__/setup-wizard.test.ts +130 -0
- package/src/__tests__/status-supervisor.test.ts +504 -0
- package/src/__tests__/status.test.ts +157 -708
- package/src/__tests__/supervisor.test.ts +471 -6
- package/src/__tests__/upgrade.test.ts +351 -5
- package/src/api-hub-upgrade.ts +384 -0
- package/src/api-hub.ts +2 -1
- package/src/api-modules-ops.ts +221 -0
- package/src/api-modules.ts +18 -2
- package/src/cli.ts +97 -12
- package/src/cloudflare/connector-service.ts +117 -322
- package/src/commands/expose-cloudflare.ts +63 -71
- package/src/commands/expose-supervisor.ts +247 -0
- package/src/commands/expose.ts +59 -48
- package/src/commands/init.ts +225 -12
- package/src/commands/lifecycle.ts +455 -816
- package/src/commands/migrate-cutover.ts +837 -0
- package/src/commands/migrate.ts +71 -2
- package/src/commands/serve-boot.ts +71 -25
- package/src/commands/status.ts +535 -235
- package/src/commands/upgrade.ts +100 -2
- package/src/help.ts +128 -68
- package/src/hub-control.ts +23 -162
- package/src/hub-server.ts +39 -0
- package/src/hub-unit.ts +735 -0
- package/src/hub-upgrade-helper.ts +306 -0
- package/src/hub-upgrade-mode.ts +209 -0
- package/src/hub-upgrade-status.ts +150 -0
- package/src/managed-unit.ts +692 -0
- package/src/migrate-offer.ts +186 -0
- package/src/module-ops-client.ts +457 -0
- package/src/port-probe.ts +50 -0
- package/src/process-state.ts +19 -3
- package/src/setup-wizard.ts +80 -1
- package/src/supervisor.ts +389 -38
- package/web/ui/dist/assets/index-D_6AFvZy.js +61 -0
- package/web/ui/dist/assets/{index-BiBlvEaj.css → index-mz8XcVPP.css} +1 -1
- package/web/ui/dist/index.html +2 -2
- package/web/ui/dist/assets/index-CIN3mnmf.js +0 -61
|
@@ -15,9 +15,12 @@ import {
|
|
|
15
15
|
exposeCloudflareOff,
|
|
16
16
|
exposeCloudflareUp,
|
|
17
17
|
} from "../commands/expose-cloudflare.ts";
|
|
18
|
+
import type { ExposeSupervisorOpts } from "../commands/expose-supervisor.ts";
|
|
18
19
|
import { readEnvFileValues } from "../env-file.ts";
|
|
19
20
|
import { readExposeState } from "../expose-state.ts";
|
|
20
21
|
import { writeHubPort } from "../hub-control.ts";
|
|
22
|
+
import type { EnsureHubUnitOpts } from "../hub-unit.ts";
|
|
23
|
+
import { type ModuleOp, ModuleOpHttpError } from "../module-ops-client.ts";
|
|
21
24
|
import type { CommandResult, Runner } from "../tailscale/run.ts";
|
|
22
25
|
|
|
23
26
|
// Default seeded hub port used by tests with `skipHub: true`. The cloudflared
|
|
@@ -283,23 +286,22 @@ describe("exposeCloudflareUp", () => {
|
|
|
283
286
|
}
|
|
284
287
|
});
|
|
285
288
|
|
|
286
|
-
test("persists the public hub origin to vault/.env + restarts vault (Cloudflare 401 fix)", async () => {
|
|
289
|
+
test("persists the public hub origin to vault/.env + restarts vault via the supervisor (Cloudflare 401 fix)", async () => {
|
|
287
290
|
// The Cloudflare 401 P0: the cloudflare path wrote expose-state.json but —
|
|
288
291
|
// unlike the Tailscale path, which auto-restarts vault and so flows the
|
|
289
|
-
// public origin into vault/.env
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
292
|
+
// public origin into vault/.env — never touched vault's .env or restarted
|
|
293
|
+
// it. The launchd/systemd daemon kept booting vault with NO
|
|
294
|
+
// PARACHUTE_HUB_ORIGIN → vault fell back to loopback as its expected issuer →
|
|
295
|
+
// every hub-minted token (iss=public) failed the iss check → 401.
|
|
296
|
+
//
|
|
297
|
+
// Phase 5b: the restart goes through the running Supervisor
|
|
298
|
+
// (`driveModuleOp("vault", "restart")`) — the helper also persists the
|
|
299
|
+
// durable .env. This asserts the durable .env write + the supervised restart
|
|
300
|
+
// (no longer a pidfile-gated detached restart; the supervisor decides
|
|
301
|
+
// liveness, and a not-supervised vault surfaces as a tolerated 404 — see the
|
|
302
|
+
// Phase 4 dual-dispatch suite).
|
|
295
303
|
const env = makeEnv();
|
|
296
304
|
try {
|
|
297
|
-
// Seed vault as "running" so the restart branch fires. PID lives at
|
|
298
|
-
// <configDir>/vault/run/vault.pid (see process-state.ts:pidPath).
|
|
299
|
-
const vaultRun = join(env.configDir, "vault", "run");
|
|
300
|
-
require("node:fs").mkdirSync(vaultRun, { recursive: true });
|
|
301
|
-
writeFileSync(join(vaultRun, "vault.pid"), "99001");
|
|
302
|
-
|
|
303
305
|
const uuid = "ffffffff-0000-0000-0000-000000000006";
|
|
304
306
|
const { runner } = queueRunner([
|
|
305
307
|
{ code: 0, stdout: "cloudflared 2024.1.0\n", stderr: "" },
|
|
@@ -312,14 +314,12 @@ describe("exposeCloudflareUp", () => {
|
|
|
312
314
|
{ code: 0, stdout: "", stderr: "" },
|
|
313
315
|
]);
|
|
314
316
|
const { spawner } = fakeSpawner(42300);
|
|
315
|
-
const
|
|
317
|
+
const sup = makeCfSupervisorStub();
|
|
316
318
|
|
|
317
319
|
const code = await exposeCloudflareUp("gitcoin-parachute.unforced.dev", {
|
|
318
320
|
runner,
|
|
319
321
|
spawner,
|
|
320
|
-
|
|
321
|
-
// "running" and the restart branch executes.
|
|
322
|
-
alive: (pid) => pid === 99001,
|
|
322
|
+
alive: () => false,
|
|
323
323
|
kill: () => {},
|
|
324
324
|
log: () => {},
|
|
325
325
|
manifestPath: env.manifestPath,
|
|
@@ -330,10 +330,7 @@ describe("exposeCloudflareUp", () => {
|
|
|
330
330
|
cloudflaredHome: env.cloudflaredHome,
|
|
331
331
|
configDir: env.configDir,
|
|
332
332
|
skipHub: true,
|
|
333
|
-
|
|
334
|
-
restarted.push(short);
|
|
335
|
-
return 0;
|
|
336
|
-
},
|
|
333
|
+
supervisor: sup.opts,
|
|
337
334
|
});
|
|
338
335
|
|
|
339
336
|
expect(code).toBe(0);
|
|
@@ -342,57 +339,8 @@ describe("exposeCloudflareUp", () => {
|
|
|
342
339
|
expect(readEnvFileValues(join(env.configDir, "vault", ".env")).PARACHUTE_HUB_ORIGIN).toBe(
|
|
343
340
|
"https://gitcoin-parachute.unforced.dev",
|
|
344
341
|
);
|
|
345
|
-
// Live half:
|
|
346
|
-
expect(
|
|
347
|
-
} finally {
|
|
348
|
-
env.cleanup();
|
|
349
|
-
}
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
test("persists vault/.env but does NOT restart when vault isn't running", async () => {
|
|
353
|
-
// No vault pidfile → processState() !== "running" → no restart, but the
|
|
354
|
-
// durable .env write still happens so the next daemon boot is correct.
|
|
355
|
-
const env = makeEnv();
|
|
356
|
-
try {
|
|
357
|
-
const uuid = "ffffffff-0000-0000-0000-000000000007";
|
|
358
|
-
const { runner } = queueRunner([
|
|
359
|
-
{ code: 0, stdout: "cloudflared 2024.1.0\n", stderr: "" },
|
|
360
|
-
{ code: 0, stdout: "[]", stderr: "" },
|
|
361
|
-
{
|
|
362
|
-
code: 0,
|
|
363
|
-
stdout: `Tunnel credentials written to ${env.cloudflaredHome}/${uuid}.json.\nCreated tunnel parachute with id ${uuid}\n`,
|
|
364
|
-
stderr: "",
|
|
365
|
-
},
|
|
366
|
-
{ code: 0, stdout: "", stderr: "" },
|
|
367
|
-
]);
|
|
368
|
-
const { spawner } = fakeSpawner(42301);
|
|
369
|
-
const restarted: string[] = [];
|
|
370
|
-
|
|
371
|
-
const code = await exposeCloudflareUp("gitcoin-parachute.unforced.dev", {
|
|
372
|
-
runner,
|
|
373
|
-
spawner,
|
|
374
|
-
alive: () => false,
|
|
375
|
-
kill: () => {},
|
|
376
|
-
log: () => {},
|
|
377
|
-
manifestPath: env.manifestPath,
|
|
378
|
-
statePath: env.statePath,
|
|
379
|
-
exposeStatePath: env.exposeStatePath,
|
|
380
|
-
configPath: env.configPath,
|
|
381
|
-
logPath: env.logPath,
|
|
382
|
-
cloudflaredHome: env.cloudflaredHome,
|
|
383
|
-
configDir: env.configDir,
|
|
384
|
-
skipHub: true,
|
|
385
|
-
restartService: async (short) => {
|
|
386
|
-
restarted.push(short);
|
|
387
|
-
return 0;
|
|
388
|
-
},
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
expect(code).toBe(0);
|
|
392
|
-
expect(readEnvFileValues(join(env.configDir, "vault", ".env")).PARACHUTE_HUB_ORIGIN).toBe(
|
|
393
|
-
"https://gitcoin-parachute.unforced.dev",
|
|
394
|
-
);
|
|
395
|
-
expect(restarted).toEqual([]);
|
|
342
|
+
// Live half: vault is restarted via the supervisor to re-read the new origin.
|
|
343
|
+
expect(sup.driveCalls).toEqual([{ short: "vault", op: "restart" }]);
|
|
396
344
|
} finally {
|
|
397
345
|
env.cleanup();
|
|
398
346
|
}
|
|
@@ -2017,3 +1965,146 @@ describe("reboot-persistent connector service wiring", () => {
|
|
|
2017
1965
|
}
|
|
2018
1966
|
});
|
|
2019
1967
|
});
|
|
1968
|
+
|
|
1969
|
+
// ---------------------------------------------------------------------------
|
|
1970
|
+
// Phase 4 dual-dispatch (design §4.3): unit-managed → "ensure the hub" ensures
|
|
1971
|
+
// the UNIT (not a detached spawn) and the post-route vault restart drives the
|
|
1972
|
+
// running Supervisor over the loopback module-ops API (firing the operator-
|
|
1973
|
+
// token self-heal). The cloudflared CONNECTOR unit is unchanged. The no-unit
|
|
1974
|
+
// arm keeps today's `persistVaultHubOrigin` + `restartService` behavior.
|
|
1975
|
+
// ---------------------------------------------------------------------------
|
|
1976
|
+
|
|
1977
|
+
interface CfExposeSupervisorStub {
|
|
1978
|
+
opts: ExposeSupervisorOpts;
|
|
1979
|
+
ensureCalls: number;
|
|
1980
|
+
driveCalls: Array<{ short: string; op: ModuleOp }>;
|
|
1981
|
+
selfHealCalls: Array<{ issuer: string }>;
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
function makeCfSupervisorStub(opts?: {
|
|
1985
|
+
ensureOutcome?: "already-up" | "started" | "no-manager";
|
|
1986
|
+
driveThrows?: () => unknown;
|
|
1987
|
+
}): CfExposeSupervisorStub {
|
|
1988
|
+
const stub: CfExposeSupervisorStub = {
|
|
1989
|
+
ensureCalls: 0,
|
|
1990
|
+
driveCalls: [],
|
|
1991
|
+
selfHealCalls: [],
|
|
1992
|
+
opts: {
|
|
1993
|
+
openDb: () => ({ close() {} }) as unknown as import("bun:sqlite").Database,
|
|
1994
|
+
ensureHubUnit: async (o: EnsureHubUnitOpts) => {
|
|
1995
|
+
stub.ensureCalls++;
|
|
1996
|
+
return { outcome: opts?.ensureOutcome ?? "already-up", port: o.port ?? 1939, messages: [] };
|
|
1997
|
+
},
|
|
1998
|
+
driveModuleOp: async (short, op) => {
|
|
1999
|
+
stub.driveCalls.push({ short, op });
|
|
2000
|
+
if (opts?.driveThrows) throw opts.driveThrows();
|
|
2001
|
+
return { status: 200, body: { short, state: { status: "running" } } };
|
|
2002
|
+
},
|
|
2003
|
+
selfHealOperatorTokenIssuer: async (_db, o) => {
|
|
2004
|
+
stub.selfHealCalls.push({ issuer: o.issuer });
|
|
2005
|
+
return { kind: "fresh" };
|
|
2006
|
+
},
|
|
2007
|
+
},
|
|
2008
|
+
};
|
|
2009
|
+
return stub;
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
describe("Phase 4 cloudflare expose dual-dispatch — unit-managed", () => {
|
|
2013
|
+
test("unit-managed → ensureHubUnit (not detached) + supervised vault restart + operator-token self-heal", async () => {
|
|
2014
|
+
const env = makeEnv();
|
|
2015
|
+
try {
|
|
2016
|
+
const uuid = "ffffffff-0000-0000-0000-0000000000a4";
|
|
2017
|
+
const { runner } = queueRunner([
|
|
2018
|
+
{ code: 0, stdout: "cloudflared 2024.1.0\n", stderr: "" }, // --version
|
|
2019
|
+
{ code: 0, stdout: "[]", stderr: "" }, // tunnel list
|
|
2020
|
+
{
|
|
2021
|
+
code: 0,
|
|
2022
|
+
stdout: `Tunnel credentials written to ${env.cloudflaredHome}/${uuid}.json.\nCreated tunnel parachute with id ${uuid}\n`,
|
|
2023
|
+
stderr: "",
|
|
2024
|
+
}, // tunnel create
|
|
2025
|
+
{ code: 0, stdout: "", stderr: "" }, // route dns
|
|
2026
|
+
]);
|
|
2027
|
+
const { spawner } = fakeSpawner(42400);
|
|
2028
|
+
const sup = makeCfSupervisorStub();
|
|
2029
|
+
const logs: string[] = [];
|
|
2030
|
+
|
|
2031
|
+
const code = await exposeCloudflareUp("gitcoin-parachute.unforced.dev", {
|
|
2032
|
+
runner,
|
|
2033
|
+
spawner,
|
|
2034
|
+
alive: () => false,
|
|
2035
|
+
kill: () => {},
|
|
2036
|
+
log: (l) => logs.push(l),
|
|
2037
|
+
manifestPath: env.manifestPath,
|
|
2038
|
+
statePath: env.statePath,
|
|
2039
|
+
exposeStatePath: env.exposeStatePath,
|
|
2040
|
+
configPath: env.configPath,
|
|
2041
|
+
logPath: env.logPath,
|
|
2042
|
+
cloudflaredHome: env.cloudflaredHome,
|
|
2043
|
+
configDir: env.configDir,
|
|
2044
|
+
// No skipHub → the hub-unit ensure runs (via the stub, no real hub).
|
|
2045
|
+
// Inert connector install so the cloudflared connector path is unchanged.
|
|
2046
|
+
installService: () => ({ outcome: "fallback", messages: [] }),
|
|
2047
|
+
supervisor: sup.opts,
|
|
2048
|
+
});
|
|
2049
|
+
|
|
2050
|
+
expect(code).toBe(0);
|
|
2051
|
+
// Ensured the hub UNIT (the stub), never a detached spawn.
|
|
2052
|
+
expect(sup.ensureCalls).toBe(1);
|
|
2053
|
+
// Vault restart drove the running Supervisor.
|
|
2054
|
+
expect(sup.driveCalls).toEqual([{ short: "vault", op: "restart" }]);
|
|
2055
|
+
// Operator-token issuer self-heal fired toward the public origin.
|
|
2056
|
+
expect(sup.selfHealCalls).toHaveLength(1);
|
|
2057
|
+
expect(sup.selfHealCalls[0]?.issuer).toBe("https://gitcoin-parachute.unforced.dev");
|
|
2058
|
+
// Durable .env still written (the helper persists it for vault).
|
|
2059
|
+
expect(readEnvFileValues(join(env.configDir, "vault", ".env")).PARACHUTE_HUB_ORIGIN).toBe(
|
|
2060
|
+
"https://gitcoin-parachute.unforced.dev",
|
|
2061
|
+
);
|
|
2062
|
+
expect(logs.join("\n")).toMatch(/hub unit up/);
|
|
2063
|
+
} finally {
|
|
2064
|
+
env.cleanup();
|
|
2065
|
+
}
|
|
2066
|
+
});
|
|
2067
|
+
|
|
2068
|
+
test("unit-managed: a not_supervised vault (404) is not a failure", async () => {
|
|
2069
|
+
const env = makeEnv();
|
|
2070
|
+
try {
|
|
2071
|
+
const uuid = "ffffffff-0000-0000-0000-0000000000a5";
|
|
2072
|
+
const { runner } = queueRunner([
|
|
2073
|
+
{ code: 0, stdout: "cloudflared 2024.1.0\n", stderr: "" },
|
|
2074
|
+
{ code: 0, stdout: "[]", stderr: "" },
|
|
2075
|
+
{
|
|
2076
|
+
code: 0,
|
|
2077
|
+
stdout: `Tunnel credentials written to ${env.cloudflaredHome}/${uuid}.json.\nCreated tunnel parachute with id ${uuid}\n`,
|
|
2078
|
+
stderr: "",
|
|
2079
|
+
},
|
|
2080
|
+
{ code: 0, stdout: "", stderr: "" },
|
|
2081
|
+
]);
|
|
2082
|
+
const { spawner } = fakeSpawner(42401);
|
|
2083
|
+
const sup = makeCfSupervisorStub({
|
|
2084
|
+
driveThrows: () => new ModuleOpHttpError(404, "not_supervised", "vault is not supervised"),
|
|
2085
|
+
});
|
|
2086
|
+
|
|
2087
|
+
const code = await exposeCloudflareUp("gitcoin-parachute.unforced.dev", {
|
|
2088
|
+
runner,
|
|
2089
|
+
spawner,
|
|
2090
|
+
alive: () => false,
|
|
2091
|
+
kill: () => {},
|
|
2092
|
+
log: () => {},
|
|
2093
|
+
manifestPath: env.manifestPath,
|
|
2094
|
+
statePath: env.statePath,
|
|
2095
|
+
exposeStatePath: env.exposeStatePath,
|
|
2096
|
+
configPath: env.configPath,
|
|
2097
|
+
logPath: env.logPath,
|
|
2098
|
+
cloudflaredHome: env.cloudflaredHome,
|
|
2099
|
+
configDir: env.configDir,
|
|
2100
|
+
installService: () => ({ outcome: "fallback", messages: [] }),
|
|
2101
|
+
supervisor: sup.opts,
|
|
2102
|
+
});
|
|
2103
|
+
|
|
2104
|
+
expect(code).toBe(0);
|
|
2105
|
+
expect(sup.driveCalls).toEqual([{ short: "vault", op: "restart" }]);
|
|
2106
|
+
} finally {
|
|
2107
|
+
env.cleanup();
|
|
2108
|
+
}
|
|
2109
|
+
});
|
|
2110
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import type { CloudflaredState } from "../cloudflare/state.ts";
|
|
3
|
+
import type { ExposeCloudflareOpts } from "../commands/expose-cloudflare.ts";
|
|
3
4
|
import {
|
|
4
5
|
type ExposePublicOffAutoOpts,
|
|
5
6
|
runExposePublicOffAutoDetect,
|
|
@@ -50,6 +51,7 @@ interface Harness {
|
|
|
50
51
|
prompts: string[];
|
|
51
52
|
tailscaleCalls: number;
|
|
52
53
|
cloudflareCalls: number;
|
|
54
|
+
cloudflareOpts: ExposeCloudflareOpts[];
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
function makeHarness(
|
|
@@ -70,6 +72,7 @@ function makeHarness(
|
|
|
70
72
|
prompts: [],
|
|
71
73
|
tailscaleCalls: 0,
|
|
72
74
|
cloudflareCalls: 0,
|
|
75
|
+
cloudflareOpts: [],
|
|
73
76
|
};
|
|
74
77
|
const answers = [...(input.promptAnswers ?? [])];
|
|
75
78
|
let i = 0;
|
|
@@ -88,8 +91,9 @@ function makeHarness(
|
|
|
88
91
|
harness.tailscaleCalls++;
|
|
89
92
|
return input.tsExitCode ?? 0;
|
|
90
93
|
},
|
|
91
|
-
exposeCloudflareOffImpl: async () => {
|
|
94
|
+
exposeCloudflareOffImpl: async (cfOpts) => {
|
|
92
95
|
harness.cloudflareCalls++;
|
|
96
|
+
harness.cloudflareOpts.push(cfOpts);
|
|
93
97
|
return input.cfExitCode ?? 0;
|
|
94
98
|
},
|
|
95
99
|
};
|
|
@@ -273,3 +277,24 @@ describe("runExposePublicOffAutoDetect — both live (non-TTY)", () => {
|
|
|
273
277
|
expect(harness.logs).toContain("(non-TTY: tearing down both.)");
|
|
274
278
|
});
|
|
275
279
|
});
|
|
280
|
+
|
|
281
|
+
describe("runExposePublicOffAutoDetect — supervisor threading (Phase 4 consistency)", () => {
|
|
282
|
+
test("cloudflareOffOpts.supervisor reaches the cloudflare teardown leg", async () => {
|
|
283
|
+
// cli.ts threads `cloudflareOffOpts: { supervisor: {} }` into the
|
|
284
|
+
// auto-detect off path so the Phase 4 supervisor resolution is consistent
|
|
285
|
+
// across both providers (matching the explicit `--cloudflare off` branch).
|
|
286
|
+
// Assert the supervisor block survives the spread-with-tunnelName wrapper
|
|
287
|
+
// and arrives at the leaf cloudflare-off impl.
|
|
288
|
+
const { harness, opts } = makeHarness({ cfState: cloudflaredState() });
|
|
289
|
+
const code = await runExposePublicOffAutoDetect({
|
|
290
|
+
...opts,
|
|
291
|
+
cloudflareOffOpts: { supervisor: {} },
|
|
292
|
+
});
|
|
293
|
+
expect(code).toBe(0);
|
|
294
|
+
expect(harness.cloudflareCalls).toBe(1);
|
|
295
|
+
expect(harness.cloudflareOpts).toHaveLength(1);
|
|
296
|
+
expect(harness.cloudflareOpts[0]?.supervisor).toEqual({});
|
|
297
|
+
// The per-record wrapper still stamps the tunnelName onto the leaf opts.
|
|
298
|
+
expect(harness.cloudflareOpts[0]?.tunnelName).toBe("vault-tunnel");
|
|
299
|
+
});
|
|
300
|
+
});
|