@openparachute/hub 0.6.5-rc.8 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/__tests__/account-setup.test.ts +310 -6
- package/src/__tests__/account-vault-admin-token.test.ts +35 -3
- package/src/__tests__/admin-channel-token.test.ts +173 -0
- package/src/__tests__/admin-connections-credentials.test.ts +1320 -0
- package/src/__tests__/admin-connections.test.ts +1154 -0
- package/src/__tests__/admin-csrf-belt.test.ts +346 -0
- package/src/__tests__/admin-module-token.test.ts +311 -0
- package/src/__tests__/admin-vaults.test.ts +590 -0
- package/src/__tests__/api-invites.test.ts +166 -6
- package/src/__tests__/api-modules-ops.test.ts +70 -5
- package/src/__tests__/api-modules.test.ts +262 -79
- package/src/__tests__/audience-gate.test.ts +752 -0
- package/src/__tests__/hub-db.test.ts +36 -0
- package/src/__tests__/hub-server.test.ts +585 -21
- package/src/__tests__/invites.test.ts +91 -1
- package/src/__tests__/lifecycle.test.ts +238 -3
- package/src/__tests__/module-manifest.test.ts +305 -8
- package/src/__tests__/serve-boot.test.ts +133 -2
- package/src/__tests__/service-spec-discovery.test.ts +109 -0
- package/src/__tests__/setup-gate.test.ts +13 -7
- package/src/__tests__/setup-wizard.test.ts +228 -1
- package/src/__tests__/vault-name.test.ts +20 -5
- package/src/__tests__/well-known.test.ts +44 -8
- package/src/__tests__/ws-bridge.test.ts +573 -0
- package/src/__tests__/ws-connection-caps.test.ts +456 -0
- package/src/account-setup.ts +94 -23
- package/src/account-vault-admin-token.ts +43 -14
- package/src/admin-channel-token.ts +135 -0
- package/src/admin-connections.ts +1882 -0
- package/src/admin-login-ui.ts +64 -15
- package/src/admin-module-token.ts +197 -0
- package/src/admin-vaults.ts +399 -12
- package/src/api-hub-upgrade.ts +4 -3
- package/src/api-invites.ts +92 -12
- package/src/api-modules-ops.ts +41 -16
- package/src/api-modules.ts +238 -116
- package/src/api-tokens.ts +8 -5
- package/src/audience-gate.ts +268 -0
- package/src/chrome-strip.ts +8 -1
- package/src/commands/lifecycle.ts +187 -47
- package/src/commands/serve-boot.ts +80 -3
- package/src/commands/setup.ts +4 -4
- package/src/connections-store.ts +191 -0
- package/src/grants.ts +50 -0
- package/src/help.ts +13 -6
- package/src/host-admin-token-validation.ts +6 -2
- package/src/hub-db.ts +26 -1
- package/src/hub-server.ts +849 -70
- package/src/invites.ts +91 -2
- package/src/jwt-sign.ts +47 -1
- package/src/module-manifest.ts +536 -23
- package/src/origin-check.ts +109 -0
- package/src/proxy-error-ui.ts +1 -1
- package/src/service-spec.ts +132 -41
- package/src/services-manifest.ts +97 -0
- package/src/setup-wizard.ts +68 -6
- package/src/users.ts +11 -0
- package/src/vault-name.ts +27 -7
- package/src/well-known.ts +41 -33
- package/src/ws-bridge.ts +256 -0
- package/src/ws-connection-caps.ts +170 -0
- package/web/ui/dist/assets/index-Cxtod68O.js +61 -0
- package/web/ui/dist/assets/index-E_9wqjEm.css +1 -0
- package/web/ui/dist/index.html +2 -2
- package/src/__tests__/api-modules-config.test.ts +0 -882
- package/src/api-modules-config.ts +0 -421
- package/web/ui/dist/assets/index-BYYUeLGA.css +0 -1
- package/web/ui/dist/assets/index-D3cDUOOj.js +0 -61
|
@@ -27,6 +27,8 @@ import {
|
|
|
27
27
|
issueInvite,
|
|
28
28
|
listInvites,
|
|
29
29
|
revokeInvite,
|
|
30
|
+
revokeInvitesForVault,
|
|
31
|
+
usernameReservedByPendingInvite,
|
|
30
32
|
} from "../invites.ts";
|
|
31
33
|
import { createUser } from "../users.ts";
|
|
32
34
|
|
|
@@ -67,7 +69,7 @@ describe("issueInvite", () => {
|
|
|
67
69
|
}
|
|
68
70
|
});
|
|
69
71
|
|
|
70
|
-
test("defaults: role=write, provision_vault=1, 7-day expiry", async () => {
|
|
72
|
+
test("defaults: role=write, provision_vault=1, 7-day expiry, no pre-named username", async () => {
|
|
71
73
|
const { db, adminId, cleanup } = await makeDb();
|
|
72
74
|
try {
|
|
73
75
|
const now = new Date("2026-06-04T00:00:00Z");
|
|
@@ -75,12 +77,74 @@ describe("issueInvite", () => {
|
|
|
75
77
|
expect(invite.role).toBe("write");
|
|
76
78
|
expect(invite.provisionVault).toBe(true);
|
|
77
79
|
expect(invite.vaultName).toBeNull();
|
|
80
|
+
expect(invite.username).toBeNull();
|
|
78
81
|
const expiry = new Date(invite.expiresAt).getTime() - now.getTime();
|
|
79
82
|
expect(Math.round(expiry / 1000)).toBe(DEFAULT_INVITE_TTL_SECONDS);
|
|
80
83
|
} finally {
|
|
81
84
|
cleanup();
|
|
82
85
|
}
|
|
83
86
|
});
|
|
87
|
+
|
|
88
|
+
test("pre-named username round-trips through the row", async () => {
|
|
89
|
+
const { db, adminId, cleanup } = await makeDb();
|
|
90
|
+
try {
|
|
91
|
+
const { rawToken, invite } = issueInvite(db, {
|
|
92
|
+
createdBy: adminId,
|
|
93
|
+
username: "jonathan",
|
|
94
|
+
vaultName: "shared",
|
|
95
|
+
provisionVault: false,
|
|
96
|
+
role: "read",
|
|
97
|
+
});
|
|
98
|
+
expect(invite.username).toBe("jonathan");
|
|
99
|
+
const found = findInviteByRawToken(db, rawToken);
|
|
100
|
+
expect(found?.username).toBe("jonathan");
|
|
101
|
+
expect(found?.vaultName).toBe("shared");
|
|
102
|
+
expect(found?.provisionVault).toBe(false);
|
|
103
|
+
expect(found?.role).toBe("read");
|
|
104
|
+
} finally {
|
|
105
|
+
cleanup();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("usernameReservedByPendingInvite", () => {
|
|
111
|
+
test("pending reserves; redeemed / revoked / expired / other-name do not", async () => {
|
|
112
|
+
const { db, adminId, cleanup } = await makeDb();
|
|
113
|
+
try {
|
|
114
|
+
const now = new Date("2026-06-10T00:00:00Z");
|
|
115
|
+
// Pending → reserved.
|
|
116
|
+
issueInvite(db, { createdBy: adminId, username: "pending-name", now: () => now });
|
|
117
|
+
expect(usernameReservedByPendingInvite(db, "pending-name", now)).toBe(true);
|
|
118
|
+
expect(usernameReservedByPendingInvite(db, "other-name", now)).toBe(false);
|
|
119
|
+
// Redeemed → free.
|
|
120
|
+
const redeemed = issueInvite(db, {
|
|
121
|
+
createdBy: adminId,
|
|
122
|
+
username: "used-name",
|
|
123
|
+
now: () => now,
|
|
124
|
+
});
|
|
125
|
+
consumeInvite(db, redeemed.invite.tokenHash, adminId, now);
|
|
126
|
+
expect(usernameReservedByPendingInvite(db, "used-name", now)).toBe(false);
|
|
127
|
+
// Revoked → free.
|
|
128
|
+
const revoked = issueInvite(db, {
|
|
129
|
+
createdBy: adminId,
|
|
130
|
+
username: "gone-name",
|
|
131
|
+
now: () => now,
|
|
132
|
+
});
|
|
133
|
+
revokeInvite(db, revoked.invite.tokenHash, now);
|
|
134
|
+
expect(usernameReservedByPendingInvite(db, "gone-name", now)).toBe(false);
|
|
135
|
+
// Expired → free.
|
|
136
|
+
issueInvite(db, {
|
|
137
|
+
createdBy: adminId,
|
|
138
|
+
username: "old-name",
|
|
139
|
+
expiresInSeconds: 60,
|
|
140
|
+
now: () => now,
|
|
141
|
+
});
|
|
142
|
+
const later = new Date(now.getTime() + 120_000);
|
|
143
|
+
expect(usernameReservedByPendingInvite(db, "old-name", later)).toBe(false);
|
|
144
|
+
} finally {
|
|
145
|
+
cleanup();
|
|
146
|
+
}
|
|
147
|
+
});
|
|
84
148
|
});
|
|
85
149
|
|
|
86
150
|
describe("findInviteByRawToken", () => {
|
|
@@ -183,6 +247,32 @@ describe("revokeInvite", () => {
|
|
|
183
247
|
});
|
|
184
248
|
});
|
|
185
249
|
|
|
250
|
+
describe("revokeInvitesForVault (B1 cascade step)", () => {
|
|
251
|
+
test("a NULL-vault_name invite (redeemer-named flow) is NOT revoked by any vault's cascade", async () => {
|
|
252
|
+
// The cascade invalidates invites PINNED to the deleted vault — an
|
|
253
|
+
// unpinned invite (vault_name NULL: the redeemer names their own vault)
|
|
254
|
+
// can't resurrect a specific name, so it must survive every vault's
|
|
255
|
+
// delete. SQL `vault_name = ?` never matches NULL; pin that boundary.
|
|
256
|
+
const { db, adminId, cleanup } = await makeDb();
|
|
257
|
+
try {
|
|
258
|
+
const unpinned = issueInvite(db, { createdBy: adminId }); // vault_name NULL
|
|
259
|
+
const pinned = issueInvite(db, { createdBy: adminId, vaultName: "work" });
|
|
260
|
+
|
|
261
|
+
expect(revokeInvitesForVault(db, "work")).toBe(1);
|
|
262
|
+
// The pinned invite is revoked; the unpinned one rides on untouched.
|
|
263
|
+
expect(findInviteByRawToken(db, pinned.rawToken)?.revokedAt).not.toBeNull();
|
|
264
|
+
expect(findInviteByRawToken(db, unpinned.rawToken)?.revokedAt).toBeNull();
|
|
265
|
+
|
|
266
|
+
// A second sweep (or another vault's sweep) finds nothing more.
|
|
267
|
+
expect(revokeInvitesForVault(db, "work")).toBe(0);
|
|
268
|
+
expect(revokeInvitesForVault(db, "other")).toBe(0);
|
|
269
|
+
expect(findInviteByRawToken(db, unpinned.rawToken)?.revokedAt).toBeNull();
|
|
270
|
+
} finally {
|
|
271
|
+
cleanup();
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
186
276
|
describe("inviteStatus / listInvites", () => {
|
|
187
277
|
test("derives pending / redeemed / expired / revoked", async () => {
|
|
188
278
|
const { db, adminId, cleanup } = await makeDb();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdtempSync, openSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { mkdtempSync, openSync, rmSync, utimesSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import {
|
|
@@ -678,10 +678,37 @@ describe("group-aware kill / liveness (hub#88)", () => {
|
|
|
678
678
|
});
|
|
679
679
|
|
|
680
680
|
// ---------------------------------------------------------------------------
|
|
681
|
-
// `parachute logs <svc
|
|
682
|
-
//
|
|
681
|
+
// `parachute logs <svc>`. Under hub-as-supervisor (Phase 5b) a module's output
|
|
682
|
+
// is multiplexed into the HUB log with a `[<svc>] ` line prefix, so `logs <svc>`
|
|
683
|
+
// reads that stream filtered to the service (hub#652). The per-service logfile
|
|
684
|
+
// keyed by short name (the readers §7.5 keeps) survives as the legacy source
|
|
685
|
+
// when it's fresher than the hub log (pre-supervised installs). `logs hub`
|
|
686
|
+
// reads the hub log unfiltered.
|
|
683
687
|
// ---------------------------------------------------------------------------
|
|
684
688
|
|
|
689
|
+
/** The supervisor's multiplexed hub-log shape (supervisor.ts pipeOutput). */
|
|
690
|
+
const INTERLEAVED_HUB_LOG =
|
|
691
|
+
"[vault] vault boot\n" +
|
|
692
|
+
"[scribe] scribe boot\n" +
|
|
693
|
+
"[vault] GET /vault/default/api/notes 200 7ms\n" +
|
|
694
|
+
"[surface] [app-dcr] client registered\n" +
|
|
695
|
+
"[vaultx] not vault's line\n" +
|
|
696
|
+
"[vault] sync ok\n";
|
|
697
|
+
|
|
698
|
+
const VAULT_LINES_STRIPPED = ["vault boot", "GET /vault/default/api/notes 200 7ms", "sync ok"];
|
|
699
|
+
|
|
700
|
+
function writeHubLog(configDir: string, content: string): string {
|
|
701
|
+
const path = ensureLogPath("hub", configDir);
|
|
702
|
+
writeFileSync(path, content);
|
|
703
|
+
return path;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/** Backdate a file's mtime so freshness comparisons are deterministic. */
|
|
707
|
+
function backdate(path: string, secondsAgo: number): void {
|
|
708
|
+
const t = new Date(Date.now() - secondsAgo * 1000);
|
|
709
|
+
utimesSync(path, t, t);
|
|
710
|
+
}
|
|
711
|
+
|
|
685
712
|
describe("parachute logs", () => {
|
|
686
713
|
test("hint when no log file exists", async () => {
|
|
687
714
|
const h = makeHarness();
|
|
@@ -829,4 +856,212 @@ describe("parachute logs", () => {
|
|
|
829
856
|
h.cleanup();
|
|
830
857
|
}
|
|
831
858
|
});
|
|
859
|
+
|
|
860
|
+
// ---- hub#652: supervised modules read the hub log's [svc]-prefixed stream ----
|
|
861
|
+
|
|
862
|
+
test("supervised module: reads the hub log filtered to its prefix, stripped (hub#652)", async () => {
|
|
863
|
+
const h = makeHarness();
|
|
864
|
+
try {
|
|
865
|
+
seedVault(h.manifestPath);
|
|
866
|
+
writeHubLog(h.configDir, INTERLEAVED_HUB_LOG);
|
|
867
|
+
// No per-service vault.log — the supervised steady state.
|
|
868
|
+
const log: string[] = [];
|
|
869
|
+
const code = await logs("vault", {
|
|
870
|
+
configDir: h.configDir,
|
|
871
|
+
manifestPath: h.manifestPath,
|
|
872
|
+
log: (l) => log.push(l),
|
|
873
|
+
});
|
|
874
|
+
expect(code).toBe(0);
|
|
875
|
+
// Exact-prefix match: `[vaultx]` noise excluded; `[vault] ` stripped.
|
|
876
|
+
expect(log).toEqual(VAULT_LINES_STRIPPED);
|
|
877
|
+
} finally {
|
|
878
|
+
h.cleanup();
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
test("stale per-service file + fresher hub log: the hub stream wins (the live hub#652 shape)", async () => {
|
|
883
|
+
const h = makeHarness();
|
|
884
|
+
try {
|
|
885
|
+
seedVault(h.manifestPath);
|
|
886
|
+
const legacy = ensureLogPath("vault", h.configDir);
|
|
887
|
+
writeFileSync(legacy, "stale pre-cutover line\n");
|
|
888
|
+
backdate(legacy, 3600);
|
|
889
|
+
writeHubLog(h.configDir, INTERLEAVED_HUB_LOG);
|
|
890
|
+
const log: string[] = [];
|
|
891
|
+
const code = await logs("vault", {
|
|
892
|
+
configDir: h.configDir,
|
|
893
|
+
manifestPath: h.manifestPath,
|
|
894
|
+
log: (l) => log.push(l),
|
|
895
|
+
});
|
|
896
|
+
expect(code).toBe(0);
|
|
897
|
+
expect(log).toEqual(VAULT_LINES_STRIPPED);
|
|
898
|
+
expect(log.join("\n")).not.toContain("stale pre-cutover line");
|
|
899
|
+
} finally {
|
|
900
|
+
h.cleanup();
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
test("per-service file fresher than the hub log: legacy file wins (pre-supervised install)", async () => {
|
|
905
|
+
const h = makeHarness();
|
|
906
|
+
try {
|
|
907
|
+
seedVault(h.manifestPath);
|
|
908
|
+
const hubLog = writeHubLog(h.configDir, "[vault] old supervised line\n");
|
|
909
|
+
backdate(hubLog, 3600);
|
|
910
|
+
const legacy = ensureLogPath("vault", h.configDir);
|
|
911
|
+
writeFileSync(legacy, "live detached line\n");
|
|
912
|
+
const log: string[] = [];
|
|
913
|
+
const code = await logs("vault", {
|
|
914
|
+
configDir: h.configDir,
|
|
915
|
+
manifestPath: h.manifestPath,
|
|
916
|
+
log: (l) => log.push(l),
|
|
917
|
+
});
|
|
918
|
+
expect(code).toBe(0);
|
|
919
|
+
expect(log).toEqual(["live detached line"]);
|
|
920
|
+
} finally {
|
|
921
|
+
h.cleanup();
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
test("lines cap applies to the FILTERED set, not raw hub-log lines", async () => {
|
|
926
|
+
const h = makeHarness();
|
|
927
|
+
try {
|
|
928
|
+
seedVault(h.manifestPath);
|
|
929
|
+
writeHubLog(h.configDir, INTERLEAVED_HUB_LOG);
|
|
930
|
+
const log: string[] = [];
|
|
931
|
+
const code = await logs("vault", {
|
|
932
|
+
configDir: h.configDir,
|
|
933
|
+
manifestPath: h.manifestPath,
|
|
934
|
+
lines: 2,
|
|
935
|
+
log: (l) => log.push(l),
|
|
936
|
+
});
|
|
937
|
+
expect(code).toBe(0);
|
|
938
|
+
expect(log).toEqual(VAULT_LINES_STRIPPED.slice(-2));
|
|
939
|
+
} finally {
|
|
940
|
+
h.cleanup();
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
test("hub log has no lines for the service + no per-service file: start hint", async () => {
|
|
945
|
+
const h = makeHarness();
|
|
946
|
+
try {
|
|
947
|
+
seedVault(h.manifestPath);
|
|
948
|
+
writeHubLog(h.configDir, "[scribe] scribe boot\n");
|
|
949
|
+
const log: string[] = [];
|
|
950
|
+
const code = await logs("vault", {
|
|
951
|
+
configDir: h.configDir,
|
|
952
|
+
manifestPath: h.manifestPath,
|
|
953
|
+
log: (l) => log.push(l),
|
|
954
|
+
});
|
|
955
|
+
expect(code).toBe(0);
|
|
956
|
+
expect(log.join("\n")).toMatch(/no logs yet for vault/);
|
|
957
|
+
} finally {
|
|
958
|
+
h.cleanup();
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
test("hub log has no lines for the service + per-service file exists: legacy shown with a note", async () => {
|
|
963
|
+
const h = makeHarness();
|
|
964
|
+
try {
|
|
965
|
+
seedVault(h.manifestPath);
|
|
966
|
+
const legacy = ensureLogPath("vault", h.configDir);
|
|
967
|
+
writeFileSync(legacy, "old detached line\n");
|
|
968
|
+
backdate(legacy, 3600);
|
|
969
|
+
writeHubLog(h.configDir, "[scribe] scribe boot\n");
|
|
970
|
+
const log: string[] = [];
|
|
971
|
+
const code = await logs("vault", {
|
|
972
|
+
configDir: h.configDir,
|
|
973
|
+
manifestPath: h.manifestPath,
|
|
974
|
+
log: (l) => log.push(l),
|
|
975
|
+
});
|
|
976
|
+
expect(code).toBe(0);
|
|
977
|
+
// The note distinguishes the stale per-service file from the live
|
|
978
|
+
// stream — the exact "stale logs presented as current" trap in hub#652.
|
|
979
|
+
expect(log[0]).toMatch(/no vault lines in the hub log/);
|
|
980
|
+
expect(log).toContain("old detached line");
|
|
981
|
+
} finally {
|
|
982
|
+
h.cleanup();
|
|
983
|
+
}
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
test("follow mode filters the hub stream and strips the prefix (hub#652)", async () => {
|
|
987
|
+
const h = makeHarness();
|
|
988
|
+
try {
|
|
989
|
+
seedVault(h.manifestPath);
|
|
990
|
+
writeHubLog(h.configDir, INTERLEAVED_HUB_LOG);
|
|
991
|
+
const encoder = new TextEncoder();
|
|
992
|
+
let streamedPath: string | undefined;
|
|
993
|
+
const followStream = (path: string): ReadableStream<Uint8Array> => {
|
|
994
|
+
streamedPath = path;
|
|
995
|
+
return new ReadableStream<Uint8Array>({
|
|
996
|
+
start(controller) {
|
|
997
|
+
controller.enqueue(encoder.encode("[vault] live one\n[scribe] noise\n"));
|
|
998
|
+
// Split a line across chunks to exercise the line buffer.
|
|
999
|
+
controller.enqueue(encoder.encode("[vault] live "));
|
|
1000
|
+
controller.enqueue(encoder.encode("two\n"));
|
|
1001
|
+
controller.close();
|
|
1002
|
+
},
|
|
1003
|
+
});
|
|
1004
|
+
};
|
|
1005
|
+
const log: string[] = [];
|
|
1006
|
+
const code = await logs("vault", {
|
|
1007
|
+
configDir: h.configDir,
|
|
1008
|
+
manifestPath: h.manifestPath,
|
|
1009
|
+
follow: true,
|
|
1010
|
+
followStream,
|
|
1011
|
+
log: (l) => log.push(l),
|
|
1012
|
+
});
|
|
1013
|
+
expect(code).toBe(0);
|
|
1014
|
+
expect(streamedPath).toContain("hub.log");
|
|
1015
|
+
expect(log).toEqual([...VAULT_LINES_STRIPPED, "live one", "live two"]);
|
|
1016
|
+
} finally {
|
|
1017
|
+
h.cleanup();
|
|
1018
|
+
}
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
test("follow mode with a fresher per-service file tails THAT file via tail -f", async () => {
|
|
1022
|
+
const h = makeHarness();
|
|
1023
|
+
try {
|
|
1024
|
+
seedVault(h.manifestPath);
|
|
1025
|
+
const hubLog = writeHubLog(h.configDir, "[vault] old supervised line\n");
|
|
1026
|
+
backdate(hubLog, 3600);
|
|
1027
|
+
const legacy = ensureLogPath("vault", h.configDir);
|
|
1028
|
+
writeFileSync(legacy, "live detached line\n");
|
|
1029
|
+
const spawned: string[][] = [];
|
|
1030
|
+
const code = await logs("vault", {
|
|
1031
|
+
configDir: h.configDir,
|
|
1032
|
+
manifestPath: h.manifestPath,
|
|
1033
|
+
follow: true,
|
|
1034
|
+
tailSpawner: {
|
|
1035
|
+
spawn(cmd) {
|
|
1036
|
+
spawned.push([...cmd]);
|
|
1037
|
+
return 12345;
|
|
1038
|
+
},
|
|
1039
|
+
},
|
|
1040
|
+
log: () => {},
|
|
1041
|
+
});
|
|
1042
|
+
expect(code).toBe(0);
|
|
1043
|
+
expect(spawned).toHaveLength(1);
|
|
1044
|
+
expect(spawned[0]?.[0]).toBe("tail");
|
|
1045
|
+
expect(spawned[0]?.at(-1)).toBe(legacy);
|
|
1046
|
+
} finally {
|
|
1047
|
+
h.cleanup();
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
test("logs hub: stays unfiltered — module-prefixed lines included", async () => {
|
|
1052
|
+
const h = makeHarness();
|
|
1053
|
+
try {
|
|
1054
|
+
writeHubLog(h.configDir, INTERLEAVED_HUB_LOG);
|
|
1055
|
+
const log: string[] = [];
|
|
1056
|
+
const code = await logs("hub", {
|
|
1057
|
+
configDir: h.configDir,
|
|
1058
|
+
manifestPath: h.manifestPath,
|
|
1059
|
+
log: (l) => log.push(l),
|
|
1060
|
+
});
|
|
1061
|
+
expect(code).toBe(0);
|
|
1062
|
+
expect(log).toEqual(INTERLEAVED_HUB_LOG.replace(/\n$/, "").split("\n"));
|
|
1063
|
+
} finally {
|
|
1064
|
+
h.cleanup();
|
|
1065
|
+
}
|
|
1066
|
+
});
|
|
832
1067
|
});
|