@animalabs/connectome-host 0.8.0 → 0.8.1

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.
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Recipe surface for tune-out's subconscious resident (agent-framework#77):
3
+ * schema validation of the `subconscious` block. The block passes through to
4
+ * FrameworkConfig.subconscious verbatim, so validation is the host's whole
5
+ * contribution — a typo'd key or a missing mode block must fail at recipe
6
+ * load, not surface as a subconscious running on an empty system prompt.
7
+ */
8
+ import { describe, test, expect } from 'bun:test';
9
+ import { validateRecipe } from '../src/recipe.js';
10
+
11
+ function baseRecipe(extra: Record<string, unknown> = {}): Record<string, unknown> {
12
+ return {
13
+ name: 'Test',
14
+ agent: { name: 'sherlock', systemPrompt: 'test' },
15
+ ...extra,
16
+ };
17
+ }
18
+
19
+ const MODE = 'You are the Subconscious. Watch tuned-out channels and report in second person.';
20
+
21
+ describe('validateRecipe — subconscious schema', () => {
22
+ test('absent block is accepted', () => {
23
+ expect(() => validateRecipe(baseRecipe())).not.toThrow();
24
+ expect(validateRecipe(baseRecipe()).subconscious).toBeUndefined();
25
+ });
26
+
27
+ test('full valid block is accepted and passes through verbatim', () => {
28
+ const block = {
29
+ enabled: true,
30
+ name: 'Subconscious',
31
+ model: 'claude-sonnet-4-5',
32
+ systemPrompt: MODE,
33
+ allowChannelSpeech: false,
34
+ reAnchorFraction: 0.5,
35
+ };
36
+ const recipe = validateRecipe(baseRecipe({ subconscious: block }));
37
+ expect(recipe.subconscious).toEqual(block);
38
+ });
39
+
40
+ test('minimal block: enabled + systemPrompt', () => {
41
+ expect(() => validateRecipe(baseRecipe({ subconscious: { enabled: true, systemPrompt: MODE } }))).not.toThrow();
42
+ expect(() => validateRecipe(baseRecipe({ subconscious: { enabled: false, systemPrompt: MODE } }))).not.toThrow();
43
+ });
44
+
45
+ test('non-object block is refused', () => {
46
+ expect(() => validateRecipe(baseRecipe({ subconscious: true }))).toThrow(/subconscious must be an object/);
47
+ expect(() => validateRecipe(baseRecipe({ subconscious: [] }))).toThrow(/subconscious must be an object/);
48
+ });
49
+
50
+ test('enabled must be a boolean', () => {
51
+ expect(() => validateRecipe(baseRecipe({ subconscious: { enabled: 'yes', systemPrompt: MODE } })))
52
+ .toThrow(/subconscious\.enabled must be a boolean/);
53
+ expect(() => validateRecipe(baseRecipe({ subconscious: { systemPrompt: MODE } })))
54
+ .toThrow(/subconscious\.enabled must be a boolean/);
55
+ });
56
+
57
+ test('systemPrompt is required and non-empty — the mode block is the whole character', () => {
58
+ expect(() => validateRecipe(baseRecipe({ subconscious: { enabled: true } })))
59
+ .toThrow(/subconscious\.systemPrompt must be a non-empty string/);
60
+ expect(() => validateRecipe(baseRecipe({ subconscious: { enabled: true, systemPrompt: ' ' } })))
61
+ .toThrow(/subconscious\.systemPrompt must be a non-empty string/);
62
+ });
63
+
64
+ test('unknown fields are refused by name', () => {
65
+ expect(() => validateRecipe(baseRecipe({ subconscious: { enabled: true, systemPrompt: MODE, cadence: 30 } })))
66
+ .toThrow(/unknown field "cadence"/);
67
+ });
68
+
69
+ test('name and model must be non-empty strings when present', () => {
70
+ expect(() => validateRecipe(baseRecipe({ subconscious: { enabled: true, systemPrompt: MODE, name: '' } })))
71
+ .toThrow(/subconscious\.name must be a non-empty string/);
72
+ expect(() => validateRecipe(baseRecipe({ subconscious: { enabled: true, systemPrompt: MODE, model: 7 } })))
73
+ .toThrow(/subconscious\.model must be a non-empty string/);
74
+ });
75
+
76
+ test('allowChannelSpeech must be a boolean; reAnchorFraction must be in (0, 1]', () => {
77
+ expect(() => validateRecipe(baseRecipe({ subconscious: { enabled: true, systemPrompt: MODE, allowChannelSpeech: 'no' } })))
78
+ .toThrow(/allowChannelSpeech must be a boolean/);
79
+ for (const bad of [0, 1.5, -0.2, 'half']) {
80
+ expect(() => validateRecipe(baseRecipe({ subconscious: { enabled: true, systemPrompt: MODE, reAnchorFraction: bad } })))
81
+ .toThrow(/reAnchorFraction must be a number in \(0, 1\]/);
82
+ }
83
+ expect(() => validateRecipe(baseRecipe({ subconscious: { enabled: true, systemPrompt: MODE, reAnchorFraction: 1 } })))
84
+ .not.toThrow();
85
+ });
86
+ });
@@ -0,0 +1,37 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { buildFrameworkAgentConfig } from '../src/framework-agent-config.js';
3
+ import { validateRecipe } from '../src/recipe.js';
4
+
5
+ const base = (value?: unknown) => ({
6
+ name: 'tool-wrapper-prose-guard',
7
+ agent: {
8
+ systemPrompt: 'sys',
9
+ ...(value === undefined ? {} : { toolWrapperProseGuard: value }),
10
+ },
11
+ });
12
+
13
+ describe('toolWrapperProseGuard recipe wiring', () => {
14
+ test('is default-off by omission and composes true into AgentConfig', () => {
15
+ const absent = validateRecipe(base());
16
+ expect(absent.agent.toolWrapperProseGuard).toBeUndefined();
17
+ expect(buildFrameworkAgentConfig(absent, 'a', 'm', { kind: 's' } as never).toolWrapperProseGuard).toBeUndefined();
18
+
19
+ const enabled = validateRecipe(base(true));
20
+ expect(enabled.agent.toolWrapperProseGuard).toBe(true);
21
+ expect(buildFrameworkAgentConfig(enabled, 'a', 'm', { kind: 's' } as never).toolWrapperProseGuard).toBe(true);
22
+
23
+ const disabled = validateRecipe(base(false));
24
+ expect(disabled.agent.toolWrapperProseGuard).toBe(false);
25
+ expect(buildFrameworkAgentConfig(disabled, 'b', 'm', { kind: 's' } as never).toolWrapperProseGuard).toBe(false);
26
+
27
+ const reloaded = validateRecipe(JSON.parse(JSON.stringify(enabled)));
28
+ expect(buildFrameworkAgentConfig(reloaded, 'c', 'm', { kind: 's' } as never).toolWrapperProseGuard).toBe(true);
29
+ expect(buildFrameworkAgentConfig(absent, 'd', 'm', { kind: 's' } as never).toolWrapperProseGuard).toBeUndefined();
30
+ });
31
+
32
+ test('rejects non-boolean values instead of silently disabling the guard', () => {
33
+ for (const value of ['true', 1, null, {}]) {
34
+ expect(() => validateRecipe(base(value))).toThrow(/toolWrapperProseGuard/);
35
+ }
36
+ });
37
+ });
@@ -330,6 +330,47 @@ describe('WebUiModule HTTP', () => {
330
330
  const body = res.status < 500 ? await res.text() : '';
331
331
  expect(body).not.toContain('pwned');
332
332
  });
333
+
334
+ // Honest-error regression set: unknown API routes and missing bundle
335
+ // assets used to fall through to the SPA shell with a 200, and wrong
336
+ // methods executed the GET handlers.
337
+
338
+ test('unknown /debug/* route returns a JSON 404, not the SPA shell', async () => {
339
+ for (const path of ['/debug/context/', '/debug/CONTEXT', '/debug/nonexistent']) {
340
+ const res = await fetch(`http://127.0.0.1:${handle.port}${path}`, {
341
+ headers: { authorization: basicAuthHeader(BASIC_USER, BASIC_PASS) },
342
+ });
343
+ expect(res.status).toBe(404);
344
+ expect(res.headers.get('content-type')).toContain('application/json');
345
+ }
346
+ });
347
+
348
+ test('missing /assets/* returns 404, not HTML', async () => {
349
+ const res = await fetch(`http://127.0.0.1:${handle.port}/assets/nonexistent.js`, {
350
+ headers: { authorization: basicAuthHeader(BASIC_USER, BASIC_PASS) },
351
+ });
352
+ expect(res.status).toBe(404);
353
+ expect(res.headers.get('content-type') ?? '').not.toContain('text/html');
354
+ });
355
+
356
+ test('non-GET methods get 405 with Allow header', async () => {
357
+ for (const method of ['POST', 'DELETE', 'PUT']) {
358
+ const res = await fetch(`http://127.0.0.1:${handle.port}/debug/context`, {
359
+ method,
360
+ headers: { authorization: basicAuthHeader(BASIC_USER, BASIC_PASS) },
361
+ });
362
+ expect(res.status).toBe(405);
363
+ expect(res.headers.get('allow')).toContain('GET');
364
+ }
365
+ });
366
+
367
+ test('SPA client-side routes still fall back to the shell', async () => {
368
+ const res = await fetch(`http://127.0.0.1:${handle.port}/totally/fake/route`, {
369
+ headers: { authorization: basicAuthHeader(BASIC_USER, BASIC_PASS) },
370
+ });
371
+ expect(res.status).toBe(200);
372
+ expect(await res.text()).toContain('<!doctype html>');
373
+ });
333
374
  });
334
375
 
335
376
  describe('WebUiModule WebSocket', () => {
package/web/src/App.tsx CHANGED
@@ -1058,6 +1058,15 @@ export function App() {
1058
1058
  if (!text) return;
1059
1059
  setDraft('');
1060
1060
  if (text.startsWith('/')) {
1061
+ // /clear is display-local (the TUI clears its scrollback the same
1062
+ // way): wipe this client's transcript view without touching the
1063
+ // Chronicle or other clients. Sending it to the host was a no-op
1064
+ // that APPENDED a "(cleared)" line — the opposite of clearing.
1065
+ if (text === '/clear' || text.startsWith('/clear ')) {
1066
+ setMessages(produce((arr) => { arr.length = 0; }));
1067
+ setStreamLines([]);
1068
+ return;
1069
+ }
1061
1070
  wire.send({ type: 'command', command: text });
1062
1071
  return;
1063
1072
  }