companion-runtime-manager-integration.test.mjs file-level

at sha256:3 · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 πŸ’₯ blast risk
sha256:9 feat(calendar): hosted bridge/gateway route parity and timeline noteRec… · aaronrene · Jun 19, 2026
1 /**
2 * Tier 2 β€” INTEGRATION: lib/companion-runtime-manager.mjs
3 *
4 * Combined flows through multiple functions β€” integrity β†’ lifecycle β†’ admission β†’ resource.
5 * Tests the interactions between sub-systems, ensuring the composition of pure functions
6 * produces the correct combined behaviour across realistic call sequences.
7 *
8 * Reference: docs/COMPANION-APP-PHASE-4-RUNTIME-MANAGER.md Β§3 (lifecycle), Β§4 (backpressure).
9 */
10
11 import { describe, it } from 'node:test';
12 import assert from 'node:assert/strict';
13 import crypto from 'node:crypto';
14
15 import {
16 RUNTIME_MANAGER_REASONS,
17 LIFECYCLE_STATES,
18 LIFECYCLE_EVENTS,
19 createIntegrityAccumulator,
20 verifyModelBytes,
21 createLifecycleState,
22 transitionLifecycle,
23 canServeInference,
24 createAdmissionState,
25 evaluateAdmission,
26 recordInFlight,
27 recordCompletion,
28 recordQueued,
29 recordDequeued,
30 createResourceLimits,
31 evaluateResourceLimits,
32 evaluateRuntimeRequest,
33 } from '../lib/companion-runtime-manager.mjs';
34
35 function makeDigest(data) {
36 return crypto.createHash('sha256').update(data).digest('hex');
37 }
38
39 const ALLOWED_URLS = ['https://models.example.com/'];
40 const VALID_URL = 'https://models.example.com/model.gguf';
41
42 // ── Integration: integrity β†’ lifecycle cold-start sequence ──────────────────
43
44 describe('integrity verification β†’ lifecycle cold-start success', () => {
45 it('full happy path: verify β†’ start β†’ health_ok β†’ ready β†’ serve', () => {
46 // Phase 5 will call this sequence. We simulate it purely.
47 const modelData = Buffer.from('simulated model binary data for integration test');
48 const digest = makeDigest(modelData);
49
50 // 1. Verify model integrity (simulated Phase 5 streaming download)
51 const acc = createIntegrityAccumulator({
52 expectedDigest: digest, expectedSizeBytes: modelData.length,
53 sourceUrl: VALID_URL, allowedSourceUrls: ALLOWED_URLS,
54 });
55 // Simulate chunked download
56 const chunkSize = 8;
57 for (let i = 0; i < modelData.length; i += chunkSize) {
58 acc.update(modelData.subarray(i, i + chunkSize));
59 }
60 const integrityVerdict = acc.finalize();
61 assert.equal(integrityVerdict.ok, true, 'integrity must pass before lifecycle proceeds');
62
63 // 2. Only after integrity passes, transition lifecycle
64 let lifecycle = createLifecycleState();
65 assert.equal(lifecycle.state, LIFECYCLE_STATES.STOPPED);
66
67 let tr = transitionLifecycle(lifecycle, LIFECYCLE_EVENTS.START);
68 assert.equal(tr.ok, true);
69 lifecycle = tr.newState;
70 assert.equal(lifecycle.state, LIFECYCLE_STATES.STARTING);
71
72 // Simulate health-check pass (Phase 5 health probe succeeds)
73 tr = transitionLifecycle(lifecycle, LIFECYCLE_EVENTS.HEALTH_OK);
74 assert.equal(tr.ok, true);
75 lifecycle = tr.newState;
76 assert.equal(lifecycle.state, LIFECYCLE_STATES.READY);
77
78 // 3. Now admission and resource checks
79 const admission = createAdmissionState({ maxInFlight: 4, queueBound: 8 });
80 const limits = createResourceLimits({ maxRamBytes: 8e9, maxVramBytes: 4e9, maxCpuPercent: 80 });
81 const obs = { ramBytes: 1e9, vramBytes: 0.5e9, cpuPercent: 10 };
82
83 const decision = evaluateRuntimeRequest({
84 lifecycleState: lifecycle, admissionState: admission,
85 resourceObservation: obs, resourceLimits: limits,
86 });
87 assert.equal(decision.ok, true);
88 });
89 });
90
91 describe('integrity failure β†’ lifecycle stays stopped', () => {
92 it('wrong digest prevents lifecycle start (Phase 5 must not call start on fail)', () => {
93 const modelData = Buffer.from('good model data');
94 const wrongDigest = '0'.repeat(64);
95
96 const r = verifyModelBytes({
97 fileData: modelData, expectedDigest: wrongDigest,
98 expectedSizeBytes: modelData.length, sourceUrl: VALID_URL, allowedSourceUrls: ALLOWED_URLS,
99 });
100 assert.equal(r.ok, false);
101 assert.equal(r.reason, RUNTIME_MANAGER_REASONS.DIGEST_MISMATCH);
102
103 // Phase 5 MUST NOT call transitionLifecycle(START) when integrity fails.
104 // The lifecycle stays in stopped β€” no way to reach ready without integrity passing.
105 const lifecycle = createLifecycleState();
106 assert.equal(canServeInference(lifecycle), false);
107 });
108 });
109
110 // ── Integration: cold-start failure path ────────────────────────────────────
111
112 describe('cold-start failure: starting β†’ stopped on health_fail', () => {
113 it('health_fail returns lifecycle to stopped, inference still denied', () => {
114 let lifecycle = createLifecycleState();
115
116 let tr = transitionLifecycle(lifecycle, LIFECYCLE_EVENTS.START);
117 lifecycle = tr.newState;
118 assert.equal(lifecycle.state, LIFECYCLE_STATES.STARTING);
119 assert.equal(canServeInference(lifecycle), false); // cannot serve while starting
120
121 tr = transitionLifecycle(lifecycle, LIFECYCLE_EVENTS.HEALTH_FAIL);
122 lifecycle = tr.newState;
123 assert.equal(lifecycle.state, LIFECYCLE_STATES.STOPPED);
124 assert.equal(canServeInference(lifecycle), false);
125 });
126
127 it('can restart after health_fail (stopped β†’ starting again)', () => {
128 let lifecycle = createLifecycleState();
129 lifecycle = transitionLifecycle(lifecycle, LIFECYCLE_EVENTS.START).newState;
130 lifecycle = transitionLifecycle(lifecycle, LIFECYCLE_EVENTS.HEALTH_FAIL).newState;
131 assert.equal(lifecycle.state, LIFECYCLE_STATES.STOPPED);
132
133 // Can start again
134 const tr = transitionLifecycle(lifecycle, LIFECYCLE_EVENTS.START);
135 assert.equal(tr.ok, true);
136 assert.equal(tr.newState.state, LIFECYCLE_STATES.STARTING);
137 });
138 });
139
140 // ── Integration: graceful drain sequence ────────────────────────────────────
141
142 describe('graceful drain: ready β†’ draining β†’ stopped', () => {
143 it('inference denied in draining state', () => {
144 let lifecycle = createLifecycleState();
145 lifecycle = transitionLifecycle(lifecycle, LIFECYCLE_EVENTS.START).newState;
146 lifecycle = transitionLifecycle(lifecycle, LIFECYCLE_EVENTS.HEALTH_OK).newState;
147 assert.equal(canServeInference(lifecycle), true);
148
149 lifecycle = transitionLifecycle(lifecycle, LIFECYCLE_EVENTS.DRAIN).newState;
150 assert.equal(lifecycle.state, LIFECYCLE_STATES.DRAINING);
151 assert.equal(canServeInference(lifecycle), false); // no new requests during drain
152
153 lifecycle = transitionLifecycle(lifecycle, LIFECYCLE_EVENTS.STOPPED).newState;
154 assert.equal(lifecycle.state, LIFECYCLE_STATES.STOPPED);
155 assert.equal(canServeInference(lifecycle), false);
156 });
157 });
158
159 // ── Integration: admission state cycling ────────────────────────────────────
160
161 describe('admission cycling with recordInFlight/recordCompletion', () => {
162 it('inFlight returns to 0 after all completions', () => {
163 let s = createAdmissionState({ maxInFlight: 4, queueBound: 8 });
164
165 s = recordInFlight(s);
166 s = recordInFlight(s);
167 s = recordInFlight(s);
168 assert.equal(s.inFlight, 3);
169
170 s = recordCompletion(s);
171 s = recordCompletion(s);
172 s = recordCompletion(s);
173 assert.equal(s.inFlight, 0);
174 });
175
176 it('admission allows again after completion brings inFlight below max', () => {
177 let s = createAdmissionState({ maxInFlight: 2, queueBound: 4 });
178
179 s = recordInFlight(s);
180 s = recordInFlight(s);
181 assert.equal(evaluateAdmission(s).reason, RUNTIME_MANAGER_REASONS.AT_CAPACITY);
182
183 s = recordCompletion(s);
184 assert.equal(evaluateAdmission(s).ok, true);
185 });
186
187 it('queue tracking does not affect inFlight', () => {
188 let s = createAdmissionState({ maxInFlight: 2, queueBound: 4 });
189 s = recordInFlight(s);
190 s = recordQueued(s);
191 s = recordQueued(s);
192 assert.equal(s.inFlight, 1);
193 assert.equal(s.queued, 2);
194 s = recordDequeued(s);
195 assert.equal(s.queued, 1);
196 assert.equal(s.inFlight, 1); // unchanged
197 });
198 });
199
200 // ── Integration: resource + admission combined ───────────────────────────────
201
202 describe('resource limits integrated with admission decision', () => {
203 it('resource over-limit while in-flight slots free β†’ resource gate fails', () => {
204 const lifecycle = { state: LIFECYCLE_STATES.READY };
205 const admission = createAdmissionState({ maxInFlight: 4, queueBound: 8 });
206 const limits = createResourceLimits({ maxRamBytes: 4e9, maxVramBytes: 4e9, maxCpuPercent: 80 });
207 const obs = { ramBytes: 5e9, vramBytes: 0, cpuPercent: 20 }; // RAM over
208
209 const r = evaluateRuntimeRequest({
210 lifecycleState: lifecycle, admissionState: admission,
211 resourceObservation: obs, resourceLimits: limits,
212 });
213 assert.equal(r.ok, false);
214 assert.equal(r.reason, RUNTIME_MANAGER_REASONS.RAM_OVER_LIMIT);
215 });
216
217 it('resource within limits + admission within limits + ready β†’ ok', () => {
218 const lifecycle = { state: LIFECYCLE_STATES.READY };
219 const admission = createAdmissionState({ maxInFlight: 4, queueBound: 8 });
220 const limits = createResourceLimits({ maxRamBytes: 8e9, maxVramBytes: 4e9, maxCpuPercent: 80 });
221 const obs = { ramBytes: 1e9, vramBytes: 0, cpuPercent: 20 };
222
223 const r = evaluateRuntimeRequest({
224 lifecycleState: lifecycle, admissionState: admission,
225 resourceObservation: obs, resourceLimits: limits,
226 });
227 assert.equal(r.ok, true);
228 });
229 });
230
231 // ── Integration: streaming integrity accumulator ─────────────────────────────
232
233 describe('streaming integrity accumulator with many small chunks', () => {
234 it('matches single-shot verifyModelBytes for identical data', () => {
235 const data = Buffer.alloc(1024, 0xab); // 1 KB of known bytes
236 const digest = makeDigest(data);
237
238 // Streaming path (accumulator)
239 const acc = createIntegrityAccumulator({
240 expectedDigest: digest, expectedSizeBytes: data.length,
241 sourceUrl: VALID_URL, allowedSourceUrls: ALLOWED_URLS,
242 });
243 for (let i = 0; i < data.length; i++) {
244 acc.update(data.subarray(i, i + 1)); // 1 byte at a time
245 }
246 const streamingVerdict = acc.finalize();
247
248 // In-memory path
249 const memoryVerdict = verifyModelBytes({
250 fileData: data, expectedDigest: digest, expectedSizeBytes: data.length,
251 sourceUrl: VALID_URL, allowedSourceUrls: ALLOWED_URLS,
252 });
253
254 assert.equal(streamingVerdict.ok, true);
255 assert.equal(memoryVerdict.ok, true);
256 });
257 });