@celilo/cli 2.3.0 → 3.0.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/CELILO_CORE_MODULES.md +1 -1
- package/package.json +3 -3
- package/src/api/serve.ts +13 -1
- package/src/cli/commands/module-health.test.ts +35 -0
- package/src/cli/commands/module-health.ts +11 -3
- package/src/cli/commands/module-publish.test.ts +22 -0
- package/src/cli/commands/module-publish.ts +14 -12
- package/src/cli/commands/module-update.ts +69 -17
- package/src/cli/commands/module-upgrade-gate.test.ts +154 -0
- package/src/cli/commands/module-upgrade.ts +43 -3
- package/src/cli/commands/publish/helpers.ts +4 -3
- package/src/cli/commands/publish/index.ts +13 -1
- package/src/cli/commands/publish/plan.test.ts +64 -0
- package/src/cli/commands/publish/plan.ts +52 -19
- package/src/cli/commands/publish/types.ts +16 -3
- package/src/cli/commands/subscribers-install-daemon.test.ts +44 -0
- package/src/cli/commands/subscribers-install-daemon.ts +107 -0
- package/src/cli/commands/subscribers-serve.test.ts +22 -0
- package/src/cli/commands/subscribers-serve.ts +22 -4
- package/src/cli/commands/system-audit.ts +14 -5
- package/src/cli/commands/system-update.ts +11 -6
- package/src/cli/completion.ts +10 -1
- package/src/cli/fuel-gauge.ts +12 -4
- package/src/cli/index.ts +12 -0
- package/src/cli/json-output.test.ts +81 -0
- package/src/cli/types.ts +9 -0
- package/src/hooks/broker.test.ts +9 -3
- package/src/hooks/executor.test.ts +5 -2
- package/src/hooks/hook-jail-toolchain-reach.test.ts +8 -3
- package/src/hooks/hook-jail-unreachability.test.ts +10 -2
- package/src/hooks/hook-trespass.test.ts +13 -4
- package/src/hooks/run-named-hook.ts +19 -16
- package/src/hooks/test-fixtures/artifact-writing-hook.ts +0 -1
- package/src/hooks/test-fixtures/capability-calling-hook.ts +20 -13
- package/src/hooks/test-fixtures/jail-probe-hook.ts +9 -1
- package/src/hooks/test-fixtures/jail-toolchain-hook.ts +7 -2
- package/src/hooks/test-fixtures/runaway-hook.ts +0 -1
- package/src/hooks/test-fixtures/sigterm-ignoring-hook.ts +0 -1
- package/src/hooks/test-fixtures/silent-hook.ts +0 -1
- package/src/hooks/test-fixtures/store-writing-hook.ts +20 -11
- package/src/hooks/test-fixtures/success-hook.ts +4 -4
- package/src/manifest/contracts/v1.ts +19 -14
- package/src/manifest/json-schema-roundtrip.test.ts +1 -1
- package/src/manifest/schema.ts +66 -16
- package/src/manifest/validate.test.ts +47 -0
- package/src/policy/capability-shape-baseline.ts +63 -21
- package/src/policy/capability-shape-drift.test.ts +53 -1
- package/src/policy/capability-shape.test.ts +105 -0
- package/src/policy/capability-shape.ts +283 -2
- package/src/registry/client.test.ts +65 -0
- package/src/registry/client.ts +7 -7
- package/src/secrets/storage.test.ts +69 -3
- package/src/secrets/storage.ts +71 -1
- package/src/services/audit/health.test.ts +58 -0
- package/src/services/audit/health.ts +15 -3
- package/src/services/audit/index.test.ts +1 -1
- package/src/services/audit/interface-classification.test.ts +16 -5
- package/src/services/audit/interface-classification.ts +25 -2
- package/src/services/audit/module-versions.ts +5 -1
- package/src/services/audit/public-dns.test.ts +20 -0
- package/src/services/audit/public-dns.ts +7 -2
- package/src/services/audit/recurrence-gate.test.ts +225 -0
- package/src/services/audit/trusted-sources.test.ts +17 -0
- package/src/services/audit/trusted-sources.ts +21 -0
- package/src/services/build-bus/hook-dispatch-executor.test.ts +269 -0
- package/src/services/build-bus/hook-dispatch-mgmt.test.ts +102 -150
- package/src/services/build-bus/hook-dispatch-path.test.ts +95 -0
- package/src/services/build-bus/hook-dispatch.test.ts +86 -116
- package/src/services/build-bus/hook-dispatch.ts +99 -121
- package/src/services/build-bus/hook-dispatcher.ts +106 -28
- package/src/services/build-bus/receiver-daemon.test.ts +189 -0
- package/src/services/build-bus/receiver-daemon.ts +355 -0
- package/src/services/build-bus/self-update.ts +156 -0
- package/src/services/capability-compat.test.ts +90 -0
- package/src/services/capability-compat.ts +128 -0
- package/src/services/deploy-terraform.ts +38 -1
- package/src/services/events-daemon.test.ts +57 -0
- package/src/services/events-daemon.ts +76 -0
- package/src/services/firewall-reach.ts +21 -8
- package/src/services/fleet-checks.test.ts +37 -1
- package/src/services/fleet-checks.ts +31 -1
- package/src/services/health-runner.ts +43 -5
- package/src/services/module-deploy-prune.test.ts +89 -0
- package/src/services/module-deploy.ts +72 -125
- package/src/services/module-types-drift.test.ts +1 -1
- package/src/services/module-validator/capability-versions.test.ts +13 -2
- package/src/services/terraform-safety.test.ts +83 -0
- package/src/services/terraform-safety.ts +53 -0
- package/src/services/update/orchestrator.test.ts +1 -1
- package/tsconfig.json +2 -13
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Build-bus hook dispatcher service.
|
|
3
3
|
*
|
|
4
|
-
* Stateful wrapper around `planHookDispatch` + `
|
|
4
|
+
* Stateful wrapper around `planHookDispatch` + `runUpstreamHook`:
|
|
5
5
|
* loads installed-module manifests from the celilo DB, dispatches
|
|
6
6
|
* matching `on_upstream_publish` hooks when a verified PublishEvent
|
|
7
|
-
* arrives,
|
|
7
|
+
* arrives, and records every run durably in the `build_bus_hook_runs`
|
|
8
|
+
* ledger (celilo#1304) — without which a failed run exists only as
|
|
9
|
+
* daemon console output, which no surface reads. Since
|
|
10
|
+
* module-orchestrator-primitives slice 7 it also performs the FRAMEWORK
|
|
11
|
+
* self-update on the same events (tasks.md 7.2): celilo replacing its
|
|
12
|
+
* own binaries on the management host is class H work that stopped
|
|
13
|
+
* being a module hook (design D6).
|
|
8
14
|
*
|
|
9
15
|
* Started by the `celilo subscribers serve` daemon (in-process,
|
|
10
16
|
* sharing the same lifetime); the receiver-server calls
|
|
@@ -18,41 +24,66 @@
|
|
|
18
24
|
import { readFileSync } from 'node:fs';
|
|
19
25
|
import { join } from 'node:path';
|
|
20
26
|
import type { PublishEvent } from '@celilo/event-bus/build-bus';
|
|
27
|
+
import { eq } from 'drizzle-orm';
|
|
21
28
|
import { parse as parseYaml } from 'yaml';
|
|
22
29
|
import { getDb } from '../../db/client';
|
|
23
30
|
import { buildBusHookRuns, modules } from '../../db/schema';
|
|
31
|
+
import { CONTROL_PLANE_MODULE_ID } from '../deployed-systems';
|
|
24
32
|
import {
|
|
25
|
-
type
|
|
33
|
+
type HookDispatchPlan,
|
|
26
34
|
type ModuleHookContext,
|
|
27
35
|
type UpstreamHookEntry,
|
|
36
|
+
type UpstreamHookResult,
|
|
37
|
+
defaultUpstreamHookDeps,
|
|
28
38
|
planHookDispatch,
|
|
29
|
-
|
|
39
|
+
runUpstreamHook,
|
|
30
40
|
} from './hook-dispatch';
|
|
41
|
+
import { type SelfUpdatePlan, planSelfUpdate, runSelfUpdate } from './self-update';
|
|
31
42
|
|
|
32
43
|
export interface HookDispatcher {
|
|
33
44
|
/** Run the matching hooks for a single event. Returns the per-hook results. */
|
|
34
|
-
handleEvent(event: PublishEvent): Promise<
|
|
45
|
+
handleEvent(event: PublishEvent): Promise<UpstreamHookResult[]>;
|
|
35
46
|
/** Clean up (currently a no-op; included for symmetry with the receiver). */
|
|
36
47
|
stop(): Promise<void>;
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
export interface HookDispatcherOptions {
|
|
51
|
+
/**
|
|
52
|
+
* Database for the default executor hop. Production omits it and the
|
|
53
|
+
* process database opens lazily; tests inject an isolated client.
|
|
54
|
+
*/
|
|
55
|
+
db?: Parameters<typeof defaultUpstreamHookDeps>[0];
|
|
40
56
|
/**
|
|
41
57
|
* Override the module-loading step. Tests inject a fixture; in
|
|
42
58
|
* production we omit and the dispatcher reads from the celilo
|
|
43
59
|
* modules table.
|
|
44
60
|
*/
|
|
45
61
|
loadModules?: () => ModuleHookContext[];
|
|
62
|
+
/**
|
|
63
|
+
* Override the executor hop for one matched upstream-publish entry.
|
|
64
|
+
* Production omits it; `runUpstreamHook` over the process database runs.
|
|
65
|
+
*/
|
|
66
|
+
runUpstreamHook?: (plan: HookDispatchPlan, event: PublishEvent) => Promise<UpstreamHookResult>;
|
|
46
67
|
/**
|
|
47
68
|
* Override the durable outcome record. The production default writes one
|
|
48
|
-
* `build_bus_hook_runs` row per run — without it a failed
|
|
49
|
-
* exists only as daemon console output, which no surface
|
|
50
|
-
* (celilo#1304). Tests inject a capture to stay DB-free.
|
|
69
|
+
* `build_bus_hook_runs` row per run — without it a failed hook run or
|
|
70
|
+
* self-update exists only as daemon console output, which no surface
|
|
71
|
+
* reads (celilo#1304). Tests inject a capture to stay DB-free. The plan
|
|
72
|
+
* is passed because the jailed executor result does not carry the
|
|
73
|
+
* script path; the ledger's `script_path` column is NOT NULL.
|
|
74
|
+
*/
|
|
75
|
+
recordRun?: (event: PublishEvent, plan: HookDispatchPlan, result: UpstreamHookResult) => void;
|
|
76
|
+
/**
|
|
77
|
+
* Override "is the control-plane module installed", the gate for the
|
|
78
|
+
* framework self-update. Production reads the modules table; tests
|
|
79
|
+
* inject the answer.
|
|
51
80
|
*/
|
|
52
|
-
|
|
81
|
+
controlPlaneInstalled?: () => boolean;
|
|
82
|
+
/** Override the self-update runner. Production: `runSelfUpdate`. */
|
|
83
|
+
selfUpdate?: (plan: SelfUpdatePlan) => ReturnType<typeof runSelfUpdate>;
|
|
53
84
|
}
|
|
54
85
|
|
|
55
|
-
/** Bound on
|
|
86
|
+
/** Bound on error tails so one chatty hook cannot bloat the ledger. */
|
|
56
87
|
const TAIL_CHARS = 2000;
|
|
57
88
|
|
|
58
89
|
function tail(s: string): string {
|
|
@@ -64,8 +95,17 @@ function tail(s: string): string {
|
|
|
64
95
|
* Throwing here would 500 nothing (the webhook already succeeded) but
|
|
65
96
|
* would kill dispatch of later plans, so a record failure is logged and
|
|
66
97
|
* swallowed — the console outcome lines below remain the fallback signal.
|
|
98
|
+
*
|
|
99
|
+
* The jailed executor path (slice 7) does not expose exit codes or
|
|
100
|
+
* captured stdio, so `exit_code` is always null — the column's "the
|
|
101
|
+
* script never reached an exit" semantics now hold of the path itself —
|
|
102
|
+
* and the executor's error text lands in `stderr_tail`.
|
|
67
103
|
*/
|
|
68
|
-
function recordHookRun(
|
|
104
|
+
function recordHookRun(
|
|
105
|
+
event: PublishEvent,
|
|
106
|
+
plan: HookDispatchPlan,
|
|
107
|
+
result: UpstreamHookResult,
|
|
108
|
+
): void {
|
|
69
109
|
try {
|
|
70
110
|
getDb()
|
|
71
111
|
.insert(buildBusHookRuns)
|
|
@@ -76,12 +116,12 @@ function recordHookRun(event: PublishEvent, result: HookRunResult): void {
|
|
|
76
116
|
tag: event.tag,
|
|
77
117
|
moduleId: result.module,
|
|
78
118
|
hookName: result.hookName,
|
|
79
|
-
scriptPath:
|
|
80
|
-
exitCode:
|
|
81
|
-
timedOut:
|
|
119
|
+
scriptPath: plan.hook.script,
|
|
120
|
+
exitCode: null,
|
|
121
|
+
timedOut: false,
|
|
82
122
|
durationMs: result.durationMs,
|
|
83
|
-
stdoutTail:
|
|
84
|
-
stderrTail: result.
|
|
123
|
+
stdoutTail: null,
|
|
124
|
+
stderrTail: result.error ? tail(result.error) : null,
|
|
85
125
|
})
|
|
86
126
|
.run();
|
|
87
127
|
} catch (err) {
|
|
@@ -95,33 +135,56 @@ export async function startHookDispatcher(
|
|
|
95
135
|
opts: HookDispatcherOptions = {},
|
|
96
136
|
): Promise<HookDispatcher> {
|
|
97
137
|
const loadModules = opts.loadModules ?? loadInstalledModulesWithHooks;
|
|
138
|
+
const runEntry =
|
|
139
|
+
opts.runUpstreamHook ??
|
|
140
|
+
((plan, event) => runUpstreamHook(plan, event, defaultUpstreamHookDeps(opts.db)));
|
|
98
141
|
const recordRun = opts.recordRun ?? recordHookRun;
|
|
142
|
+
const isControlPlaneInstalled = opts.controlPlaneInstalled ?? defaultControlPlaneInstalled;
|
|
143
|
+
const selfUpdateRunner = opts.selfUpdate ?? ((plan) => runSelfUpdate(plan));
|
|
99
144
|
|
|
100
145
|
return {
|
|
101
146
|
async handleEvent(event) {
|
|
147
|
+
// Framework self-update first (design D6): celilo replacing celilo's
|
|
148
|
+
// own binaries is not a module hook, and the dispatcher is the
|
|
149
|
+
// component that already receives the publish event.
|
|
150
|
+
const update = planSelfUpdate(event);
|
|
151
|
+
if (update && isControlPlaneInstalled()) {
|
|
152
|
+
console.log(
|
|
153
|
+
`[build-bus] self-update: ${update.packageName}@${update.version} (@${event.tag})`,
|
|
154
|
+
);
|
|
155
|
+
const result = selfUpdateRunner(update);
|
|
156
|
+
if (result.updated && result.verified) {
|
|
157
|
+
console.log(
|
|
158
|
+
` ✓ ${update.binary} reports ${result.version} (was ${result.beforeVersion ?? 'not installed'})`,
|
|
159
|
+
);
|
|
160
|
+
} else {
|
|
161
|
+
console.warn(` ✗ self-update did not complete: ${result.error ?? 'unknown error'}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
102
165
|
const moduleContexts = loadModules();
|
|
103
166
|
const plans = planHookDispatch(event, moduleContexts);
|
|
104
167
|
if (plans.length === 0) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
168
|
+
if (!update) {
|
|
169
|
+
console.log(
|
|
170
|
+
`[build-bus] ${event.package.name}@${event.package.version} (${event.tag}) — no module hooks match`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
108
173
|
return [];
|
|
109
174
|
}
|
|
110
|
-
const results:
|
|
175
|
+
const results: UpstreamHookResult[] = [];
|
|
111
176
|
for (const plan of plans) {
|
|
112
177
|
console.log(
|
|
113
178
|
`[build-bus] ${event.package.name}@${event.package.version} → ${plan.module.moduleId} (${plan.hook.name ?? plan.hook.script})`,
|
|
114
179
|
);
|
|
115
|
-
const result = await
|
|
116
|
-
recordRun(event, result);
|
|
180
|
+
const result = await runEntry(plan, event);
|
|
181
|
+
recordRun(event, plan, result);
|
|
117
182
|
results.push(result);
|
|
118
|
-
if (result.
|
|
119
|
-
console.log(` ✓
|
|
120
|
-
} else if (result.timedOut) {
|
|
121
|
-
console.warn(` ✗ timed out after ${result.durationMs}ms`);
|
|
183
|
+
if (result.success) {
|
|
184
|
+
console.log(` ✓ success in ${result.durationMs}ms`);
|
|
122
185
|
} else {
|
|
123
|
-
console.warn(` ✗
|
|
124
|
-
if (result.
|
|
186
|
+
console.warn(` ✗ failed in ${result.durationMs}ms`);
|
|
187
|
+
if (result.error) console.warn(` ${result.error.trim().slice(0, 500)}`);
|
|
125
188
|
}
|
|
126
189
|
}
|
|
127
190
|
return results;
|
|
@@ -133,6 +196,21 @@ export async function startHookDispatcher(
|
|
|
133
196
|
};
|
|
134
197
|
}
|
|
135
198
|
|
|
199
|
+
/**
|
|
200
|
+
* The framework self-update runs only where the control-plane module is
|
|
201
|
+
* deployed — the same population celilo-mgmt's hook covered, since the
|
|
202
|
+
* module is installed on the management host and nowhere else.
|
|
203
|
+
*/
|
|
204
|
+
function defaultControlPlaneInstalled(): boolean {
|
|
205
|
+
const db = getDb();
|
|
206
|
+
const row = db
|
|
207
|
+
.select({ id: modules.id })
|
|
208
|
+
.from(modules)
|
|
209
|
+
.where(eq(modules.id, CONTROL_PLANE_MODULE_ID))
|
|
210
|
+
.get();
|
|
211
|
+
return row != null;
|
|
212
|
+
}
|
|
213
|
+
|
|
136
214
|
/**
|
|
137
215
|
* Load every installed module's `on_upstream_publish` hooks from the
|
|
138
216
|
* celilo DB + each module's manifest.yml. Modules without the hook
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import {
|
|
6
|
+
getReceiverUnitPath,
|
|
7
|
+
installReceiverDaemon,
|
|
8
|
+
planReceiverInstall,
|
|
9
|
+
renderReceiverLaunchdPlist,
|
|
10
|
+
renderReceiverSystemdUnit,
|
|
11
|
+
uninstallReceiverDaemon,
|
|
12
|
+
} from './receiver-daemon';
|
|
13
|
+
|
|
14
|
+
const SECRET = 'a1b2c3d4e5f6secret';
|
|
15
|
+
|
|
16
|
+
// resolveCeliloPath existence-checks explicit overrides, so plan tests need
|
|
17
|
+
// a path that exists — a fake binary in a temp dir.
|
|
18
|
+
const FAKE_CELOLILO = join(mkdtempSync(join(tmpdir(), 'celilo-fake-bin-')), 'celilo');
|
|
19
|
+
writeFileSync(FAKE_CELOLILO, '#!/bin/sh\n', { mode: 0o755 });
|
|
20
|
+
|
|
21
|
+
describe('renderReceiverSystemdUnit', () => {
|
|
22
|
+
it('runs `subscribers serve` with --no-dispatch and the secret as an Environment line', () => {
|
|
23
|
+
const out = renderReceiverSystemdUnit({
|
|
24
|
+
celiloPath: FAKE_CELOLILO,
|
|
25
|
+
port: 8123,
|
|
26
|
+
secret: SECRET,
|
|
27
|
+
scope: 'user',
|
|
28
|
+
dispatch: false,
|
|
29
|
+
});
|
|
30
|
+
expect(out).toContain('subscribers serve --port 8123 --no-dispatch');
|
|
31
|
+
expect(out).toContain(`Environment=CELILO_BUS_SECRET=${SECRET}`);
|
|
32
|
+
expect(out).toContain('Restart=on-failure');
|
|
33
|
+
expect(out).toContain('WantedBy=default.target');
|
|
34
|
+
expect(out).not.toContain('User=');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('honors the port and a --dispatch render for boxes without the events daemon', () => {
|
|
38
|
+
const out = renderReceiverSystemdUnit({
|
|
39
|
+
celiloPath: '/c',
|
|
40
|
+
port: 9999,
|
|
41
|
+
secret: SECRET,
|
|
42
|
+
scope: 'user',
|
|
43
|
+
dispatch: true,
|
|
44
|
+
});
|
|
45
|
+
expect(out).toContain('ExecStart=/c subscribers serve --port 9999');
|
|
46
|
+
expect(out).not.toContain('--no-dispatch');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('system scope sets an explicit User= and multi-user.target', () => {
|
|
50
|
+
const out = renderReceiverSystemdUnit({
|
|
51
|
+
celiloPath: FAKE_CELOLILO,
|
|
52
|
+
port: 8123,
|
|
53
|
+
secret: SECRET,
|
|
54
|
+
scope: 'system',
|
|
55
|
+
runAsUser: 'celilo',
|
|
56
|
+
dispatch: false,
|
|
57
|
+
});
|
|
58
|
+
expect(out).toContain('User=celilo');
|
|
59
|
+
expect(out).toContain('WantedBy=multi-user.target');
|
|
60
|
+
expect(out).toContain('journalctl -u celilo-build-bus-receiver.service');
|
|
61
|
+
expect(out).not.toContain('journalctl --user');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('renderReceiverLaunchdPlist', () => {
|
|
66
|
+
it('carries the secret under EnvironmentVariables and --no-dispatch by default', () => {
|
|
67
|
+
const out = renderReceiverLaunchdPlist({
|
|
68
|
+
celiloPath: FAKE_CELOLILO,
|
|
69
|
+
port: 8123,
|
|
70
|
+
secret: SECRET,
|
|
71
|
+
scope: 'system',
|
|
72
|
+
home: '/root',
|
|
73
|
+
dispatch: false,
|
|
74
|
+
});
|
|
75
|
+
expect(out).toContain('<key>Label</key>');
|
|
76
|
+
expect(out).toContain('com.celilo.build-bus-receiver');
|
|
77
|
+
expect(out).toContain('<key>CELILO_BUS_SECRET</key>');
|
|
78
|
+
expect(out).toContain(`<string>${SECRET}</string>`);
|
|
79
|
+
expect(out).toContain('<string>--no-dispatch</string>');
|
|
80
|
+
expect(out).toContain('<string>subscribers</string>');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('getReceiverUnitPath', () => {
|
|
85
|
+
it('names the unit distinctly from the dispatcher unit in every platform/scope', () => {
|
|
86
|
+
expect(getReceiverUnitPath('linux', '/home/op', 'user')).toBe(
|
|
87
|
+
join('/home/op/.config/systemd/user', 'celilo-build-bus-receiver.service'),
|
|
88
|
+
);
|
|
89
|
+
expect(getReceiverUnitPath('linux', '/root', 'system')).toBe(
|
|
90
|
+
'/etc/systemd/system/celilo-build-bus-receiver.service',
|
|
91
|
+
);
|
|
92
|
+
expect(getReceiverUnitPath('darwin', '/root', 'system')).toBe(
|
|
93
|
+
'/Library/LaunchDaemons/com.celilo.build-bus-receiver.plist',
|
|
94
|
+
);
|
|
95
|
+
expect(getReceiverUnitPath('darwin', '/home/op', 'user')).toBe(
|
|
96
|
+
'/home/op/Library/LaunchAgents/com.celilo.build-bus-receiver.plist',
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('planReceiverInstall', () => {
|
|
102
|
+
it('requires a secret and refuses to invent one', () => {
|
|
103
|
+
expect(() => planReceiverInstall({ secret: undefined, home: '/h' })).toThrow(/secret/i);
|
|
104
|
+
expect(() => planReceiverInstall({ secret: '', home: '/h' })).toThrow(/secret/i);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('plans a user-scope unit that runs the receiver receiver-only by default', () => {
|
|
108
|
+
const plan = planReceiverInstall({
|
|
109
|
+
secret: SECRET,
|
|
110
|
+
home: '/home/op',
|
|
111
|
+
platform: 'linux',
|
|
112
|
+
celiloPath: FAKE_CELOLILO,
|
|
113
|
+
});
|
|
114
|
+
expect(plan.platform).toBe('linux');
|
|
115
|
+
expect(plan.scope).toBe('user');
|
|
116
|
+
expect(plan.unitContent).toContain('--no-dispatch');
|
|
117
|
+
expect(plan.unitContent).toContain(`CELILO_BUS_SECRET=${SECRET}`);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('plans a system-scope unit with an explicit run-as user', () => {
|
|
121
|
+
const plan = planReceiverInstall({
|
|
122
|
+
secret: SECRET,
|
|
123
|
+
platform: 'linux',
|
|
124
|
+
scope: 'system',
|
|
125
|
+
home: '/root',
|
|
126
|
+
runAsUser: 'celilo',
|
|
127
|
+
celiloPath: FAKE_CELOLILO,
|
|
128
|
+
});
|
|
129
|
+
expect(plan.unitContent).toContain('User=celilo');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('system scope falls back to the bus-dir owner when no run-as user is given', () => {
|
|
133
|
+
// events-daemon's resolveRunAsUser is the shared seam; here we only pin
|
|
134
|
+
// that the receiver unit passes a runAsUser through rather than dropping it.
|
|
135
|
+
const plan = planReceiverInstall({
|
|
136
|
+
secret: SECRET,
|
|
137
|
+
platform: 'linux',
|
|
138
|
+
scope: 'system',
|
|
139
|
+
home: '/root',
|
|
140
|
+
runAsUser: 'celilo',
|
|
141
|
+
celiloPath: FAKE_CELOLILO,
|
|
142
|
+
});
|
|
143
|
+
expect(plan.runAsUser).toBe('celilo');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('installReceiverDaemon / uninstallReceiverDaemon', () => {
|
|
148
|
+
const home = mkdtempSync(join(tmpdir(), 'celilo-receiver-daemon-'));
|
|
149
|
+
|
|
150
|
+
it('writes the unit file mode 0600 (it carries a secret) and idempotently rewrites', () => {
|
|
151
|
+
// celiloPath is passed explicitly: the unit under test must not
|
|
152
|
+
// consult the ambient PATH (resolveCeliloPath throws where no global
|
|
153
|
+
// celilo is installed — forge runners). Every planReceiverInstall test
|
|
154
|
+
// above injects FAKE_CELOLILO; the install path resolves the same way.
|
|
155
|
+
const first = installReceiverDaemon({
|
|
156
|
+
secret: SECRET,
|
|
157
|
+
platform: 'linux',
|
|
158
|
+
scope: 'user',
|
|
159
|
+
home,
|
|
160
|
+
celiloPath: FAKE_CELOLILO,
|
|
161
|
+
});
|
|
162
|
+
expect(existsSync(first.unitPath)).toBe(true);
|
|
163
|
+
expect(statSync(first.unitPath).mode & 0o777).toBe(0o600);
|
|
164
|
+
const content = readFileSync(first.unitPath, 'utf-8');
|
|
165
|
+
expect(content).toContain('--no-dispatch');
|
|
166
|
+
expect(content).toContain(`CELILO_BUS_SECRET=${SECRET}`);
|
|
167
|
+
|
|
168
|
+
// Idempotent second install.
|
|
169
|
+
const second = installReceiverDaemon({
|
|
170
|
+
secret: SECRET,
|
|
171
|
+
platform: 'linux',
|
|
172
|
+
scope: 'user',
|
|
173
|
+
home,
|
|
174
|
+
celiloPath: FAKE_CELOLILO,
|
|
175
|
+
});
|
|
176
|
+
expect(second.unitPath).toBe(first.unitPath);
|
|
177
|
+
|
|
178
|
+
const removed = uninstallReceiverDaemon({ platform: 'linux', scope: 'user', home });
|
|
179
|
+
expect(removed.removed).toBe(true);
|
|
180
|
+
expect(existsSync(first.unitPath)).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('uninstall of a missing unit reports removed=false', () => {
|
|
184
|
+
const empty = mkdtempSync(join(tmpdir(), 'celilo-receiver-daemon-'));
|
|
185
|
+
const result = uninstallReceiverDaemon({ platform: 'linux', scope: 'user', home: empty });
|
|
186
|
+
expect(result.removed).toBe(false);
|
|
187
|
+
rmSync(empty, { recursive: true, force: true });
|
|
188
|
+
});
|
|
189
|
+
});
|