principles-disciple 1.76.0 → 1.78.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/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/commands/context.ts +5 -5
- package/src/core/surface-guard.ts +130 -0
- package/src/hooks/prompt.ts +9 -117
- package/src/i18n/commands.ts +2 -2
- package/src/index.ts +45 -27
- package/src/types.ts +3 -3
- package/tests/core-anti-growth.test.ts +1 -0
- package/tests/hooks/prompt-characterization.test.ts +22 -36
- package/tests/hooks/prompt-diet.test.ts +343 -0
- package/tests/integration/mvp-surface-registry-guard.test.ts +398 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import {
|
|
5
|
+
PLUGIN_SURFACE_REGISTRY,
|
|
6
|
+
validateSurfaceRegistry,
|
|
7
|
+
findUnclassifiedSurfaces,
|
|
8
|
+
getSurfacesByCategory,
|
|
9
|
+
getSurfacesByKind,
|
|
10
|
+
} from '@principles/core/runtime-v2';
|
|
11
|
+
|
|
12
|
+
function findRepoRoot(cwd: string): string {
|
|
13
|
+
let dir = cwd;
|
|
14
|
+
while (dir !== path.dirname(dir)) {
|
|
15
|
+
if (fs.existsSync(path.join(dir, '.git'))) {
|
|
16
|
+
return dir;
|
|
17
|
+
}
|
|
18
|
+
dir = path.dirname(dir);
|
|
19
|
+
}
|
|
20
|
+
return cwd;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const repoRoot = findRepoRoot(process.cwd());
|
|
24
|
+
|
|
25
|
+
function read(relativePath: string): string {
|
|
26
|
+
return fs.readFileSync(path.join(repoRoot, relativePath), 'utf8');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface ApiOnRegistration {
|
|
30
|
+
event: string;
|
|
31
|
+
surfaceId: string | null;
|
|
32
|
+
rawLine: string;
|
|
33
|
+
index: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function extractApiOnRegistrations(source: string): ApiOnRegistration[] {
|
|
37
|
+
const registrations: ApiOnRegistration[] = [];
|
|
38
|
+
const apiOnPattern = /api\.on\s*\(\s*['"]([^'"]+)['"]\s*,\s*/g;
|
|
39
|
+
let match: RegExpExecArray | null;
|
|
40
|
+
let regIndex = 0;
|
|
41
|
+
while ((match = apiOnPattern.exec(source)) !== null) {
|
|
42
|
+
const event = match[1];
|
|
43
|
+
const afterMatch = source.slice(match.index + match[0].length);
|
|
44
|
+
const guardHookMatch = afterMatch.match(/^guardHook\s*\(\s*['"]([^'"]+)['"]\s*,/);
|
|
45
|
+
registrations.push({
|
|
46
|
+
event,
|
|
47
|
+
surfaceId: guardHookMatch ? guardHookMatch[1] : null,
|
|
48
|
+
rawLine: match[0],
|
|
49
|
+
index: regIndex++,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return registrations;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface ServiceRegistration {
|
|
56
|
+
surfaceId: string;
|
|
57
|
+
index: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function extractServiceRegistrations(source: string): ServiceRegistration[] {
|
|
61
|
+
const registrations: ServiceRegistration[] = [];
|
|
62
|
+
const servicePattern = /guardService\s*\(\s*['"]([^'"]+)['"]\s*,/g;
|
|
63
|
+
let match: RegExpExecArray | null;
|
|
64
|
+
let serviceIndex = 0;
|
|
65
|
+
while ((match = servicePattern.exec(source)) !== null) {
|
|
66
|
+
registrations.push({
|
|
67
|
+
surfaceId: match[1],
|
|
68
|
+
index: serviceIndex++,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return registrations;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describe('MVP Surface Registry Guard (PRI-289)', () => {
|
|
75
|
+
describe('registry self-validation', () => {
|
|
76
|
+
it('PLUGIN_SURFACE_REGISTRY passes validateSurfaceRegistry', () => {
|
|
77
|
+
const result = validateSurfaceRegistry(PLUGIN_SURFACE_REGISTRY);
|
|
78
|
+
expect(result.valid).toBe(true);
|
|
79
|
+
expect(result.errors).toEqual([]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('has no duplicate surface ids', () => {
|
|
83
|
+
const ids = PLUGIN_SURFACE_REGISTRY.map(s => s.id);
|
|
84
|
+
const uniqueIds = new Set(ids);
|
|
85
|
+
expect(uniqueIds.size).toBe(ids.length);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('core surfaces are all enabledByDefault', () => {
|
|
89
|
+
const coreSurfaces = getSurfacesByCategory(PLUGIN_SURFACE_REGISTRY, 'core');
|
|
90
|
+
for (const surface of coreSurfaces) {
|
|
91
|
+
expect(surface.enabledByDefault).toBe(true);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('non-core surfaces are not enabledByDefault', () => {
|
|
96
|
+
const nonCore = PLUGIN_SURFACE_REGISTRY.filter(s => s.category !== 'core');
|
|
97
|
+
for (const surface of nonCore) {
|
|
98
|
+
expect(surface.enabledByDefault).toBe(false);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('quiet/gone/legacy_retire surfaces have disabledReason', () => {
|
|
103
|
+
const disabled = PLUGIN_SURFACE_REGISTRY.filter(
|
|
104
|
+
s => s.category === 'quiet' || s.category === 'gone' || s.category === 'legacy_retire',
|
|
105
|
+
);
|
|
106
|
+
for (const surface of disabled) {
|
|
107
|
+
expect(surface.disabledReason).toBeDefined();
|
|
108
|
+
expect(typeof surface.disabledReason).toBe('string');
|
|
109
|
+
expect(surface.disabledReason!.length).toBeGreaterThan(0);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('api.on() registration coverage — every hook must be guarded', () => {
|
|
115
|
+
const source = read('packages/openclaw-plugin/src/index.ts');
|
|
116
|
+
const registrations = extractApiOnRegistrations(source);
|
|
117
|
+
|
|
118
|
+
it('has at least one api.on registration', () => {
|
|
119
|
+
expect(registrations.length).toBeGreaterThan(0);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('every api.on() handler is wrapped with guardHook()', () => {
|
|
123
|
+
const unguarded = registrations.filter(r => r.surfaceId === null);
|
|
124
|
+
if (unguarded.length > 0) {
|
|
125
|
+
const details = unguarded.map(r => `api.on('${r.event}', ...) #${r.index} — NOT wrapped with guardHook`);
|
|
126
|
+
throw new Error(
|
|
127
|
+
`Found ${unguarded.length} unguarded api.on() registration(s):\n${details.join('\n')}\n` +
|
|
128
|
+
`Every api.on() handler MUST be wrapped with guardHook('<surfaceId>', api.logger, ...) per PRI-289.`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('every guardHook surface id exists in PLUGIN_SURFACE_REGISTRY', () => {
|
|
134
|
+
const guarded = registrations.filter(r => r.surfaceId !== null);
|
|
135
|
+
const registeredIds = new Set(PLUGIN_SURFACE_REGISTRY.map(s => s.id));
|
|
136
|
+
const unclassified: string[] = [];
|
|
137
|
+
for (const reg of guarded) {
|
|
138
|
+
if (!registeredIds.has(reg.surfaceId!)) {
|
|
139
|
+
unclassified.push(reg.surfaceId!);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
expect(unclassified).toEqual([]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('each individual api.on registration is covered (no dedup by event name)', () => {
|
|
146
|
+
const guarded = registrations.filter(r => r.surfaceId !== null);
|
|
147
|
+
const registeredIds = new Set(PLUGIN_SURFACE_REGISTRY.map(s => s.id));
|
|
148
|
+
for (const reg of guarded) {
|
|
149
|
+
expect(registeredIds.has(reg.surfaceId!)).toBe(true);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('after_tool_call has two registrations: core + trajectory', () => {
|
|
154
|
+
const afterToolCallRegs = registrations.filter(r => r.event === 'after_tool_call');
|
|
155
|
+
expect(afterToolCallRegs.length).toBe(2);
|
|
156
|
+
expect(afterToolCallRegs[0].surfaceId).toBe('hook:after_tool_call');
|
|
157
|
+
expect(afterToolCallRegs[1].surfaceId).toBe('hook:after_tool_call.trajectory');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('llm_output has two registrations: core + trajectory', () => {
|
|
161
|
+
const llmOutputRegs = registrations.filter(r => r.event === 'llm_output');
|
|
162
|
+
expect(llmOutputRegs.length).toBe(2);
|
|
163
|
+
expect(llmOutputRegs[0].surfaceId).toBe('hook:llm_output');
|
|
164
|
+
expect(llmOutputRegs[1].surfaceId).toBe('hook:llm_output.trajectory');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('total api.on registrations with guardHook match registry hook count', () => {
|
|
168
|
+
const guarded = registrations.filter(r => r.surfaceId !== null);
|
|
169
|
+
const registryHookCount = PLUGIN_SURFACE_REGISTRY.filter(s => s.kind === 'hook').length;
|
|
170
|
+
expect(guarded.length).toBe(registryHookCount);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('all guardHook calls pass api.logger as second argument', () => {
|
|
174
|
+
const guardHookWithLogger = /guardHook\s*\(\s*['"][^'"]+['"]\s*,\s*api\.logger\s*,/g;
|
|
175
|
+
const guardHookTotal = /guardHook\s*\(\s*['"][^'"]+['"]\s*,/g;
|
|
176
|
+
const withLoggerCount = (source.match(guardHookWithLogger) ?? []).length;
|
|
177
|
+
const totalCount = (source.match(guardHookTotal) ?? []).length;
|
|
178
|
+
expect(withLoggerCount).toBe(totalCount);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe('service registration coverage — per-registration', () => {
|
|
183
|
+
it('every guardService() call in index.ts has a classified surface id', () => {
|
|
184
|
+
const source = read('packages/openclaw-plugin/src/index.ts');
|
|
185
|
+
const registrations = extractServiceRegistrations(source);
|
|
186
|
+
|
|
187
|
+
expect(registrations.length).toBeGreaterThan(0);
|
|
188
|
+
|
|
189
|
+
const registeredIds = new Set(
|
|
190
|
+
PLUGIN_SURFACE_REGISTRY.filter(s => s.kind === 'service').map(s => s.id),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const unclassified: string[] = [];
|
|
194
|
+
for (const reg of registrations) {
|
|
195
|
+
if (!registeredIds.has(reg.surfaceId)) {
|
|
196
|
+
unclassified.push(reg.surfaceId);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
expect(unclassified).toEqual([]);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('total service registrations match expected count', () => {
|
|
204
|
+
const source = read('packages/openclaw-plugin/src/index.ts');
|
|
205
|
+
const registrations = extractServiceRegistrations(source);
|
|
206
|
+
|
|
207
|
+
const registryServiceCount = PLUGIN_SURFACE_REGISTRY.filter(s => s.kind === 'service').length;
|
|
208
|
+
expect(registrations.length).toBe(registryServiceCount);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('ADR-0014 compliance', () => {
|
|
213
|
+
it('only MVP-Core surfaces are enabledByDefault', () => {
|
|
214
|
+
const enabledByDefault = PLUGIN_SURFACE_REGISTRY.filter(s => s.enabledByDefault);
|
|
215
|
+
for (const surface of enabledByDefault) {
|
|
216
|
+
expect(surface.category).toBe('core');
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('core hooks match ADR-0014 MVP-Core activation paths', () => {
|
|
221
|
+
const coreHooks = getSurfacesByKind(PLUGIN_SURFACE_REGISTRY, 'hook')
|
|
222
|
+
.filter(s => s.category === 'core')
|
|
223
|
+
.map(s => s.id);
|
|
224
|
+
|
|
225
|
+
expect(coreHooks).toContain('hook:before_prompt_build');
|
|
226
|
+
expect(coreHooks).toContain('hook:before_tool_call');
|
|
227
|
+
expect(coreHooks).toContain('hook:after_tool_call');
|
|
228
|
+
expect(coreHooks).toContain('hook:llm_output');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('evolution-worker service is MVP-Quiet (PRI-288/ADR-0014 alignment)', () => {
|
|
232
|
+
const ew = PLUGIN_SURFACE_REGISTRY.find(s => s.id === 'service:evolution-worker');
|
|
233
|
+
expect(ew).toBeDefined();
|
|
234
|
+
expect(ew!.category).toBe('quiet');
|
|
235
|
+
expect(ew!.enabledByDefault).toBe(false);
|
|
236
|
+
expect(ew!.disabledReason).toContain('PRI-288');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('evolution-worker startup is MVP-Quiet (PRI-288/ADR-0014 alignment)', () => {
|
|
240
|
+
const ew = PLUGIN_SURFACE_REGISTRY.find(s => s.id === 'startup:evolution-worker');
|
|
241
|
+
expect(ew).toBeDefined();
|
|
242
|
+
expect(ew!.category).toBe('quiet');
|
|
243
|
+
expect(ew!.enabledByDefault).toBe(false);
|
|
244
|
+
expect(ew!.disabledReason).toContain('PRI-288');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('trajectory hooks are MVP-Quiet (ADR-0014 §2.5)', () => {
|
|
248
|
+
const trajectoryHooks = PLUGIN_SURFACE_REGISTRY.filter(
|
|
249
|
+
s => s.kind === 'hook' && s.id.includes('trajectory'),
|
|
250
|
+
);
|
|
251
|
+
for (const hook of trajectoryHooks) {
|
|
252
|
+
expect(hook.category).toBe('quiet');
|
|
253
|
+
expect(hook.enabledByDefault).toBe(false);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('subagent/shadow hooks are MVP-Quiet (ADR-0014 §2.5)', () => {
|
|
258
|
+
const shadowHooks = PLUGIN_SURFACE_REGISTRY.filter(
|
|
259
|
+
s => s.kind === 'hook' && (s.id.includes('subagent') || s.id.includes('shadow')),
|
|
260
|
+
);
|
|
261
|
+
for (const hook of shadowHooks) {
|
|
262
|
+
expect(hook.category).toBe('quiet');
|
|
263
|
+
expect(hook.enabledByDefault).toBe(false);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('lifecycle hooks are MVP-Quiet (ADR-0014 §2.5)', () => {
|
|
268
|
+
const lifecycleHooks = PLUGIN_SURFACE_REGISTRY.filter(
|
|
269
|
+
s => s.kind === 'hook' && (s.id.includes('reset') || s.id.includes('compaction')),
|
|
270
|
+
);
|
|
271
|
+
for (const hook of lifecycleHooks) {
|
|
272
|
+
expect(hook.category).toBe('quiet');
|
|
273
|
+
expect(hook.enabledByDefault).toBe(false);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('central-sync service is MVP-Quiet (ADR-0014 §2.5: single workspace)', () => {
|
|
278
|
+
const centralSync = PLUGIN_SURFACE_REGISTRY.find(s => s.id === 'service:central-sync');
|
|
279
|
+
expect(centralSync).toBeDefined();
|
|
280
|
+
expect(centralSync!.category).toBe('quiet');
|
|
281
|
+
expect(centralSync!.enabledByDefault).toBe(false);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe('surface guard runtime', () => {
|
|
286
|
+
it('checkSurfaceGuard passes with current registry', async () => {
|
|
287
|
+
const { checkSurfaceGuard } = await import('../../src/core/surface-guard.js');
|
|
288
|
+
const result = checkSurfaceGuard();
|
|
289
|
+
expect(result.passed).toBe(true);
|
|
290
|
+
expect(result.violations).toEqual([]);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('isSurfaceEnabled returns false for unknown surface with reason', async () => {
|
|
294
|
+
const { isSurfaceEnabled } = await import('../../src/core/surface-guard.js');
|
|
295
|
+
const result = isSurfaceEnabled('hook:unknown_new_hook');
|
|
296
|
+
expect(result.enabled).toBe(false);
|
|
297
|
+
expect(result.reason).toContain('not found in registry');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('isSurfaceEnabled returns true for core surfaces', async () => {
|
|
301
|
+
const { isSurfaceEnabled } = await import('../../src/core/surface-guard.js');
|
|
302
|
+
const result = isSurfaceEnabled('hook:before_prompt_build');
|
|
303
|
+
expect(result.enabled).toBe(true);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('isSurfaceEnabled returns false for unknown surfaces even with override', async () => {
|
|
307
|
+
const { isSurfaceEnabled } = await import('../../src/core/surface-guard.js');
|
|
308
|
+
const result = isSurfaceEnabled('hook:nonexistent_gone', { 'hook:nonexistent_gone': true });
|
|
309
|
+
expect(result.enabled).toBe(false);
|
|
310
|
+
expect(result.reason).toContain('not found in registry');
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('isSurfaceEnabled returns true for core surfaces even with false override', async () => {
|
|
314
|
+
const { isSurfaceEnabled } = await import('../../src/core/surface-guard.js');
|
|
315
|
+
const result = isSurfaceEnabled('hook:before_prompt_build', { 'hook:before_prompt_build': false });
|
|
316
|
+
expect(result.enabled).toBe(true);
|
|
317
|
+
expect(result.reason).toContain('core');
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('isSurfaceEnabled returns false for quiet surfaces by default', async () => {
|
|
321
|
+
const { isSurfaceEnabled } = await import('../../src/core/surface-guard.js');
|
|
322
|
+
const result = isSurfaceEnabled('hook:after_tool_call.trajectory');
|
|
323
|
+
expect(result.enabled).toBe(false);
|
|
324
|
+
expect(result.reason).toBeDefined();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('isSurfaceEnabled allows quiet surfaces with explicit override', async () => {
|
|
328
|
+
const { isSurfaceEnabled } = await import('../../src/core/surface-guard.js');
|
|
329
|
+
const result = isSurfaceEnabled('hook:after_tool_call.trajectory', { 'hook:after_tool_call.trajectory': true });
|
|
330
|
+
expect(result.enabled).toBe(true);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('guardHook returns original handler for core surfaces', async () => {
|
|
334
|
+
const { guardHook } = await import('../../src/core/surface-guard.js');
|
|
335
|
+
const handler = () => 'result';
|
|
336
|
+
const guarded = guardHook('hook:before_prompt_build', undefined, handler);
|
|
337
|
+
expect(guarded).toBe(handler);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('guardHook returns no-op handler for quiet surfaces', async () => {
|
|
341
|
+
const { guardHook } = await import('../../src/core/surface-guard.js');
|
|
342
|
+
const handler = () => 'result';
|
|
343
|
+
const guarded = guardHook('hook:after_tool_call.trajectory', undefined, handler);
|
|
344
|
+
expect(guarded).not.toBe(handler);
|
|
345
|
+
expect(guarded({} as never, {} as never)).toBeUndefined();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('guardHook returns no-op handler for unregistered surfaces', async () => {
|
|
349
|
+
const { guardHook } = await import('../../src/core/surface-guard.js');
|
|
350
|
+
const handler = () => 'result';
|
|
351
|
+
const guarded = guardHook('hook:nonexistent_hook', undefined, handler);
|
|
352
|
+
expect(guarded).not.toBe(handler);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('guardHook logs disabled reason via logger for quiet surfaces', async () => {
|
|
356
|
+
const { guardHook } = await import('../../src/core/surface-guard.js');
|
|
357
|
+
const logs: string[] = [];
|
|
358
|
+
const logger = { info: (msg: string) => { logs.push(msg); } };
|
|
359
|
+
const handler = () => 'result';
|
|
360
|
+
const guarded = guardHook('hook:after_tool_call.trajectory', logger, handler);
|
|
361
|
+
guarded({} as never, {} as never);
|
|
362
|
+
expect(logs.length).toBe(1);
|
|
363
|
+
expect(logs[0]).toContain('[PD:surface-guard] SKIP');
|
|
364
|
+
expect(logs[0]).toContain('hook:after_tool_call.trajectory');
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('guardHook does not log for enabled surfaces', async () => {
|
|
368
|
+
const { guardHook } = await import('../../src/core/surface-guard.js');
|
|
369
|
+
const logs: string[] = [];
|
|
370
|
+
const logger = { info: (msg: string) => { logs.push(msg); } };
|
|
371
|
+
const handler = () => 'result';
|
|
372
|
+
const guarded = guardHook('hook:before_prompt_build', logger, handler);
|
|
373
|
+
guarded({} as never, {} as never);
|
|
374
|
+
expect(logs.length).toBe(0);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('guardService returns null for evolution-worker (quiet, default off)', async () => {
|
|
378
|
+
const { guardService } = await import('../../src/core/surface-guard.js');
|
|
379
|
+
const service = { api: null, start: () => {} };
|
|
380
|
+
const guarded = guardService('service:evolution-worker', service);
|
|
381
|
+
expect(guarded).toBeNull();
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('guardService returns null for quiet surfaces', async () => {
|
|
385
|
+
const { guardService } = await import('../../src/core/surface-guard.js');
|
|
386
|
+
const service = { api: null, start: () => {} };
|
|
387
|
+
const guarded = guardService('service:trajectory', service);
|
|
388
|
+
expect(guarded).toBeNull();
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('guardService returns null for unregistered surfaces', async () => {
|
|
392
|
+
const { guardService } = await import('../../src/core/surface-guard.js');
|
|
393
|
+
const service = { api: null, start: () => {} };
|
|
394
|
+
const guarded = guardService('service:nonexistent_service', service);
|
|
395
|
+
expect(guarded).toBeNull();
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
});
|