@openparachute/hub 0.3.0-rc.1 → 0.5.0
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 +19 -17
- package/package.json +15 -4
- package/src/__tests__/admin-auth.test.ts +197 -0
- package/src/__tests__/admin-config.test.ts +281 -0
- package/src/__tests__/admin-grants.test.ts +271 -0
- package/src/__tests__/admin-handlers.test.ts +530 -0
- package/src/__tests__/admin-host-admin-token.test.ts +115 -0
- package/src/__tests__/admin-vault-admin-token.test.ts +190 -0
- package/src/__tests__/admin-vaults.test.ts +615 -0
- package/src/__tests__/auth-codes.test.ts +253 -0
- package/src/__tests__/auth.test.ts +712 -17
- package/src/__tests__/cli.test.ts +50 -0
- package/src/__tests__/clients.test.ts +264 -0
- package/src/__tests__/cloudflare-state.test.ts +167 -7
- package/src/__tests__/csrf.test.ts +117 -0
- package/src/__tests__/expose-cloudflare.test.ts +232 -37
- package/src/__tests__/expose-off-auto.test.ts +15 -9
- package/src/__tests__/expose-public-auto.test.ts +153 -0
- package/src/__tests__/expose.test.ts +216 -24
- package/src/__tests__/grants.test.ts +164 -0
- package/src/__tests__/hub-db.test.ts +153 -0
- package/src/__tests__/hub-server.test.ts +984 -26
- package/src/__tests__/hub.test.ts +56 -49
- package/src/__tests__/install.test.ts +327 -3
- package/src/__tests__/jwks.test.ts +37 -0
- package/src/__tests__/jwt-sign.test.ts +361 -0
- package/src/__tests__/lifecycle.test.ts +519 -5
- package/src/__tests__/module-manifest.test.ts +183 -0
- package/src/__tests__/oauth-handlers.test.ts +3112 -0
- package/src/__tests__/oauth-ui.test.ts +253 -0
- package/src/__tests__/operator-token.test.ts +140 -0
- package/src/__tests__/providers-detect.test.ts +158 -0
- package/src/__tests__/scope-explanations.test.ts +108 -0
- package/src/__tests__/scope-registry.test.ts +220 -0
- package/src/__tests__/services-manifest.test.ts +137 -1
- package/src/__tests__/sessions.test.ts +116 -0
- package/src/__tests__/setup.test.ts +361 -0
- package/src/__tests__/signing-keys.test.ts +153 -0
- package/src/__tests__/upgrade.test.ts +541 -0
- package/src/__tests__/users.test.ts +154 -0
- package/src/__tests__/well-known.test.ts +127 -10
- package/src/admin-auth.ts +126 -0
- package/src/admin-config-ui.ts +534 -0
- package/src/admin-config.ts +226 -0
- package/src/admin-grants.ts +160 -0
- package/src/admin-handlers.ts +365 -0
- package/src/admin-host-admin-token.ts +83 -0
- package/src/admin-vault-admin-token.ts +98 -0
- package/src/admin-vaults.ts +359 -0
- package/src/auth-codes.ts +189 -0
- package/src/cli.ts +202 -25
- package/src/clients.ts +210 -0
- package/src/cloudflare/config.ts +25 -6
- package/src/cloudflare/state.ts +108 -28
- package/src/commands/auth.ts +652 -19
- package/src/commands/expose-cloudflare.ts +85 -45
- package/src/commands/expose-interactive.ts +20 -44
- package/src/commands/expose-off-auto.ts +27 -11
- package/src/commands/expose-public-auto.ts +179 -0
- package/src/commands/expose.ts +63 -32
- package/src/commands/install.ts +337 -48
- package/src/commands/lifecycle.ts +242 -37
- package/src/commands/setup.ts +366 -0
- package/src/commands/status.ts +4 -1
- package/src/commands/upgrade.ts +429 -0
- package/src/csrf.ts +101 -0
- package/src/grants.ts +142 -0
- package/src/help.ts +133 -19
- package/src/hub-control.ts +12 -0
- package/src/hub-db.ts +164 -0
- package/src/hub-server.ts +643 -22
- package/src/hub.ts +97 -390
- package/src/jwks.ts +41 -0
- package/src/jwt-sign.ts +275 -0
- package/src/module-manifest.ts +435 -0
- package/src/oauth-handlers.ts +1206 -0
- package/src/oauth-ui.ts +582 -0
- package/src/operator-token.ts +129 -0
- package/src/providers/detect.ts +97 -0
- package/src/scope-explanations.ts +137 -0
- package/src/scope-registry.ts +158 -0
- package/src/service-spec.ts +270 -97
- package/src/services-manifest.ts +57 -1
- package/src/sessions.ts +115 -0
- package/src/signing-keys.ts +120 -0
- package/src/users.ts +144 -0
- package/src/well-known.ts +62 -26
- package/web/ui/dist/assets/index-BKzPDdB0.js +60 -0
- package/web/ui/dist/assets/index-Dyk6g7vT.css +1 -0
- package/web/ui/dist/index.html +14 -0
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
defaultAlive,
|
|
7
|
+
defaultKill,
|
|
8
|
+
defaultSpawner,
|
|
9
|
+
logs,
|
|
10
|
+
restart,
|
|
11
|
+
start,
|
|
12
|
+
stop,
|
|
13
|
+
} from "../commands/lifecycle.ts";
|
|
6
14
|
import { writeHubPort } from "../hub-control.ts";
|
|
7
15
|
import { ensureLogPath, logPath, readPid, writePid } from "../process-state.ts";
|
|
8
16
|
import { upsertService } from "../services-manifest.ts";
|
|
@@ -48,12 +56,62 @@ function seedNotes(manifestPath: string): void {
|
|
|
48
56
|
);
|
|
49
57
|
}
|
|
50
58
|
|
|
59
|
+
interface ThirdPartySeed {
|
|
60
|
+
installDir: string;
|
|
61
|
+
manifestName?: string;
|
|
62
|
+
startCmd?: readonly string[];
|
|
63
|
+
port?: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Seed a third-party services.json row + write a `.parachute/module.json` at
|
|
68
|
+
* `installDir`. Mirrors what `parachute install /tmp/foo` produces in
|
|
69
|
+
* production: row carries `installDir`, lifecycle resolves spec from the
|
|
70
|
+
* filesystem.
|
|
71
|
+
*/
|
|
72
|
+
function seedThirdParty(
|
|
73
|
+
manifestPath: string,
|
|
74
|
+
configDirRoot: string,
|
|
75
|
+
name: string,
|
|
76
|
+
opts: ThirdPartySeed,
|
|
77
|
+
): string {
|
|
78
|
+
const installDir = opts.installDir;
|
|
79
|
+
mkdirSync(join(installDir, ".parachute"), { recursive: true });
|
|
80
|
+
const manifest = {
|
|
81
|
+
name,
|
|
82
|
+
manifestName: opts.manifestName ?? name,
|
|
83
|
+
kind: "api" as const,
|
|
84
|
+
port: opts.port ?? 1944,
|
|
85
|
+
paths: [`/${name}`],
|
|
86
|
+
health: `/${name}/health`,
|
|
87
|
+
...(opts.startCmd ? { startCmd: opts.startCmd } : {}),
|
|
88
|
+
};
|
|
89
|
+
writeFileSync(join(installDir, ".parachute", "module.json"), JSON.stringify(manifest));
|
|
90
|
+
upsertService(
|
|
91
|
+
{
|
|
92
|
+
name: opts.manifestName ?? name,
|
|
93
|
+
port: opts.port ?? 1944,
|
|
94
|
+
paths: [`/${name}`],
|
|
95
|
+
health: `/${name}/health`,
|
|
96
|
+
version: "0.0.1",
|
|
97
|
+
installDir,
|
|
98
|
+
},
|
|
99
|
+
manifestPath,
|
|
100
|
+
);
|
|
101
|
+
return configDirRoot;
|
|
102
|
+
}
|
|
103
|
+
|
|
51
104
|
interface SpawnerStub {
|
|
52
|
-
spawn: (
|
|
105
|
+
spawn: (
|
|
106
|
+
cmd: readonly string[],
|
|
107
|
+
logFile: string,
|
|
108
|
+
opts?: { env?: Record<string, string>; cwd?: string },
|
|
109
|
+
) => number;
|
|
53
110
|
calls: Array<{
|
|
54
111
|
cmd: readonly string[];
|
|
55
112
|
logFile: string;
|
|
56
113
|
env?: Record<string, string>;
|
|
114
|
+
cwd?: string;
|
|
57
115
|
}>;
|
|
58
116
|
}
|
|
59
117
|
|
|
@@ -62,12 +120,13 @@ function makeSpawner(pidSequence: number[]): SpawnerStub {
|
|
|
62
120
|
cmd: readonly string[];
|
|
63
121
|
logFile: string;
|
|
64
122
|
env?: Record<string, string>;
|
|
123
|
+
cwd?: string;
|
|
65
124
|
}> = [];
|
|
66
125
|
let i = 0;
|
|
67
126
|
return {
|
|
68
127
|
calls,
|
|
69
|
-
spawn(cmd, logFile,
|
|
70
|
-
calls.push({ cmd: [...cmd], logFile, env });
|
|
128
|
+
spawn(cmd, logFile, opts) {
|
|
129
|
+
calls.push({ cmd: [...cmd], logFile, env: opts?.env, cwd: opts?.cwd });
|
|
71
130
|
return pidSequence[i++] ?? 99999;
|
|
72
131
|
},
|
|
73
132
|
};
|
|
@@ -428,6 +487,141 @@ describe("parachute start", () => {
|
|
|
428
487
|
h.cleanup();
|
|
429
488
|
}
|
|
430
489
|
});
|
|
490
|
+
|
|
491
|
+
test("third-party module starts via installDir module.json with cwd", async () => {
|
|
492
|
+
// hub#83: services.json rows that carry installDir resolve their spec
|
|
493
|
+
// from `<installDir>/.parachute/module.json` at lifecycle time. Spawn
|
|
494
|
+
// gets cwd=installDir so manifest-declared relative paths work.
|
|
495
|
+
const h = makeHarness();
|
|
496
|
+
try {
|
|
497
|
+
const installDir = join(h.configDir, "_pkg-agent");
|
|
498
|
+
seedThirdParty(h.manifestPath, h.configDir, "agent", {
|
|
499
|
+
installDir,
|
|
500
|
+
startCmd: ["bun", "web/server/src/server.ts"],
|
|
501
|
+
port: 1944,
|
|
502
|
+
});
|
|
503
|
+
const spawner = makeSpawner([8080]);
|
|
504
|
+
const code = await start("agent", {
|
|
505
|
+
configDir: h.configDir,
|
|
506
|
+
manifestPath: h.manifestPath,
|
|
507
|
+
spawner,
|
|
508
|
+
log: () => {},
|
|
509
|
+
});
|
|
510
|
+
expect(code).toBe(0);
|
|
511
|
+
expect(spawner.calls).toHaveLength(1);
|
|
512
|
+
expect(spawner.calls[0]?.cmd).toEqual(["bun", "web/server/src/server.ts"]);
|
|
513
|
+
expect(spawner.calls[0]?.cwd).toBe(installDir);
|
|
514
|
+
expect(readPid("agent", h.configDir)).toBe(8080);
|
|
515
|
+
} finally {
|
|
516
|
+
h.cleanup();
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
test("third-party with no installDir errors as unknown service", async () => {
|
|
521
|
+
// A row whose name isn't a known short name AND has no installDir is
|
|
522
|
+
// unmanageable — we have no way to find a spec for it.
|
|
523
|
+
const h = makeHarness();
|
|
524
|
+
try {
|
|
525
|
+
upsertService(
|
|
526
|
+
{
|
|
527
|
+
name: "mystery",
|
|
528
|
+
port: 1944,
|
|
529
|
+
paths: ["/mystery"],
|
|
530
|
+
health: "/mystery/health",
|
|
531
|
+
version: "0.0.1",
|
|
532
|
+
},
|
|
533
|
+
h.manifestPath,
|
|
534
|
+
);
|
|
535
|
+
const lines: string[] = [];
|
|
536
|
+
const code = await start("mystery", {
|
|
537
|
+
configDir: h.configDir,
|
|
538
|
+
manifestPath: h.manifestPath,
|
|
539
|
+
log: (l) => lines.push(l),
|
|
540
|
+
});
|
|
541
|
+
expect(code).toBe(1);
|
|
542
|
+
expect(lines.join("\n")).toMatch(/unknown service "mystery"/);
|
|
543
|
+
} finally {
|
|
544
|
+
h.cleanup();
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
test("start (no svc) sweeps both first-party and third-party rows", async () => {
|
|
549
|
+
const h = makeHarness();
|
|
550
|
+
try {
|
|
551
|
+
seedVault(h.manifestPath);
|
|
552
|
+
const installDir = join(h.configDir, "_pkg-agent");
|
|
553
|
+
seedThirdParty(h.manifestPath, h.configDir, "agent", {
|
|
554
|
+
installDir,
|
|
555
|
+
startCmd: ["bun", "server.ts"],
|
|
556
|
+
port: 1944,
|
|
557
|
+
});
|
|
558
|
+
const spawner = makeSpawner([4242, 8080]);
|
|
559
|
+
const code = await start(undefined, {
|
|
560
|
+
configDir: h.configDir,
|
|
561
|
+
manifestPath: h.manifestPath,
|
|
562
|
+
spawner,
|
|
563
|
+
log: () => {},
|
|
564
|
+
});
|
|
565
|
+
expect(code).toBe(0);
|
|
566
|
+
expect(spawner.calls).toHaveLength(2);
|
|
567
|
+
const cmds = spawner.calls.map((c) => c.cmd);
|
|
568
|
+
expect(cmds).toContainEqual(["parachute-vault", "serve"]);
|
|
569
|
+
expect(cmds).toContainEqual(["bun", "server.ts"]);
|
|
570
|
+
} finally {
|
|
571
|
+
h.cleanup();
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
test("third-party with malformed module.json fails clearly", async () => {
|
|
576
|
+
const h = makeHarness();
|
|
577
|
+
try {
|
|
578
|
+
const installDir = join(h.configDir, "_pkg-broken");
|
|
579
|
+
mkdirSync(join(installDir, ".parachute"), { recursive: true });
|
|
580
|
+
writeFileSync(join(installDir, ".parachute", "module.json"), "{ not valid json");
|
|
581
|
+
upsertService(
|
|
582
|
+
{
|
|
583
|
+
name: "broken",
|
|
584
|
+
port: 1944,
|
|
585
|
+
paths: ["/broken"],
|
|
586
|
+
health: "/broken/health",
|
|
587
|
+
version: "0.0.1",
|
|
588
|
+
installDir,
|
|
589
|
+
},
|
|
590
|
+
h.manifestPath,
|
|
591
|
+
);
|
|
592
|
+
const lines: string[] = [];
|
|
593
|
+
const code = await start("broken", {
|
|
594
|
+
configDir: h.configDir,
|
|
595
|
+
manifestPath: h.manifestPath,
|
|
596
|
+
log: (l) => lines.push(l),
|
|
597
|
+
});
|
|
598
|
+
expect(code).toBe(1);
|
|
599
|
+
expect(lines.join("\n")).toMatch(/broken: invalid module\.json/);
|
|
600
|
+
} finally {
|
|
601
|
+
h.cleanup();
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
test("third-party with no startCmd in module.json reports lifecycle-unsupported", async () => {
|
|
606
|
+
const h = makeHarness();
|
|
607
|
+
try {
|
|
608
|
+
const installDir = join(h.configDir, "_pkg-noop");
|
|
609
|
+
seedThirdParty(h.manifestPath, h.configDir, "noop", {
|
|
610
|
+
installDir,
|
|
611
|
+
port: 1945,
|
|
612
|
+
});
|
|
613
|
+
const lines: string[] = [];
|
|
614
|
+
const code = await start("noop", {
|
|
615
|
+
configDir: h.configDir,
|
|
616
|
+
manifestPath: h.manifestPath,
|
|
617
|
+
log: (l) => lines.push(l),
|
|
618
|
+
});
|
|
619
|
+
expect(code).toBe(1);
|
|
620
|
+
expect(lines.join("\n")).toMatch(/lifecycle not yet supported/);
|
|
621
|
+
} finally {
|
|
622
|
+
h.cleanup();
|
|
623
|
+
}
|
|
624
|
+
});
|
|
431
625
|
});
|
|
432
626
|
|
|
433
627
|
describe("parachute stop", () => {
|
|
@@ -605,4 +799,324 @@ describe("parachute logs", () => {
|
|
|
605
799
|
h.cleanup();
|
|
606
800
|
}
|
|
607
801
|
});
|
|
802
|
+
|
|
803
|
+
test("third-party module name with installDir is recognised", async () => {
|
|
804
|
+
const h = makeHarness();
|
|
805
|
+
try {
|
|
806
|
+
const installDir = join(h.configDir, "_pkg-agent");
|
|
807
|
+
seedThirdParty(h.manifestPath, h.configDir, "agent", {
|
|
808
|
+
installDir,
|
|
809
|
+
startCmd: ["bun", "server.ts"],
|
|
810
|
+
});
|
|
811
|
+
const p = ensureLogPath("agent", h.configDir);
|
|
812
|
+
writeFileSync(p, "agent line 1\nagent line 2\n");
|
|
813
|
+
const lines: string[] = [];
|
|
814
|
+
const code = await logs("agent", {
|
|
815
|
+
configDir: h.configDir,
|
|
816
|
+
manifestPath: h.manifestPath,
|
|
817
|
+
log: (l) => lines.push(l),
|
|
818
|
+
});
|
|
819
|
+
expect(code).toBe(0);
|
|
820
|
+
expect(lines).toEqual(["agent line 1", "agent line 2"]);
|
|
821
|
+
} finally {
|
|
822
|
+
h.cleanup();
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
describe("process-group lifecycle (hub#88)", () => {
|
|
828
|
+
// Spawn a wrapper that forks a long-running grandchild (sleep), wait for
|
|
829
|
+
// both to come up, then check that the wrapper PID equals its PGID — the
|
|
830
|
+
// post-fix invariant that makes group-kill safe. Without `detached: true`
|
|
831
|
+
// the child inherits the test runner's PGID and group-kill would target
|
|
832
|
+
// the wrong tree.
|
|
833
|
+
test("defaultSpawner puts child in its own process group", async () => {
|
|
834
|
+
const h = makeHarness();
|
|
835
|
+
try {
|
|
836
|
+
const logFile = ensureLogPath("test", h.configDir);
|
|
837
|
+
const pid = defaultSpawner.spawn(["sh", "-c", "sleep 2 & wait"], logFile);
|
|
838
|
+
try {
|
|
839
|
+
// Resolve the child's PGID via ps; the kernel reports it as a
|
|
840
|
+
// numeric column. PGID == PID means our setsid-equivalent worked.
|
|
841
|
+
const ps = Bun.spawnSync(["ps", "-o", "pgid=", "-p", String(pid)]);
|
|
842
|
+
const pgid = Number.parseInt(ps.stdout.toString().trim(), 10);
|
|
843
|
+
expect(pgid).toBe(pid);
|
|
844
|
+
} finally {
|
|
845
|
+
try {
|
|
846
|
+
process.kill(-pid, "SIGKILL");
|
|
847
|
+
} catch {}
|
|
848
|
+
}
|
|
849
|
+
} finally {
|
|
850
|
+
h.cleanup();
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
// The smoking-gun scenario from #88: a wrapper (sh) forks a grandchild
|
|
855
|
+
// (sleep) that keeps a resource — here, just stays alive. SIGKILL on the
|
|
856
|
+
// wrapper PID alone leaves the grandchild running. With detached spawn +
|
|
857
|
+
// group-kill, both go down. We assert by checking the grandchild's PID
|
|
858
|
+
// is no longer kill-able after `defaultKill`.
|
|
859
|
+
test("defaultKill takes down the wrapper and its grandchildren together", async () => {
|
|
860
|
+
const h = makeHarness();
|
|
861
|
+
try {
|
|
862
|
+
const logFile = ensureLogPath("test", h.configDir);
|
|
863
|
+
// Wrapper sh forks `sleep 30 & echo $!` so we capture the grandchild
|
|
864
|
+
// PID via the log file, then `wait` so the wrapper sticks around as
|
|
865
|
+
// a parent (mirrors `pnpm exec tsx`'s shape).
|
|
866
|
+
const wrapperPid = defaultSpawner.spawn(
|
|
867
|
+
["sh", "-c", "sleep 30 & echo $! >&2; wait"],
|
|
868
|
+
logFile,
|
|
869
|
+
);
|
|
870
|
+
// Give the grandchild time to start and the log line to flush.
|
|
871
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
872
|
+
const log = await Bun.file(logFile).text();
|
|
873
|
+
const grandchildPid = Number.parseInt(log.trim().split("\n").pop() ?? "", 10);
|
|
874
|
+
expect(grandchildPid).toBeGreaterThan(0);
|
|
875
|
+
expect(grandchildPid).not.toBe(wrapperPid);
|
|
876
|
+
// Both should be alive before kill.
|
|
877
|
+
expect(() => process.kill(grandchildPid, 0)).not.toThrow();
|
|
878
|
+
|
|
879
|
+
defaultKill(wrapperPid, "SIGKILL");
|
|
880
|
+
|
|
881
|
+
// Reap + wait for the grandchild to exit; on macOS the kernel may
|
|
882
|
+
// take a tick to deliver the signal.
|
|
883
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
884
|
+
let grandchildStillAlive = true;
|
|
885
|
+
try {
|
|
886
|
+
process.kill(grandchildPid, 0);
|
|
887
|
+
} catch {
|
|
888
|
+
grandchildStillAlive = false;
|
|
889
|
+
}
|
|
890
|
+
expect(grandchildStillAlive).toBe(false);
|
|
891
|
+
} finally {
|
|
892
|
+
h.cleanup();
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
// defaultAlive's post-fix semantics: returns true while any group member
|
|
897
|
+
// is alive (the wrapper stays in the group as long as it's running),
|
|
898
|
+
// false after the group drains.
|
|
899
|
+
test("defaultAlive reports group liveness for detached children", async () => {
|
|
900
|
+
const h = makeHarness();
|
|
901
|
+
try {
|
|
902
|
+
const logFile = ensureLogPath("test", h.configDir);
|
|
903
|
+
const pid = defaultSpawner.spawn(["sh", "-c", "sleep 2"], logFile);
|
|
904
|
+
try {
|
|
905
|
+
expect(defaultAlive(pid)).toBe(true);
|
|
906
|
+
} finally {
|
|
907
|
+
try {
|
|
908
|
+
process.kill(-pid, "SIGKILL");
|
|
909
|
+
} catch {}
|
|
910
|
+
}
|
|
911
|
+
// Wait for the kill to drain the group, then re-check.
|
|
912
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
913
|
+
expect(defaultAlive(pid)).toBe(false);
|
|
914
|
+
} finally {
|
|
915
|
+
h.cleanup();
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
// Legacy pidfile compatibility: a pre-detached pidfile holds a positive
|
|
920
|
+
// PID whose pgid is the parent shell, not the pid itself. defaultAlive
|
|
921
|
+
// must fall back to a bare-pid check so the next `stop` actually runs;
|
|
922
|
+
// defaultKill must fall back to a bare-pid signal so it can be reaped.
|
|
923
|
+
test("defaultAlive + defaultKill fall back to bare-pid for legacy (non-detached) processes", async () => {
|
|
924
|
+
// Spawn a non-detached child to simulate a legacy pidfile (pre-fix
|
|
925
|
+
// start). It shares the test runner's pgid, so kill(-pid, 0) will
|
|
926
|
+
// ESRCH and we should fall back.
|
|
927
|
+
const proc = Bun.spawn(["sh", "-c", "sleep 5"], { stdio: ["ignore", "ignore", "ignore"] });
|
|
928
|
+
const pid = proc.pid;
|
|
929
|
+
try {
|
|
930
|
+
expect(defaultAlive(pid)).toBe(true);
|
|
931
|
+
defaultKill(pid, "SIGKILL");
|
|
932
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
933
|
+
expect(defaultAlive(pid)).toBe(false);
|
|
934
|
+
} finally {
|
|
935
|
+
try {
|
|
936
|
+
process.kill(pid, "SIGKILL");
|
|
937
|
+
} catch {}
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* `parachute start|stop|restart hub` — the bug Aaron filed as hub#166. Hub
|
|
944
|
+
* isn't a row in services.json, so the generic services-manifest path
|
|
945
|
+
* surfaced "unknown service: hub". The fix dispatches `svc === "hub"`
|
|
946
|
+
* straight to hub-control.ts. These tests inject `ensureRunning`/`stop`
|
|
947
|
+
* stubs so we don't actually fork bun.
|
|
948
|
+
*/
|
|
949
|
+
describe("parachute start|stop|restart hub", () => {
|
|
950
|
+
test("start hub: dispatches to ensureHubRunning, propagates configDir + issuer", async () => {
|
|
951
|
+
const h = makeHarness();
|
|
952
|
+
try {
|
|
953
|
+
const log: string[] = [];
|
|
954
|
+
const ensureCalls: Array<{ configDir?: string; issuer?: string }> = [];
|
|
955
|
+
const code = await start("hub", {
|
|
956
|
+
configDir: h.configDir,
|
|
957
|
+
manifestPath: h.manifestPath,
|
|
958
|
+
hubOrigin: "https://hub.example.com",
|
|
959
|
+
hub: {
|
|
960
|
+
ensureRunning: async (opts) => {
|
|
961
|
+
ensureCalls.push({ configDir: opts.configDir, issuer: opts.issuer });
|
|
962
|
+
return { pid: 4711, port: 1939, started: true };
|
|
963
|
+
},
|
|
964
|
+
},
|
|
965
|
+
log: (l) => log.push(l),
|
|
966
|
+
});
|
|
967
|
+
expect(code).toBe(0);
|
|
968
|
+
expect(ensureCalls).toHaveLength(1);
|
|
969
|
+
expect(ensureCalls[0]).toEqual({
|
|
970
|
+
configDir: h.configDir,
|
|
971
|
+
issuer: "https://hub.example.com",
|
|
972
|
+
});
|
|
973
|
+
} finally {
|
|
974
|
+
h.cleanup();
|
|
975
|
+
}
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
test("start hub: reports already-running cleanly when ensureHubRunning returns started=false", async () => {
|
|
979
|
+
const h = makeHarness();
|
|
980
|
+
try {
|
|
981
|
+
const log: string[] = [];
|
|
982
|
+
const code = await start("hub", {
|
|
983
|
+
configDir: h.configDir,
|
|
984
|
+
manifestPath: h.manifestPath,
|
|
985
|
+
hub: {
|
|
986
|
+
ensureRunning: async () => ({ pid: 8888, port: 1939, started: false }),
|
|
987
|
+
},
|
|
988
|
+
log: (l) => log.push(l),
|
|
989
|
+
});
|
|
990
|
+
expect(code).toBe(0);
|
|
991
|
+
expect(log.join("\n")).toMatch(/hub already running \(pid 8888\) on port 1939/);
|
|
992
|
+
} finally {
|
|
993
|
+
h.cleanup();
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
test("start hub: surfaces ensureHubRunning errors as exit 1", async () => {
|
|
998
|
+
const h = makeHarness();
|
|
999
|
+
try {
|
|
1000
|
+
const log: string[] = [];
|
|
1001
|
+
const code = await start("hub", {
|
|
1002
|
+
configDir: h.configDir,
|
|
1003
|
+
manifestPath: h.manifestPath,
|
|
1004
|
+
hub: {
|
|
1005
|
+
ensureRunning: async () => {
|
|
1006
|
+
throw new Error("hub: port 1939 unavailable");
|
|
1007
|
+
},
|
|
1008
|
+
},
|
|
1009
|
+
log: (l) => log.push(l),
|
|
1010
|
+
});
|
|
1011
|
+
expect(code).toBe(1);
|
|
1012
|
+
expect(log.join("\n")).toMatch(/hub failed to start.*port 1939 unavailable/);
|
|
1013
|
+
} finally {
|
|
1014
|
+
h.cleanup();
|
|
1015
|
+
}
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
test("stop hub: dispatches to stopHub, true → '✓ hub stopped'", async () => {
|
|
1019
|
+
const h = makeHarness();
|
|
1020
|
+
try {
|
|
1021
|
+
const log: string[] = [];
|
|
1022
|
+
const stopCalls: Array<{ configDir?: string }> = [];
|
|
1023
|
+
const code = await stop("hub", {
|
|
1024
|
+
configDir: h.configDir,
|
|
1025
|
+
manifestPath: h.manifestPath,
|
|
1026
|
+
hub: {
|
|
1027
|
+
stop: async (opts) => {
|
|
1028
|
+
stopCalls.push({ configDir: opts.configDir });
|
|
1029
|
+
return true;
|
|
1030
|
+
},
|
|
1031
|
+
},
|
|
1032
|
+
log: (l) => log.push(l),
|
|
1033
|
+
});
|
|
1034
|
+
expect(code).toBe(0);
|
|
1035
|
+
expect(stopCalls).toHaveLength(1);
|
|
1036
|
+
expect(stopCalls[0]?.configDir).toBe(h.configDir);
|
|
1037
|
+
expect(log.join("\n")).toMatch(/✓ hub stopped/);
|
|
1038
|
+
} finally {
|
|
1039
|
+
h.cleanup();
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
test("stop hub: false → 'wasn't running' (still exit 0)", async () => {
|
|
1044
|
+
const h = makeHarness();
|
|
1045
|
+
try {
|
|
1046
|
+
const log: string[] = [];
|
|
1047
|
+
const code = await stop("hub", {
|
|
1048
|
+
configDir: h.configDir,
|
|
1049
|
+
manifestPath: h.manifestPath,
|
|
1050
|
+
hub: { stop: async () => false },
|
|
1051
|
+
log: (l) => log.push(l),
|
|
1052
|
+
});
|
|
1053
|
+
expect(code).toBe(0);
|
|
1054
|
+
expect(log.join("\n")).toMatch(/hub wasn't running/);
|
|
1055
|
+
} finally {
|
|
1056
|
+
h.cleanup();
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
test("restart hub: chains stop then start through the same hub seam", async () => {
|
|
1061
|
+
const h = makeHarness();
|
|
1062
|
+
try {
|
|
1063
|
+
const log: string[] = [];
|
|
1064
|
+
const order: string[] = [];
|
|
1065
|
+
const code = await restart("hub", {
|
|
1066
|
+
configDir: h.configDir,
|
|
1067
|
+
manifestPath: h.manifestPath,
|
|
1068
|
+
hub: {
|
|
1069
|
+
stop: async () => {
|
|
1070
|
+
order.push("stop");
|
|
1071
|
+
return true;
|
|
1072
|
+
},
|
|
1073
|
+
ensureRunning: async () => {
|
|
1074
|
+
order.push("start");
|
|
1075
|
+
return { pid: 5151, port: 1939, started: true };
|
|
1076
|
+
},
|
|
1077
|
+
},
|
|
1078
|
+
log: (l) => log.push(l),
|
|
1079
|
+
});
|
|
1080
|
+
expect(code).toBe(0);
|
|
1081
|
+
expect(order).toEqual(["stop", "start"]);
|
|
1082
|
+
} finally {
|
|
1083
|
+
h.cleanup();
|
|
1084
|
+
}
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
test("logs hub: doesn't reject 'hub' as an unknown service", async () => {
|
|
1088
|
+
const h = makeHarness();
|
|
1089
|
+
try {
|
|
1090
|
+
// No log file yet — exercise the "no logs yet" branch, which still
|
|
1091
|
+
// returns 0. Goal of this test is just the unknown-service guard.
|
|
1092
|
+
const log: string[] = [];
|
|
1093
|
+
const code = await logs("hub", {
|
|
1094
|
+
configDir: h.configDir,
|
|
1095
|
+
manifestPath: h.manifestPath,
|
|
1096
|
+
log: (l) => log.push(l),
|
|
1097
|
+
});
|
|
1098
|
+
expect(code).toBe(0);
|
|
1099
|
+
expect(log.join("\n")).toMatch(/no logs yet for hub/);
|
|
1100
|
+
} finally {
|
|
1101
|
+
h.cleanup();
|
|
1102
|
+
}
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
test("logs hub: prints the tail when a log file exists", async () => {
|
|
1106
|
+
const h = makeHarness();
|
|
1107
|
+
try {
|
|
1108
|
+
const path = ensureLogPath("hub", h.configDir);
|
|
1109
|
+
writeFileSync(path, "hub line one\nhub line two\n");
|
|
1110
|
+
const log: string[] = [];
|
|
1111
|
+
const code = await logs("hub", {
|
|
1112
|
+
configDir: h.configDir,
|
|
1113
|
+
manifestPath: h.manifestPath,
|
|
1114
|
+
log: (l) => log.push(l),
|
|
1115
|
+
});
|
|
1116
|
+
expect(code).toBe(0);
|
|
1117
|
+
expect(log).toEqual(["hub line one", "hub line two"]);
|
|
1118
|
+
} finally {
|
|
1119
|
+
h.cleanup();
|
|
1120
|
+
}
|
|
1121
|
+
});
|
|
608
1122
|
});
|