@treenity/mods 3.0.1 → 3.0.2
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/board/view.tsx +1 -1
- package/brahman/helpers.ts +7 -7
- package/brahman/service.ts +24 -24
- package/brahman/types.ts +21 -21
- package/brahman/views/action-cards.tsx +33 -23
- package/brahman/views/bot-view.tsx +3 -2
- package/brahman/views/chat-editor.tsx +119 -124
- package/brahman/views/menu-editor.tsx +75 -89
- package/brahman/views/page-layout.tsx +10 -8
- package/brahman/views/tstring-input.tsx +25 -15
- package/canary/service.ts +18 -18
- package/dist/board/view.js +1 -1
- package/dist/board/view.js.map +1 -1
- package/dist/brahman/helpers.d.ts +1 -1
- package/dist/brahman/helpers.d.ts.map +1 -1
- package/dist/brahman/helpers.js +6 -6
- package/dist/brahman/helpers.js.map +1 -1
- package/dist/brahman/service.js +24 -24
- package/dist/brahman/service.js.map +1 -1
- package/dist/brahman/types.d.ts +1 -1
- package/dist/brahman/types.d.ts.map +1 -1
- package/dist/brahman/types.js +21 -21
- package/dist/brahman/types.js.map +1 -1
- package/dist/brahman/views/action-cards.d.ts.map +1 -1
- package/dist/brahman/views/action-cards.js +7 -4
- package/dist/brahman/views/action-cards.js.map +1 -1
- package/dist/brahman/views/bot-view.d.ts.map +1 -1
- package/dist/brahman/views/bot-view.js +2 -1
- package/dist/brahman/views/bot-view.js.map +1 -1
- package/dist/brahman/views/chat-editor.d.ts.map +1 -1
- package/dist/brahman/views/chat-editor.js +27 -18
- package/dist/brahman/views/chat-editor.js.map +1 -1
- package/dist/brahman/views/menu-editor.d.ts.map +1 -1
- package/dist/brahman/views/menu-editor.js +12 -16
- package/dist/brahman/views/menu-editor.js.map +1 -1
- package/dist/brahman/views/page-layout.d.ts.map +1 -1
- package/dist/brahman/views/page-layout.js +1 -1
- package/dist/brahman/views/page-layout.js.map +1 -1
- package/dist/brahman/views/tstring-input.d.ts.map +1 -1
- package/dist/brahman/views/tstring-input.js +7 -3
- package/dist/brahman/views/tstring-input.js.map +1 -1
- package/dist/canary/service.js +18 -18
- package/dist/canary/service.js.map +1 -1
- package/dist/doc/fs-codec.js +1 -1
- package/dist/doc/fs-codec.js.map +1 -1
- package/dist/doc/renderers.d.ts.map +1 -1
- package/dist/doc/renderers.js +2 -1
- package/dist/doc/renderers.js.map +1 -1
- package/dist/doc/toolbar.d.ts.map +1 -1
- package/dist/doc/toolbar.js +5 -5
- package/dist/doc/toolbar.js.map +1 -1
- package/dist/launcher/types.js +2 -2
- package/dist/launcher/types.js.map +1 -1
- package/dist/launcher/view.js +2 -2
- package/dist/launcher/view.js.map +1 -1
- package/dist/mindmap/branch.d.ts +10 -0
- package/dist/mindmap/branch.d.ts.map +1 -1
- package/dist/mindmap/branch.js +42 -9
- package/dist/mindmap/branch.js.map +1 -1
- package/dist/mindmap/sidebar.d.ts.map +1 -1
- package/dist/mindmap/sidebar.js +4 -3
- package/dist/mindmap/sidebar.js.map +1 -1
- package/dist/mindmap/view.d.ts.map +1 -1
- package/dist/mindmap/view.js +35 -4
- package/dist/mindmap/view.js.map +1 -1
- package/dist/sensor-demo/service.js +6 -5
- package/dist/sensor-demo/service.js.map +1 -1
- package/dist/sensor-generator/action.js +1 -1
- package/dist/sensor-generator/action.js.map +1 -1
- package/dist/sim/service.js +41 -41
- package/dist/sim/service.js.map +1 -1
- package/dist/table/view.js.map +1 -1
- package/dist/todo/types.js +2 -2
- package/dist/todo/types.js.map +1 -1
- package/dist/todo/view.js +6 -4
- package/dist/todo/view.js.map +1 -1
- package/dist/whisper/inbox.js +3 -3
- package/dist/whisper/inbox.js.map +1 -1
- package/dist/whisper/route.d.ts +1 -1
- package/dist/whisper/route.d.ts.map +1 -1
- package/dist/whisper/route.js +13 -13
- package/dist/whisper/route.js.map +1 -1
- package/doc/CLAUDE.md +1 -1
- package/doc/fs-codec.ts +1 -1
- package/doc/renderers.tsx +4 -3
- package/doc/toolbar.tsx +12 -9
- package/launcher/types.ts +2 -2
- package/launcher/view.tsx +12 -8
- package/mindmap/branch.tsx +121 -22
- package/mindmap/mindmap.css +52 -0
- package/mindmap/sidebar.tsx +9 -6
- package/mindmap/view.tsx +40 -4
- package/package.json +27 -3
- package/sensor-demo/service.ts +6 -5
- package/sensor-generator/action.ts +1 -1
- package/sim/service.ts +41 -41
- package/table/view.tsx +7 -2
- package/todo/types.ts +2 -2
- package/todo/view.tsx +9 -10
- package/whisper/inbox.ts +3 -3
- package/whisper/route.ts +13 -13
- package/board/board.test.ts +0 -212
- package/brahman/brahman.test.ts +0 -855
- package/doc/fs-codec.test.ts +0 -119
- package/doc/markdown.test.ts +0 -152
- package/sim/sim.test.ts +0 -282
package/doc/fs-codec.test.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import type { Tree } from '@treenity/core/tree';
|
|
2
|
-
import { createFsTree } from '@treenity/core/tree/fs';
|
|
3
|
-
import assert from 'node:assert/strict';
|
|
4
|
-
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
5
|
-
import { tmpdir } from 'node:os';
|
|
6
|
-
import { join } from 'node:path';
|
|
7
|
-
import { afterEach, beforeEach, describe, it } from 'node:test';
|
|
8
|
-
|
|
9
|
-
// Register the codec — side effect import
|
|
10
|
-
import './fs-codec';
|
|
11
|
-
|
|
12
|
-
describe('doc fs-codec', () => {
|
|
13
|
-
let dir: string;
|
|
14
|
-
let store: Tree;
|
|
15
|
-
|
|
16
|
-
beforeEach(async () => {
|
|
17
|
-
dir = await mkdtemp(join(tmpdir(), 'treenity-doc-codec-'));
|
|
18
|
-
store = await createFsTree(dir);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
afterEach(async () => {
|
|
22
|
-
if (dir) await rm(dir, { recursive: true, force: true });
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('decode: .md file → doc.page node', async () => {
|
|
26
|
-
await writeFile(join(dir, 'readme.md'), '# Hello World\n\nSome content here.\n');
|
|
27
|
-
|
|
28
|
-
const node = await store.get('/readme');
|
|
29
|
-
assert.equal(node?.$type, 'doc.page');
|
|
30
|
-
assert.equal((node as any).title, 'Hello World');
|
|
31
|
-
|
|
32
|
-
const content = JSON.parse((node as any).content);
|
|
33
|
-
assert.equal(content.type, 'doc');
|
|
34
|
-
assert.ok(content.content.length > 0);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('decode: .md without H1 — empty title', async () => {
|
|
38
|
-
await writeFile(join(dir, 'notes.md'), 'Just a paragraph.\n\n## Second heading\n');
|
|
39
|
-
|
|
40
|
-
const node = await store.get('/notes');
|
|
41
|
-
assert.equal(node?.$type, 'doc.page');
|
|
42
|
-
assert.equal((node as any).title, '');
|
|
43
|
-
|
|
44
|
-
const content = JSON.parse((node as any).content);
|
|
45
|
-
assert.equal(content.content[0].type, 'paragraph');
|
|
46
|
-
assert.equal(content.content[1].type, 'heading');
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('encode: doc.page node → .md file', async () => {
|
|
50
|
-
const tiptapDoc = {
|
|
51
|
-
type: 'doc',
|
|
52
|
-
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Hello from Tiptap' }] }],
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
await store.set({
|
|
56
|
-
$path: '/output',
|
|
57
|
-
$type: 'doc.page',
|
|
58
|
-
title: 'My Doc',
|
|
59
|
-
content: JSON.stringify(tiptapDoc),
|
|
60
|
-
} as any);
|
|
61
|
-
|
|
62
|
-
const raw = await readFile(join(dir, 'output.md'), 'utf-8');
|
|
63
|
-
assert.ok(raw.startsWith('# My Doc'));
|
|
64
|
-
assert.ok(raw.includes('Hello from Tiptap'));
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('encode: no title — no H1 prefix', async () => {
|
|
68
|
-
const tiptapDoc = {
|
|
69
|
-
type: 'doc',
|
|
70
|
-
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Just text' }] }],
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
await store.set({
|
|
74
|
-
$path: '/bare',
|
|
75
|
-
$type: 'doc.page',
|
|
76
|
-
title: '',
|
|
77
|
-
content: JSON.stringify(tiptapDoc),
|
|
78
|
-
} as any);
|
|
79
|
-
|
|
80
|
-
const raw = await readFile(join(dir, 'bare.md'), 'utf-8');
|
|
81
|
-
assert.ok(!raw.startsWith('#'));
|
|
82
|
-
assert.ok(raw.includes('Just text'));
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('roundtrip: .md → node → .md preserves content', async () => {
|
|
86
|
-
const original = '# Project Notes\n\nThis is **bold** and *italic*.\n\n- item one\n- item two\n\n```ts\nconst x = 1;\n```\n';
|
|
87
|
-
await writeFile(join(dir, 'notes.md'), original);
|
|
88
|
-
|
|
89
|
-
// Decode
|
|
90
|
-
const node = await store.get('/notes');
|
|
91
|
-
assert.equal(node?.$type, 'doc.page');
|
|
92
|
-
assert.equal((node as any).title, 'Project Notes');
|
|
93
|
-
|
|
94
|
-
// Encode back
|
|
95
|
-
await store.set(node!);
|
|
96
|
-
const result = await readFile(join(dir, 'notes.md'), 'utf-8');
|
|
97
|
-
|
|
98
|
-
// Verify key elements survived roundtrip
|
|
99
|
-
assert.ok(result.includes('# Project Notes'));
|
|
100
|
-
assert.ok(result.includes('**bold**'));
|
|
101
|
-
assert.ok(result.includes('*italic*'));
|
|
102
|
-
assert.ok(result.includes('- item one'));
|
|
103
|
-
assert.ok(result.includes('```ts'));
|
|
104
|
-
assert.ok(result.includes('const x = 1;'));
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('getChildren lists .md files as doc.page', async () => {
|
|
108
|
-
await writeFile(join(dir, 'a.md'), '# Alpha\n\nContent A\n');
|
|
109
|
-
await writeFile(join(dir, 'b.md'), '# Beta\n\nContent B\n');
|
|
110
|
-
await writeFile(join(dir, 'c.txt'), 'plain text');
|
|
111
|
-
|
|
112
|
-
const { items } = await store.getChildren('/');
|
|
113
|
-
const types = Object.fromEntries(items.map(n => [n.$path, n.$type]));
|
|
114
|
-
|
|
115
|
-
assert.equal(types['/a'], 'doc.page');
|
|
116
|
-
assert.equal(types['/b'], 'doc.page');
|
|
117
|
-
assert.equal(types['/c'], 'text/plain');
|
|
118
|
-
});
|
|
119
|
-
});
|
package/doc/markdown.test.ts
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { describe, it } from 'node:test';
|
|
3
|
-
import { mdToTiptap, type TiptapNode, tiptapToMd } from './markdown';
|
|
4
|
-
|
|
5
|
-
describe('mdToTiptap', () => {
|
|
6
|
-
it('parses heading', () => {
|
|
7
|
-
const doc = mdToTiptap('# Hello');
|
|
8
|
-
assert.equal(doc.type, 'doc');
|
|
9
|
-
assert.equal(doc.content?.[0].type, 'heading');
|
|
10
|
-
assert.equal(doc.content?.[0].attrs?.level, 1);
|
|
11
|
-
assert.equal(doc.content?.[0].content?.[0].text, 'Hello');
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it('parses multiple heading levels', () => {
|
|
15
|
-
const doc = mdToTiptap('## Second\n### Third');
|
|
16
|
-
assert.equal(doc.content?.[0].attrs?.level, 2);
|
|
17
|
-
assert.equal(doc.content?.[1].attrs?.level, 3);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('parses paragraph', () => {
|
|
21
|
-
const doc = mdToTiptap('Just some text');
|
|
22
|
-
assert.equal(doc.content?.[0].type, 'paragraph');
|
|
23
|
-
assert.equal(doc.content?.[0].content?.[0].text, 'Just some text');
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('parses bold and italic', () => {
|
|
27
|
-
const doc = mdToTiptap('This is **bold** and *italic*');
|
|
28
|
-
const nodes = doc.content?.[0].content ?? [];
|
|
29
|
-
assert.ok(nodes.some((n) => n.text === 'bold' && n.marks?.[0]?.type === 'bold'));
|
|
30
|
-
assert.ok(nodes.some((n) => n.text === 'italic' && n.marks?.[0]?.type === 'italic'));
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('parses inline code', () => {
|
|
34
|
-
const doc = mdToTiptap('Use `foo()` here');
|
|
35
|
-
const nodes = doc.content?.[0].content ?? [];
|
|
36
|
-
assert.ok(nodes.some((n) => n.text === 'foo()' && n.marks?.[0]?.type === 'code'));
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('parses bullet list', () => {
|
|
40
|
-
const doc = mdToTiptap('- one\n- two\n- three');
|
|
41
|
-
assert.equal(doc.content?.[0].type, 'bulletList');
|
|
42
|
-
assert.equal(doc.content?.[0].content?.length, 3);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('parses ordered list', () => {
|
|
46
|
-
const doc = mdToTiptap('1. first\n2. second');
|
|
47
|
-
assert.equal(doc.content?.[0].type, 'orderedList');
|
|
48
|
-
assert.equal(doc.content?.[0].content?.length, 2);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('parses code block', () => {
|
|
52
|
-
const doc = mdToTiptap('```ts\nconst x = 1;\n```');
|
|
53
|
-
assert.equal(doc.content?.[0].type, 'codeBlock');
|
|
54
|
-
assert.equal(doc.content?.[0].attrs?.language, 'ts');
|
|
55
|
-
assert.equal(doc.content?.[0].content?.[0].text, 'const x = 1;');
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('parses blockquote', () => {
|
|
59
|
-
const doc = mdToTiptap('> quoted text');
|
|
60
|
-
assert.equal(doc.content?.[0].type, 'blockquote');
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('parses horizontal rule', () => {
|
|
64
|
-
const doc = mdToTiptap('---');
|
|
65
|
-
assert.equal(doc.content?.[0].type, 'horizontalRule');
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('handles empty input', () => {
|
|
69
|
-
const doc = mdToTiptap('');
|
|
70
|
-
assert.equal(doc.type, 'doc');
|
|
71
|
-
assert.ok(doc.content?.length);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('handles mixed content', () => {
|
|
75
|
-
const md = '# Title\n\nSome text\n\n- item 1\n- item 2\n\n```\ncode\n```';
|
|
76
|
-
const doc = mdToTiptap(md);
|
|
77
|
-
const types = doc.content?.map((c) => c.type);
|
|
78
|
-
assert.deepEqual(types, ['heading', 'paragraph', 'bulletList', 'codeBlock']);
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
describe('tiptapToMd', () => {
|
|
83
|
-
it('converts heading', () => {
|
|
84
|
-
const doc: TiptapNode = {
|
|
85
|
-
type: 'doc',
|
|
86
|
-
content: [{ type: 'heading', attrs: { level: 2 }, content: [{ type: 'text', text: 'Title' }] }],
|
|
87
|
-
};
|
|
88
|
-
assert.equal(tiptapToMd(doc), '## Title');
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('converts paragraph with marks', () => {
|
|
92
|
-
const doc: TiptapNode = {
|
|
93
|
-
type: 'doc',
|
|
94
|
-
content: [{
|
|
95
|
-
type: 'paragraph',
|
|
96
|
-
content: [
|
|
97
|
-
{ type: 'text', text: 'Hello ' },
|
|
98
|
-
{ type: 'text', text: 'world', marks: [{ type: 'bold' }] },
|
|
99
|
-
],
|
|
100
|
-
}],
|
|
101
|
-
};
|
|
102
|
-
assert.equal(tiptapToMd(doc), 'Hello **world**');
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('converts bullet list', () => {
|
|
106
|
-
const doc: TiptapNode = {
|
|
107
|
-
type: 'doc',
|
|
108
|
-
content: [{
|
|
109
|
-
type: 'bulletList',
|
|
110
|
-
content: [
|
|
111
|
-
{ type: 'listItem', content: [{ type: 'paragraph', content: [{ type: 'text', text: 'a' }] }] },
|
|
112
|
-
{ type: 'listItem', content: [{ type: 'paragraph', content: [{ type: 'text', text: 'b' }] }] },
|
|
113
|
-
],
|
|
114
|
-
}],
|
|
115
|
-
};
|
|
116
|
-
assert.equal(tiptapToMd(doc), '- a\n- b');
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('converts code block', () => {
|
|
120
|
-
const doc: TiptapNode = {
|
|
121
|
-
type: 'doc',
|
|
122
|
-
content: [{ type: 'codeBlock', attrs: { language: 'js' }, content: [{ type: 'text', text: 'x = 1' }] }],
|
|
123
|
-
};
|
|
124
|
-
assert.equal(tiptapToMd(doc), '```js\nx = 1\n```');
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('converts horizontal rule', () => {
|
|
128
|
-
const doc: TiptapNode = { type: 'doc', content: [{ type: 'horizontalRule' }] };
|
|
129
|
-
assert.equal(tiptapToMd(doc), '---');
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
describe('roundtrip md → tiptap → md', () => {
|
|
134
|
-
it('preserves basic structure', () => {
|
|
135
|
-
const original = '# Hello\n\nSome paragraph\n\n- one\n- two';
|
|
136
|
-
const tiptap = mdToTiptap(original);
|
|
137
|
-
const result = tiptapToMd(tiptap);
|
|
138
|
-
// Should contain the same elements (whitespace may differ)
|
|
139
|
-
assert.ok(result.includes('# Hello'));
|
|
140
|
-
assert.ok(result.includes('Some paragraph'));
|
|
141
|
-
assert.ok(result.includes('- one'));
|
|
142
|
-
assert.ok(result.includes('- two'));
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('preserves code block', () => {
|
|
146
|
-
const original = '```ts\nconst x = 1;\n```';
|
|
147
|
-
const tiptap = mdToTiptap(original);
|
|
148
|
-
const result = tiptapToMd(tiptap);
|
|
149
|
-
assert.ok(result.includes('```ts'));
|
|
150
|
-
assert.ok(result.includes('const x = 1;'));
|
|
151
|
-
});
|
|
152
|
-
});
|
package/sim/sim.test.ts
DELETED
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
// AgentSim tests — round engine, proximity, tools, quorum
|
|
2
|
-
|
|
3
|
-
import { createNode, getComponent, resolve } from '@treenity/core';
|
|
4
|
-
import type { ServiceHandle } from '@treenity/core/contexts/service';
|
|
5
|
-
import { createMemoryTree, type Tree } from '@treenity/core/tree';
|
|
6
|
-
import assert from 'node:assert/strict';
|
|
7
|
-
import { beforeEach, describe, it } from 'node:test';
|
|
8
|
-
import './service'; // registers handlers once (ESM cache)
|
|
9
|
-
|
|
10
|
-
let store: Tree;
|
|
11
|
-
|
|
12
|
-
function agent(path: string, name: string, icon: string, x: number, y: number, radius = 200) {
|
|
13
|
-
return createNode(path, 'sim.agent', {}, {
|
|
14
|
-
descriptive: { $type: 'sim.descriptive', name, icon, description: `${name} agent` },
|
|
15
|
-
ai: { $type: 'sim.ai', systemPrompt: `You are ${name}.` },
|
|
16
|
-
position: { $type: 'sim.position', x, y, radius },
|
|
17
|
-
memory: { $type: 'sim.memory', entries: [] },
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function world(path = '/w', running = false) {
|
|
22
|
-
return createNode(path, 'sim.world', {}, {
|
|
23
|
-
config: { $type: 'sim.config', width: 600, height: 400, roundDelay: 100, running },
|
|
24
|
-
round: { $type: 'sim.round', current: 0, phase: 'idle', log: [] },
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
beforeEach(() => {
|
|
29
|
-
store = createMemoryTree();
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
describe('sim.world registration', () => {
|
|
33
|
-
it('registers service handler', () => {
|
|
34
|
-
assert.ok(resolve('sim.world', 'service'));
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('registers start/stop actions', () => {
|
|
38
|
-
assert.ok(resolve('sim.world', 'action:start'));
|
|
39
|
-
assert.ok(resolve('sim.world', 'action:stop'));
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
describe('start/stop actions', () => {
|
|
44
|
-
it('action:start sets running=true', async () => {
|
|
45
|
-
const w = world();
|
|
46
|
-
await store.set(w);
|
|
47
|
-
const handler = resolve('sim.world', 'action:start')!;
|
|
48
|
-
await handler({ node: w, store, signal: AbortSignal.timeout(5000) }, {});
|
|
49
|
-
// Handlers mutate ctx.node in-place; caller persists (executeAction or service loop)
|
|
50
|
-
await store.set(w);
|
|
51
|
-
const fresh = await store.get(w.$path);
|
|
52
|
-
assert.equal((getComponent(fresh!, 'config') as any).running, true);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('action:stop sets running=false', async () => {
|
|
56
|
-
const w = world('/w', true);
|
|
57
|
-
await store.set(w);
|
|
58
|
-
const handler = resolve('sim.world', 'action:stop')!;
|
|
59
|
-
await handler({ node: w, store, signal: AbortSignal.timeout(5000) }, {});
|
|
60
|
-
await store.set(w);
|
|
61
|
-
const fresh = await store.get(w.$path);
|
|
62
|
-
assert.equal((getComponent(fresh!, 'config') as any).running, false);
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
describe('proximity', () => {
|
|
67
|
-
it('agents within radius are nearby', async () => {
|
|
68
|
-
await store.set(world('/w', true));
|
|
69
|
-
// Large radius covers entire map — stays nearby even after random moves
|
|
70
|
-
await store.set(agent('/w/a', 'Alice', 'A', 100, 100, 900));
|
|
71
|
-
await store.set(agent('/w/b', 'Bob', 'B', 200, 200, 900));
|
|
72
|
-
|
|
73
|
-
const svc = resolve('sim.world', 'service')!;
|
|
74
|
-
const handle: ServiceHandle = await svc((await store.get('/w'))!, { store, subscribe: () => () => {} });
|
|
75
|
-
await new Promise((r) => setTimeout(r, 800));
|
|
76
|
-
await handle.stop();
|
|
77
|
-
|
|
78
|
-
const a = await store.get('/w/a');
|
|
79
|
-
const nearby = getComponent(a!, 'nearby') as any;
|
|
80
|
-
assert.ok(nearby);
|
|
81
|
-
assert.ok(nearby.agents.includes('Bob'));
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('agents outside radius are not nearby', async () => {
|
|
85
|
-
await store.set(world('/w', true));
|
|
86
|
-
await store.set(agent('/w/a', 'Alice', 'A', 0, 0, 50));
|
|
87
|
-
await store.set(agent('/w/b', 'Bob', 'B', 500, 500, 50)); // dist ~707 >> 50
|
|
88
|
-
|
|
89
|
-
const svc = resolve('sim.world', 'service')!;
|
|
90
|
-
const handle: ServiceHandle = await svc((await store.get('/w'))!, { store, subscribe: () => () => {} });
|
|
91
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
92
|
-
await handle.stop();
|
|
93
|
-
|
|
94
|
-
const a = await store.get('/w/a');
|
|
95
|
-
const nearby = getComponent(a!, 'nearby') as any;
|
|
96
|
-
assert.ok(nearby);
|
|
97
|
-
assert.equal(nearby.agents.length, 0);
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
describe('round engine', () => {
|
|
102
|
-
it('advances round number after execution', async () => {
|
|
103
|
-
await store.set(world('/w', true));
|
|
104
|
-
await store.set(agent('/w/a', 'Alice', 'A', 100, 100));
|
|
105
|
-
|
|
106
|
-
const svc = resolve('sim.world', 'service')!;
|
|
107
|
-
const handle: ServiceHandle = await svc((await store.get('/w'))!, { store, subscribe: () => () => {} });
|
|
108
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
109
|
-
await handle.stop();
|
|
110
|
-
|
|
111
|
-
const w = await store.get('/w');
|
|
112
|
-
const round = getComponent(w!, 'round') as any;
|
|
113
|
-
assert.ok(round.current >= 1, `expected round >= 1, got ${round.current}`);
|
|
114
|
-
assert.equal(round.phase, 'idle');
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('does not run when running=false', async () => {
|
|
118
|
-
await store.set(world('/w', false));
|
|
119
|
-
await store.set(agent('/w/a', 'Alice', 'A', 100, 100));
|
|
120
|
-
|
|
121
|
-
const svc = resolve('sim.world', 'service')!;
|
|
122
|
-
const handle: ServiceHandle = await svc((await store.get('/w'))!, { store, subscribe: () => () => {} });
|
|
123
|
-
await new Promise((r) => setTimeout(r, 400));
|
|
124
|
-
await handle.stop();
|
|
125
|
-
|
|
126
|
-
const w = await store.get('/w');
|
|
127
|
-
const round = getComponent(w!, 'round') as any;
|
|
128
|
-
assert.equal(round.current, 0);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('produces event log entries', async () => {
|
|
132
|
-
await store.set(world('/w', true));
|
|
133
|
-
await store.set(agent('/w/a', 'Alice', 'A', 100, 100));
|
|
134
|
-
await store.set(agent('/w/b', 'Bob', 'B', 200, 200));
|
|
135
|
-
|
|
136
|
-
const svc = resolve('sim.world', 'service')!;
|
|
137
|
-
const handle: ServiceHandle = await svc((await store.get('/w'))!, { store, subscribe: () => () => {} });
|
|
138
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
139
|
-
await handle.stop();
|
|
140
|
-
|
|
141
|
-
const w = await store.get('/w');
|
|
142
|
-
const round = getComponent(w!, 'round') as any;
|
|
143
|
-
assert.ok(round.log.length > 0, 'expected at least 1 event in log');
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('log entries have required fields', async () => {
|
|
147
|
-
await store.set(world('/w', true));
|
|
148
|
-
await store.set(agent('/w/a', 'Alice', 'A', 100, 100));
|
|
149
|
-
|
|
150
|
-
const svc = resolve('sim.world', 'service')!;
|
|
151
|
-
const handle: ServiceHandle = await svc((await store.get('/w'))!, { store, subscribe: () => () => {} });
|
|
152
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
153
|
-
await handle.stop();
|
|
154
|
-
|
|
155
|
-
const w = await store.get('/w');
|
|
156
|
-
const round = getComponent(w!, 'round') as any;
|
|
157
|
-
for (const entry of round.log) {
|
|
158
|
-
assert.ok(typeof entry.round === 'number');
|
|
159
|
-
assert.ok(typeof entry.agent === 'string');
|
|
160
|
-
assert.ok(typeof entry.action === 'string');
|
|
161
|
-
assert.ok(typeof entry.ts === 'number');
|
|
162
|
-
assert.ok(entry.data !== undefined);
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
describe('mock tools', () => {
|
|
168
|
-
it('move clamps to world bounds', async () => {
|
|
169
|
-
await store.set(world('/w', true));
|
|
170
|
-
// Place agent so random move could exceed bounds
|
|
171
|
-
await store.set(agent('/w/a', 'Alice', 'A', 599, 399));
|
|
172
|
-
|
|
173
|
-
const svc = resolve('sim.world', 'service')!;
|
|
174
|
-
const handle: ServiceHandle = await svc((await store.get('/w'))!, { store, subscribe: () => () => {} });
|
|
175
|
-
// Run several rounds to get a move action
|
|
176
|
-
await new Promise((r) => setTimeout(r, 1500));
|
|
177
|
-
await handle.stop();
|
|
178
|
-
|
|
179
|
-
const a = await store.get('/w/a');
|
|
180
|
-
const pos = getComponent(a!, 'position') as any;
|
|
181
|
-
assert.ok(pos.x >= 0 && pos.x <= 600, `x=${pos.x} out of bounds`);
|
|
182
|
-
assert.ok(pos.y >= 0 && pos.y <= 400, `y=${pos.y} out of bounds`);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('remember adds to memory', async () => {
|
|
186
|
-
await store.set(world('/w', true));
|
|
187
|
-
await store.set(agent('/w/a', 'Alice', 'A', 100, 100));
|
|
188
|
-
|
|
189
|
-
const svc = resolve('sim.world', 'service')!;
|
|
190
|
-
const handle: ServiceHandle = await svc((await store.get('/w'))!, { store, subscribe: () => () => {} });
|
|
191
|
-
// Run enough rounds for a remember action (mock: ~30% chance per round)
|
|
192
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
193
|
-
await handle.stop();
|
|
194
|
-
|
|
195
|
-
// Check if memory was modified (at least one remember should fire in ~10 rounds)
|
|
196
|
-
const a = await store.get('/w/a');
|
|
197
|
-
const mem = getComponent(a!, 'memory') as any;
|
|
198
|
-
// Can't guarantee remember fires, so just verify shape is intact
|
|
199
|
-
assert.ok(Array.isArray(mem.entries));
|
|
200
|
-
assert.ok(mem.entries.length <= 20, 'memory should be capped at 20');
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it('speak sets heardBy for nearby agents', async () => {
|
|
204
|
-
await store.set(world('/w', true));
|
|
205
|
-
await store.set(agent('/w/a', 'Alice', 'A', 100, 100, 300));
|
|
206
|
-
await store.set(agent('/w/b', 'Bob', 'B', 150, 150, 300));
|
|
207
|
-
|
|
208
|
-
const svc = resolve('sim.world', 'service')!;
|
|
209
|
-
const handle: ServiceHandle = await svc((await store.get('/w'))!, { store, subscribe: () => () => {} });
|
|
210
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
211
|
-
await handle.stop();
|
|
212
|
-
|
|
213
|
-
const w = await store.get('/w');
|
|
214
|
-
const round = getComponent(w!, 'round') as any;
|
|
215
|
-
const speakEvents = round.log.filter((e: any) => e.action === 'speak');
|
|
216
|
-
assert.ok(speakEvents.length > 0, 'should have at least one speak event');
|
|
217
|
-
for (const e of speakEvents) {
|
|
218
|
-
assert.ok(Array.isArray(e.heardBy), 'speak event should have heardBy');
|
|
219
|
-
}
|
|
220
|
-
});
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
describe('quorum (parallel execution)', () => {
|
|
224
|
-
it('all agents act in same round', async () => {
|
|
225
|
-
await store.set(world('/w', true));
|
|
226
|
-
await store.set(agent('/w/a', 'Alice', 'A', 100, 100));
|
|
227
|
-
await store.set(agent('/w/b', 'Bob', 'B', 200, 200));
|
|
228
|
-
await store.set(agent('/w/c', 'Eve', 'C', 300, 300));
|
|
229
|
-
|
|
230
|
-
const svc = resolve('sim.world', 'service')!;
|
|
231
|
-
const handle: ServiceHandle = await svc((await store.get('/w'))!, { store, subscribe: () => () => {} });
|
|
232
|
-
// Run several rounds — remember actions don't produce log entries, need enough rounds
|
|
233
|
-
await new Promise((r) => setTimeout(r, 1500));
|
|
234
|
-
await handle.stop();
|
|
235
|
-
|
|
236
|
-
const w = await store.get('/w');
|
|
237
|
-
const round = getComponent(w!, 'round') as any;
|
|
238
|
-
assert.ok(round.log.length >= 1, 'should have at least 1 event across all rounds');
|
|
239
|
-
// Verify multiple agents produced events across rounds
|
|
240
|
-
const actors = new Set(round.log.map((e: any) => e.agent));
|
|
241
|
-
assert.ok(actors.size >= 2, `expected >= 2 actors, got ${actors.size}: ${[...actors]}`);
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
describe('service lifecycle', () => {
|
|
246
|
-
it('stop halts the service', async () => {
|
|
247
|
-
await store.set(world('/w', true));
|
|
248
|
-
await store.set(agent('/w/a', 'Alice', 'A', 100, 100));
|
|
249
|
-
|
|
250
|
-
const svc = resolve('sim.world', 'service')!;
|
|
251
|
-
const handle: ServiceHandle = await svc((await store.get('/w'))!, { store, subscribe: () => () => {} });
|
|
252
|
-
await new Promise((r) => setTimeout(r, 300));
|
|
253
|
-
await handle.stop();
|
|
254
|
-
|
|
255
|
-
const w1 = await store.get('/w');
|
|
256
|
-
const round1 = (getComponent(w1!, 'round') as any).current;
|
|
257
|
-
|
|
258
|
-
// Wait more — should NOT advance
|
|
259
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
260
|
-
const w2 = await store.get('/w');
|
|
261
|
-
const round2 = (getComponent(w2!, 'round') as any).current;
|
|
262
|
-
assert.equal(round1, round2, 'round should not advance after stop');
|
|
263
|
-
});
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
describe('log trimming', () => {
|
|
267
|
-
it('log stays within 50 entries', async () => {
|
|
268
|
-
await store.set(world('/w', true));
|
|
269
|
-
await store.set(agent('/w/a', 'Alice', 'A', 100, 100, 300));
|
|
270
|
-
await store.set(agent('/w/b', 'Bob', 'B', 150, 150, 300));
|
|
271
|
-
|
|
272
|
-
const svc = resolve('sim.world', 'service')!;
|
|
273
|
-
const handle: ServiceHandle = await svc((await store.get('/w'))!, { store, subscribe: () => () => {} });
|
|
274
|
-
// Run many rounds
|
|
275
|
-
await new Promise((r) => setTimeout(r, 3000));
|
|
276
|
-
await handle.stop();
|
|
277
|
-
|
|
278
|
-
const w = await store.get('/w');
|
|
279
|
-
const round = getComponent(w!, 'round') as any;
|
|
280
|
-
assert.ok(round.log.length <= 50, `log has ${round.log.length} entries, expected <= 50`);
|
|
281
|
-
});
|
|
282
|
-
});
|