companion-shell-unit.test.mjs
155 lines 5.6 KB
Raw
sha256:8915fe406161f95c1681f9469375e7bae5b28c884f00bedbdef65e4b0cd0738d docs(flow): commit FLOW-V0-SPEC.md hygiene for 7A-INT merge Human 13 hours ago
1 /**
2 * Tier 1 — UNIT: Phase 5 companion shell and I/O adapters.
3 */
4
5 import { describe, it } from 'node:test';
6 import assert from 'node:assert/strict';
7
8 import { KEYCHAIN_ACCOUNTS } from '../lib/companion-token-custody.mjs';
9 import {
10 KEYCHAIN_ADAPTER_REASONS,
11 requireKeychainSecret,
12 requireKnownKeychainAccount,
13 } from '../lib/companion-keychain-adapter.mjs';
14 import { buildAllowedHosts } from '../lib/companion-inference-listener.mjs';
15 import {
16 SECRET_ENV_KEY_PATTERNS,
17 buildRuntimeArgv,
18 createScrubbedRuntimeEnv,
19 } from '../lib/companion-spawn-adapter.mjs';
20 import { requireHttpsDownloadUrl } from '../lib/companion-download-adapter.mjs';
21 import { requireRuntimePid } from '../lib/companion-resource-probe.mjs';
22 import {
23 computeCompanionAvailable,
24 createRuntimeGroup,
25 validateManifestTrustAnchor,
26 } from '../lib/companion-shell.mjs';
27
28 describe('keychain adapter surface', () => {
29 it('accepts only the four custody accounts', () => {
30 for (const account of Object.values(KEYCHAIN_ACCOUNTS)) {
31 assert.equal(requireKnownKeychainAccount(account), account);
32 }
33 assert.throws(
34 () => requireKnownKeychainAccount('knowtation.companion.other'),
35 { message: KEYCHAIN_ADAPTER_REASONS.UNKNOWN_ACCOUNT },
36 );
37 });
38
39 it('rejects empty and oversized secrets', () => {
40 assert.throws(() => requireKeychainSecret(''));
41 assert.throws(() => requireKeychainSecret('x'.repeat(8193)));
42 assert.equal(requireKeychainSecret('ok-secret'), 'ok-secret');
43 });
44 });
45
46 describe('loopback bind helpers', () => {
47 it('builds allowed hosts from an IPv4 loopback port', () => {
48 assert.deepEqual(buildAllowedHosts({ host: '127.0.0.1', port: 49152 }), [
49 '127.0.0.1:49152',
50 'localhost:49152',
51 ]);
52 });
53
54 it('rejects wildcard bind helpers', () => {
55 assert.throws(() => buildAllowedHosts({ host: '0.0.0.0', port: 49152 }));
56 assert.throws(() => buildAllowedHosts({ host: '::', port: 49152 }));
57 });
58 });
59
60 describe('spawn adapter helpers', () => {
61 it('scrubs secret-bearing environment keys', () => {
62 const env = createScrubbedRuntimeEnv({
63 HOME: '/home/a',
64 PATH: '/bin',
65 SESSION_SECRET: 'secret',
66 OPENAI_API_KEY: 'secret',
67 REFRESH_TOKEN: 'secret',
68 JWT: 'secret',
69 });
70 assert.deepEqual(env, { HOME: '/home/a', PATH: '/bin' });
71 });
72
73 it('builds argv from inert values only', () => {
74 const argv = buildRuntimeArgv({
75 modelPath: '/models/verified.gguf',
76 port: 41234,
77 maxRamBytes: 1024,
78 });
79 assert.deepEqual(argv, [
80 '--host', '127.0.0.1',
81 '--port', '41234',
82 '--model', '/models/verified.gguf',
83 '--max-ram-bytes', '1024',
84 ]);
85 });
86
87 it('secret env patterns cover API keys, tokens, JWTs, and keychain refs', () => {
88 for (const key of ['SESSION_SECRET', 'OPENAI_API_KEY', 'JWT', 'REFRESH_TOKEN', 'KEYCHAIN_REF']) {
89 assert.equal(SECRET_ENV_KEY_PATTERNS.some((pattern) => pattern.test(key)), true, key);
90 }
91 });
92 });
93
94 describe('download and resource helpers', () => {
95 it('download URLs must be HTTPS', () => {
96 assert.equal(requireHttpsDownloadUrl('https://models.example.com/m.bin').protocol, 'https:');
97 assert.throws(() => requireHttpsDownloadUrl('http://models.example.com/m.bin'));
98 });
99
100 it('resource probe requires a positive runtime PID', () => {
101 assert.equal(requireRuntimePid(123), 123);
102 assert.throws(() => requireRuntimePid(0));
103 assert.throws(() => requireRuntimePid(-1));
104 });
105 });
106
107 describe('companionAvailable predicate', () => {
108 const ready = { state: 'ready' };
109
110 it('is true only when every Phase 5 readiness condition holds', () => {
111 assert.equal(computeCompanionAvailable({
112 integrityVerified: true,
113 lifecycleState: ready,
114 lastHealthOkAt: 1000,
115 now: 1100,
116 listenerBound: true,
117 loopbackTokenPresent: true,
118 healthRecencyMs: 500,
119 }), true);
120 });
121
122 it('fails closed for stale health, missing token, or missing integrity', () => {
123 assert.equal(computeCompanionAvailable({ integrityVerified: false, lifecycleState: ready, lastHealthOkAt: 1000, now: 1100, listenerBound: true, loopbackTokenPresent: true }), false);
124 assert.equal(computeCompanionAvailable({ integrityVerified: true, lifecycleState: ready, lastHealthOkAt: 1000, now: 20_000, listenerBound: true, loopbackTokenPresent: true }), false);
125 assert.equal(computeCompanionAvailable({ integrityVerified: true, lifecycleState: ready, lastHealthOkAt: 1000, now: 1100, listenerBound: true, loopbackTokenPresent: false }), false);
126 });
127 });
128
129 describe('manifest and runtime group shape', () => {
130 it('requires the manifest origin to be out-of-band from model bytes', () => {
131 assert.equal(validateManifestTrustAnchor({
132 manifestUrl: 'https://gateway.knowtation.com/models/manifest.json',
133 modelUrl: 'https://cdn.knowtation-models.com/model.bin',
134 expectedDigest: 'a'.repeat(64),
135 expectedSizeBytes: 12,
136 allowedSourceUrls: ['https://cdn.knowtation-models.com/'],
137 }).ok, true);
138 assert.equal(validateManifestTrustAnchor({
139 manifestUrl: 'https://cdn.knowtation-models.com/manifest.json',
140 modelUrl: 'https://cdn.knowtation-models.com/model.bin',
141 expectedDigest: 'a'.repeat(64),
142 expectedSizeBytes: 12,
143 allowedSourceUrls: ['https://cdn.knowtation-models.com/'],
144 }).ok, false);
145 });
146
147 it('runtime group exposes no authority accessors', () => {
148 const group = createRuntimeGroup({
149 spawn: async () => ({ pid: 1, kill: async () => {} }),
150 download: async () => {},
151 healthCheck: async () => true,
152 });
153 assert.deepEqual(Object.keys(group).sort(), ['download', 'healthCheck', 'spawn', 'statResources'].sort());
154 });
155 });
File History 1 commit
sha256:8915fe406161f95c1681f9469375e7bae5b28c884f00bedbdef65e4b0cd0738d docs(flow): commit FLOW-V0-SPEC.md hygiene for 7A-INT merge Human 13 hours ago