@serfab/cadre-host 1.10.0 → 1.12.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serfab/cadre-host",
3
- "version": "1.10.0",
3
+ "version": "1.12.0",
4
4
  "type": "module",
5
5
  "description": "Self-hosted cadre node manager for basement-PC deployments",
6
6
  "main": "dist/index.js",
@@ -65,9 +65,9 @@
65
65
  "@libp2p/interface": "^3.3.0",
66
66
  "@libp2p/peer-id": "^6.0.15",
67
67
  "@multiformats/multiaddr": "^13.0.3",
68
- "@serfab/cadre-cli": "^1.10.0",
69
- "@serfab/cadre-core": "^1.10.0",
70
- "@serfab/cadre-provider": "^1.10.0",
68
+ "@serfab/cadre-cli": "^1.12.0",
69
+ "@serfab/cadre-core": "^1.12.0",
70
+ "@serfab/cadre-provider": "^1.12.0",
71
71
  "commander": "^14.0.2",
72
72
  "debug": "^4.4.3",
73
73
  "fastify": "^5.7.1",
@@ -1,58 +1,58 @@
1
- /**
2
- * Vitest global setup — runs once before the whole cadre-host suite.
3
- *
4
- * `src/update/manifest.ts` and `src/push/index.ts` import real symbols from
5
- * `@serfab/cadre-core` (`canonicalJson`, `validatePushCredentials`), and
6
- * `trust-circle-integration.test.ts` imports and drives a real `CadreNode` from
7
- * it too. `@serfab/cadre-core`'s compiled entry point in turn imports
8
- * `@serfab/quereus-plugin-sereus`, which imports the `@optimystic/*` and
9
- * `@quereus/quereus` packages linked in from their sibling checkouts.
10
- * `host-process-orchestrator.ts` also resolves `@serfab/cadre-cli`'s bin path at
11
- * runtime, and `@serfab/cadre-provider` is a declared dependency in its own
12
- * right. Every one of those resolves through a `node_modules` symlink whose
13
- * manifest points at `dist`, so an edit to any of their `src` with no following
14
- * build is invisible here: the suite runs the previous build and reports green
15
- * about code it never executed. Fail the run up front instead.
16
- *
17
- * NOTE: accepted tradeoff — this does not cover cadre-host's *own* `src` going
18
- * stale relative to its own `dist`. The CLI smoke tests spawn `dist/bin/host.js`
19
- * as a child process, which is a same-package staleness concern, not the
20
- * cross-package one `BuildTarget` is meant to check (see
21
- * `test-harness/build-freshness.ts`'s module comment). Consequence: a `src`-only
22
- * edit to `bin/host.ts` is checked by `bin/__tests__/cli-reference.test.ts`
23
- * (which reads `src`) but not by the smoke tests (which read `dist`), so run
24
- * `yarn workspace @serfab/cadre-host build:server` before the suite. Revisit if
25
- * a stale `dist` ever produces a false green rather than merely a weaker check.
26
- *
27
- * The guard itself lives at the repo root (`test-harness/build-freshness.ts`),
28
- * shared with the other suites that call it; the list of packages below is this
29
- * suite's own concern.
30
- */
31
-
32
- import { assertBuildFresh, type BuildTarget } from '../../../../test-harness/build-freshness.js';
33
-
34
- /**
35
- * Every package this suite runs compiled code from — the `workspace:` entries of
36
- * `package.json`'s `dependencies`, plus the `link:` packages reached
37
- * transitively through them.
38
- *
39
- * Exported so `build-targets.test.ts` can hold it against this package's actual
40
- * `dependencies` — a hand-written list rots silently otherwise.
41
- */
42
- export const TARGETS: BuildTarget[] = [
43
- { packageName: '@serfab/cadre-cli', distEntry: 'dist/bin/cadre.js', location: 'workspace' },
44
- { packageName: '@serfab/cadre-core', distEntry: 'dist/index.js', location: 'workspace' },
45
- { packageName: '@serfab/cadre-provider', distEntry: 'dist/index.js', location: 'workspace' },
46
- // Reached through `@serfab/cadre-cli`, whose spawned `dist` loads it.
47
- { packageName: '@serfab/config-check', distEntry: 'dist/index.js', location: 'workspace' },
48
- { packageName: '@serfab/quereus-plugin-sereus', distEntry: 'dist/index.js', location: 'workspace' },
49
- { packageName: '@optimystic/db-core', distEntry: 'dist/src/index.js', location: 'linked' },
50
- { packageName: '@optimystic/db-p2p', distEntry: 'dist/src/index.js', location: 'linked' },
51
- { packageName: '@optimystic/quereus-plugin-crypto', distEntry: 'dist/index.js', location: 'linked' },
52
- { packageName: '@optimystic/quereus-plugin-optimystic', distEntry: 'dist/index.js', location: 'linked' },
53
- { packageName: '@quereus/quereus', distEntry: 'dist/src/index.js', location: 'linked' },
54
- ];
55
-
56
- export default function setup(): void {
57
- assertBuildFresh(TARGETS, import.meta.url);
58
- }
1
+ /**
2
+ * Vitest global setup — runs once before the whole cadre-host suite.
3
+ *
4
+ * `src/update/manifest.ts` and `src/push/index.ts` import real symbols from
5
+ * `@serfab/cadre-core` (`canonicalJson`, `validatePushCredentials`), and
6
+ * `trust-circle-integration.test.ts` imports and drives a real `CadreNode` from
7
+ * it too. `@serfab/cadre-core`'s compiled entry point in turn imports
8
+ * `@serfab/quereus-plugin-sereus`, which imports the `@optimystic/*` and
9
+ * `@quereus/quereus` packages linked in from their sibling checkouts.
10
+ * `host-process-orchestrator.ts` also resolves `@serfab/cadre-cli`'s bin path at
11
+ * runtime, and `@serfab/cadre-provider` is a declared dependency in its own
12
+ * right. Every one of those resolves through a `node_modules` symlink whose
13
+ * manifest points at `dist`, so an edit to any of their `src` with no following
14
+ * build is invisible here: the suite runs the previous build and reports green
15
+ * about code it never executed. Fail the run up front instead.
16
+ *
17
+ * NOTE: accepted tradeoff — this does not cover cadre-host's *own* `src` going
18
+ * stale relative to its own `dist`. The CLI smoke tests spawn `dist/bin/host.js`
19
+ * as a child process, which is a same-package staleness concern, not the
20
+ * cross-package one `BuildTarget` is meant to check (see
21
+ * `test-harness/build-freshness.ts`'s module comment). Consequence: a `src`-only
22
+ * edit to `bin/host.ts` is checked by `bin/__tests__/cli-reference.test.ts`
23
+ * (which reads `src`) but not by the smoke tests (which read `dist`), so run
24
+ * `yarn workspace @serfab/cadre-host build:server` before the suite. Revisit if
25
+ * a stale `dist` ever produces a false green rather than merely a weaker check.
26
+ *
27
+ * The guard itself lives at the repo root (`test-harness/build-freshness.ts`),
28
+ * shared with the other suites that call it; the list of packages below is this
29
+ * suite's own concern.
30
+ */
31
+
32
+ import { assertBuildFresh, type BuildTarget } from '../../../../test-harness/build-freshness.js';
33
+
34
+ /**
35
+ * Every package this suite runs compiled code from — the `workspace:` entries of
36
+ * `package.json`'s `dependencies`, plus the `link:` packages reached
37
+ * transitively through them.
38
+ *
39
+ * Exported so `build-targets.test.ts` can hold it against this package's actual
40
+ * `dependencies` — a hand-written list rots silently otherwise.
41
+ */
42
+ export const TARGETS: BuildTarget[] = [
43
+ { packageName: '@serfab/cadre-cli', distEntry: 'dist/bin/cadre.js', location: 'workspace' },
44
+ { packageName: '@serfab/cadre-core', distEntry: 'dist/index.js', location: 'workspace' },
45
+ { packageName: '@serfab/cadre-provider', distEntry: 'dist/index.js', location: 'workspace' },
46
+ // Reached through `@serfab/cadre-cli`, whose spawned `dist` loads it.
47
+ { packageName: '@serfab/config-check', distEntry: 'dist/index.js', location: 'workspace' },
48
+ { packageName: '@serfab/quereus-plugin-sereus', distEntry: 'dist/index.js', location: 'workspace' },
49
+ { packageName: '@optimystic/db-core', distEntry: 'dist/src/index.js', location: 'linked' },
50
+ { packageName: '@optimystic/db-p2p', distEntry: 'dist/src/index.js', location: 'linked' },
51
+ { packageName: '@optimystic/quereus-plugin-crypto', distEntry: 'dist/index.js', location: 'linked' },
52
+ { packageName: '@optimystic/quereus-plugin-optimystic', distEntry: 'dist/index.js', location: 'linked' },
53
+ { packageName: '@quereus/quereus', distEntry: 'dist/src/index.js', location: 'linked' },
54
+ ];
55
+
56
+ export default function setup(): void {
57
+ assertBuildFresh(TARGETS, import.meta.url);
58
+ }
@@ -1,134 +1,134 @@
1
- /**
2
- * The handle rules `HostProcessOrchestrator` and its test stand-in
3
- * `FakeOrchestrator` (`donation/__tests__/fake-orchestrator.ts`) must both obey,
4
- * written once and run against each: `orchestrator.test.ts` registers them over
5
- * the real class, `fake-orchestrator.test.ts` over the fake. A rule one of them
6
- * stops obeying fails in that file's run.
7
- *
8
- * Not a `.test.ts` file, so vitest does not collect it on its own.
9
- *
10
- * Only what both classes can satisfy belongs here — no ports, no files on disk,
11
- * none of the fake's recording arrays. Those stay in the two calling files.
12
- */
13
-
14
- import { describe, expect, it } from 'vitest';
15
-
16
- import type { OrchestratorCreateRequest, OrchestratorCreateResult } from '@serfab/cadre-provider';
17
-
18
- import type { HostProcessOrchestrator } from '../orchestrator/host-process-orchestrator.js';
19
-
20
- /** What the contract needs from either orchestrator. */
21
- export type ContractOrchestrator = Pick<
22
- HostProcessOrchestrator,
23
- 'createContainer' | 'stopContainer' | 'removeContainer' | 'resolveDockerId'
24
- >;
25
-
26
- export interface HandleContractHarness<O extends ContractOrchestrator> {
27
- /** A fresh orchestrator. Teardown is the calling file's own afterEach. */
28
- make(): O;
29
- request(containerId: string): OrchestratorCreateRequest;
30
- /** Resolve once the spawn named by `dockerId` reads as running. */
31
- started(orch: O, dockerId: string): Promise<void>;
32
- /**
33
- * Make `createContainer` for `containerId` throw from here on. Only called
34
- * after that container has been spawned once and stopped, and no case creates
35
- * again afterwards.
36
- */
37
- failNextCreate(orch: O, containerId: string): void;
38
- }
39
-
40
- const CONTAINER = 'grn_contract_a';
41
- const OTHER_CONTAINER = 'grn_contract_b';
42
-
43
- export function describeHandleContract<O extends ContractOrchestrator>(
44
- label: string,
45
- harness: HandleContractHarness<O>,
46
- ): void {
47
- async function spawn(orch: O, containerId: string): Promise<OrchestratorCreateResult> {
48
- const result = await orch.createContainer(harness.request(containerId));
49
- await harness.started(orch, result.dockerId);
50
- return result;
51
- }
52
-
53
- // Stops first: a container whose previous child is still running is refused.
54
- async function respawn(orch: O, containerId: string, previousDockerId: string): Promise<OrchestratorCreateResult> {
55
- await orch.stopContainer(previousDockerId);
56
- return spawn(orch, containerId);
57
- }
58
-
59
- describe(`${label} handle contract`, () => {
60
- it('rejects a dockerId it never issued', async () => {
61
- const orch = harness.make();
62
-
63
- await expectNotFound(orch.stopContainer('never-issued'), 'never-issued');
64
- await expectNotFound(orch.removeContainer('never-issued'), 'never-issued');
65
- });
66
-
67
- it('rejects a dockerId once its container has been removed', async () => {
68
- const orch = harness.make();
69
- const { dockerId } = await spawn(orch, CONTAINER);
70
- await orch.stopContainer(dockerId);
71
-
72
- await orch.removeContainer(dockerId);
73
-
74
- await expectNotFound(orch.stopContainer(dockerId), dockerId);
75
- await expectNotFound(orch.removeContainer(dockerId), dockerId);
76
- });
77
-
78
- it('replaces the first spawn\'s dockerId when the container re-spawns', async () => {
79
- const orch = harness.make();
80
- const first = await spawn(orch, CONTAINER);
81
-
82
- const second = await respawn(orch, CONTAINER, first.dockerId);
83
-
84
- await expectNotFound(orch.stopContainer(first.dockerId), first.dockerId);
85
- expect(orch.resolveDockerId(CONTAINER)).toBe(second.dockerId);
86
- await expect(orch.stopContainer(second.dockerId)).resolves.toBeUndefined();
87
- });
88
-
89
- // Launching over a live child hands its ports to a second child that then
90
- // dies on them, while the caller records the new dockerId and seed token.
91
- it('refuses to re-spawn a container whose previous child is still running', async () => {
92
- const orch = harness.make();
93
- const first = await spawn(orch, CONTAINER);
94
-
95
- await expect(orch.createContainer(harness.request(CONTAINER)))
96
- .rejects.toHaveProperty('message', `container ${CONTAINER} is still running`);
97
-
98
- expect(orch.resolveDockerId(CONTAINER)).toBe(first.dockerId);
99
- await expect(orch.stopContainer(first.dockerId)).resolves.toBeUndefined();
100
- });
101
-
102
- // The drop on re-spawn filters on containerId; an orchestrator that cleared
103
- // every handle would still pass the same-container case above.
104
- it('leaves another container\'s dockerId alone when one container re-spawns', async () => {
105
- const orch = harness.make();
106
- const other = await spawn(orch, OTHER_CONTAINER);
107
- const first = await spawn(orch, CONTAINER);
108
-
109
- await respawn(orch, CONTAINER, first.dockerId);
110
-
111
- expect(orch.resolveDockerId(OTHER_CONTAINER)).toBe(other.dockerId);
112
- await expect(orch.stopContainer(other.dockerId)).resolves.toBeUndefined();
113
- });
114
-
115
- // What lets a caller still clean up the node it knows about after a re-spawn
116
- // that did not happen.
117
- it('keeps the first spawn\'s dockerId when the re-spawn fails', async () => {
118
- const orch = harness.make();
119
- const first = await spawn(orch, CONTAINER);
120
- await orch.stopContainer(first.dockerId);
121
-
122
- harness.failNextCreate(orch, CONTAINER);
123
- await expect(orch.createContainer(harness.request(CONTAINER))).rejects.toThrow();
124
-
125
- expect(orch.resolveDockerId(CONTAINER)).toBe(first.dockerId);
126
- await expect(orch.removeContainer(first.dockerId)).resolves.toBeUndefined();
127
- });
128
- });
129
- }
130
-
131
- /** The whole message, not a substring, so the two classes cannot come to word it differently. */
132
- async function expectNotFound(call: Promise<void>, dockerId: string): Promise<void> {
133
- await expect(call).rejects.toHaveProperty('message', `Container not found: ${dockerId}`);
134
- }
1
+ /**
2
+ * The handle rules `HostProcessOrchestrator` and its test stand-in
3
+ * `FakeOrchestrator` (`donation/__tests__/fake-orchestrator.ts`) must both obey,
4
+ * written once and run against each: `orchestrator.test.ts` registers them over
5
+ * the real class, `fake-orchestrator.test.ts` over the fake. A rule one of them
6
+ * stops obeying fails in that file's run.
7
+ *
8
+ * Not a `.test.ts` file, so vitest does not collect it on its own.
9
+ *
10
+ * Only what both classes can satisfy belongs here — no ports, no files on disk,
11
+ * none of the fake's recording arrays. Those stay in the two calling files.
12
+ */
13
+
14
+ import { describe, expect, it } from 'vitest';
15
+
16
+ import type { OrchestratorCreateRequest, OrchestratorCreateResult } from '@serfab/cadre-provider';
17
+
18
+ import type { HostProcessOrchestrator } from '../orchestrator/host-process-orchestrator.js';
19
+
20
+ /** What the contract needs from either orchestrator. */
21
+ export type ContractOrchestrator = Pick<
22
+ HostProcessOrchestrator,
23
+ 'createContainer' | 'stopContainer' | 'removeContainer' | 'resolveDockerId'
24
+ >;
25
+
26
+ export interface HandleContractHarness<O extends ContractOrchestrator> {
27
+ /** A fresh orchestrator. Teardown is the calling file's own afterEach. */
28
+ make(): O;
29
+ request(containerId: string): OrchestratorCreateRequest;
30
+ /** Resolve once the spawn named by `dockerId` reads as running. */
31
+ started(orch: O, dockerId: string): Promise<void>;
32
+ /**
33
+ * Make `createContainer` for `containerId` throw from here on. Only called
34
+ * after that container has been spawned once and stopped, and no case creates
35
+ * again afterwards.
36
+ */
37
+ failNextCreate(orch: O, containerId: string): void;
38
+ }
39
+
40
+ const CONTAINER = 'grn_contract_a';
41
+ const OTHER_CONTAINER = 'grn_contract_b';
42
+
43
+ export function describeHandleContract<O extends ContractOrchestrator>(
44
+ label: string,
45
+ harness: HandleContractHarness<O>,
46
+ ): void {
47
+ async function spawn(orch: O, containerId: string): Promise<OrchestratorCreateResult> {
48
+ const result = await orch.createContainer(harness.request(containerId));
49
+ await harness.started(orch, result.dockerId);
50
+ return result;
51
+ }
52
+
53
+ // Stops first: a container whose previous child is still running is refused.
54
+ async function respawn(orch: O, containerId: string, previousDockerId: string): Promise<OrchestratorCreateResult> {
55
+ await orch.stopContainer(previousDockerId);
56
+ return spawn(orch, containerId);
57
+ }
58
+
59
+ describe(`${label} handle contract`, () => {
60
+ it('rejects a dockerId it never issued', async () => {
61
+ const orch = harness.make();
62
+
63
+ await expectNotFound(orch.stopContainer('never-issued'), 'never-issued');
64
+ await expectNotFound(orch.removeContainer('never-issued'), 'never-issued');
65
+ });
66
+
67
+ it('rejects a dockerId once its container has been removed', async () => {
68
+ const orch = harness.make();
69
+ const { dockerId } = await spawn(orch, CONTAINER);
70
+ await orch.stopContainer(dockerId);
71
+
72
+ await orch.removeContainer(dockerId);
73
+
74
+ await expectNotFound(orch.stopContainer(dockerId), dockerId);
75
+ await expectNotFound(orch.removeContainer(dockerId), dockerId);
76
+ });
77
+
78
+ it('replaces the first spawn\'s dockerId when the container re-spawns', async () => {
79
+ const orch = harness.make();
80
+ const first = await spawn(orch, CONTAINER);
81
+
82
+ const second = await respawn(orch, CONTAINER, first.dockerId);
83
+
84
+ await expectNotFound(orch.stopContainer(first.dockerId), first.dockerId);
85
+ expect(orch.resolveDockerId(CONTAINER)).toBe(second.dockerId);
86
+ await expect(orch.stopContainer(second.dockerId)).resolves.toBeUndefined();
87
+ });
88
+
89
+ // Launching over a live child hands its ports to a second child that then
90
+ // dies on them, while the caller records the new dockerId and seed token.
91
+ it('refuses to re-spawn a container whose previous child is still running', async () => {
92
+ const orch = harness.make();
93
+ const first = await spawn(orch, CONTAINER);
94
+
95
+ await expect(orch.createContainer(harness.request(CONTAINER)))
96
+ .rejects.toHaveProperty('message', `container ${CONTAINER} is still running`);
97
+
98
+ expect(orch.resolveDockerId(CONTAINER)).toBe(first.dockerId);
99
+ await expect(orch.stopContainer(first.dockerId)).resolves.toBeUndefined();
100
+ });
101
+
102
+ // The drop on re-spawn filters on containerId; an orchestrator that cleared
103
+ // every handle would still pass the same-container case above.
104
+ it('leaves another container\'s dockerId alone when one container re-spawns', async () => {
105
+ const orch = harness.make();
106
+ const other = await spawn(orch, OTHER_CONTAINER);
107
+ const first = await spawn(orch, CONTAINER);
108
+
109
+ await respawn(orch, CONTAINER, first.dockerId);
110
+
111
+ expect(orch.resolveDockerId(OTHER_CONTAINER)).toBe(other.dockerId);
112
+ await expect(orch.stopContainer(other.dockerId)).resolves.toBeUndefined();
113
+ });
114
+
115
+ // What lets a caller still clean up the node it knows about after a re-spawn
116
+ // that did not happen.
117
+ it('keeps the first spawn\'s dockerId when the re-spawn fails', async () => {
118
+ const orch = harness.make();
119
+ const first = await spawn(orch, CONTAINER);
120
+ await orch.stopContainer(first.dockerId);
121
+
122
+ harness.failNextCreate(orch, CONTAINER);
123
+ await expect(orch.createContainer(harness.request(CONTAINER))).rejects.toThrow();
124
+
125
+ expect(orch.resolveDockerId(CONTAINER)).toBe(first.dockerId);
126
+ await expect(orch.removeContainer(first.dockerId)).resolves.toBeUndefined();
127
+ });
128
+ });
129
+ }
130
+
131
+ /** The whole message, not a substring, so the two classes cannot come to word it differently. */
132
+ async function expectNotFound(call: Promise<void>, dockerId: string): Promise<void> {
133
+ await expect(call).rejects.toHaveProperty('message', `Container not found: ${dockerId}`);
134
+ }