lyra-core 0.1.1 → 0.1.10

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.
Files changed (40) hide show
  1. package/package.json +3 -15
  2. package/src/paths.ts +10 -0
  3. package/src/brain/compaction.test.ts +0 -156
  4. package/src/brain/frozen-prefix.test.ts +0 -235
  5. package/src/brain/history-persist.test.ts +0 -129
  6. package/src/brain/openai-brain.test.ts +0 -61
  7. package/src/brain/sanitize.test.ts +0 -27
  8. package/src/claude-plugins/discovery.test.ts +0 -71
  9. package/src/commands/registry.test.ts +0 -186
  10. package/src/events/queue.test.ts +0 -43
  11. package/src/index.test.ts +0 -6
  12. package/src/locks.test.ts +0 -259
  13. package/src/mcp/discovery.test.ts +0 -97
  14. package/src/mcp/manager.test.ts +0 -97
  15. package/src/memory/edit.test.ts +0 -41
  16. package/src/memory/encryption.test.ts +0 -68
  17. package/src/memory/index-sync.test.ts +0 -81
  18. package/src/memory/pack-blob.test.ts +0 -90
  19. package/src/memory/pack-gather.test.ts +0 -110
  20. package/src/memory/parser.test.ts +0 -29
  21. package/src/memory/path-drift.test.ts +0 -71
  22. package/src/memory/read-tool-fallback.test.ts +0 -54
  23. package/src/memory/save-tool.test.ts +0 -193
  24. package/src/memory/scan.test.ts +0 -24
  25. package/src/migration/option3-crypto.test.ts +0 -106
  26. package/src/pairing.test.ts +0 -212
  27. package/src/permission/permission.test.ts +0 -299
  28. package/src/plugins/plugins.test.ts +0 -196
  29. package/src/public/card.test.ts +0 -70
  30. package/src/runtime/runtime.test.ts +0 -55
  31. package/src/sandbox/sandbox.test.ts +0 -386
  32. package/src/skills/scanner.test.ts +0 -137
  33. package/src/skills/triggers.test.ts +0 -77
  34. package/src/storage/encryption.test.ts +0 -30
  35. package/src/storage/sqlite.test.ts +0 -29
  36. package/src/tools/escalation.test.ts +0 -348
  37. package/src/tools/registry.test.ts +0 -70
  38. package/src/tools/zod-helpers.test.ts +0 -63
  39. package/src/wallet/drain.test.ts +0 -41
  40. package/src/wallet/keystore.test.ts +0 -17
@@ -1,97 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
2
- import { mkdtemp, rm, writeFile } from 'node:fs/promises'
3
- import { tmpdir } from 'node:os'
4
- import { join } from 'node:path'
5
- import { ToolRegistry } from '../tools/registry'
6
- import type { ToolDef } from '../tools/types'
7
- import { McpManager } from './manager'
8
-
9
- let scratch: string
10
- let serverPath: string
11
-
12
- beforeEach(async () => {
13
- scratch = await mkdtemp(join(tmpdir(), 'lyra-mcp-server-'))
14
- serverPath = join(scratch, 'server.ts')
15
- // Minimal stdio MCP server: handles initialize, tools/list, tools/call.
16
- await writeFile(
17
- serverPath,
18
- `process.stdin.setEncoding('utf8')
19
- let buf = ''
20
- process.stdin.on('data', chunk => {
21
- buf += chunk
22
- let nl = buf.indexOf('\\n')
23
- while (nl !== -1) {
24
- const line = buf.slice(0, nl).trim()
25
- buf = buf.slice(nl + 1)
26
- nl = buf.indexOf('\\n')
27
- if (!line) continue
28
- const msg = JSON.parse(line)
29
- if (msg.method === 'initialize') {
30
- process.stdout.write(JSON.stringify({
31
- jsonrpc: '2.0', id: msg.id,
32
- result: { protocolVersion: '2024-11-05', capabilities: {}, serverInfo: { name: 'fake', version: '1.0.0' } },
33
- }) + '\\n')
34
- } else if (msg.method === 'tools/list') {
35
- process.stdout.write(JSON.stringify({
36
- jsonrpc: '2.0', id: msg.id,
37
- result: { tools: [{ name: 'echo', description: 'echoes input', inputSchema: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] } }] },
38
- }) + '\\n')
39
- } else if (msg.method === 'tools/call') {
40
- process.stdout.write(JSON.stringify({
41
- jsonrpc: '2.0', id: msg.id,
42
- result: { content: [{ type: 'text', text: msg.params.arguments.text }] },
43
- }) + '\\n')
44
- }
45
- }
46
- })
47
- `,
48
- )
49
- })
50
-
51
- afterEach(async () => {
52
- await rm(scratch, { recursive: true, force: true })
53
- })
54
-
55
- describe('McpManager', () => {
56
- it('spawns stdio server, registers tool, dispatches via registry', async () => {
57
- const mgr = new McpManager([
58
- { name: 'fake', type: 'stdio', command: 'bun', args: ['run', serverPath] },
59
- ])
60
- const reg = new ToolRegistry()
61
- const result = await mgr.registerAll(def => reg.register(def))
62
- expect(result.registered).toBe(1)
63
- expect(result.failed).toEqual([])
64
-
65
- const tool = reg.find('mcp.fake.echo') as ToolDef
66
- expect(tool).toBeDefined()
67
- expect(tool.shouldDefer).toBe(true)
68
- expect(tool.parametersOverride?.properties).toEqual({ text: { type: 'string' } })
69
-
70
- const out = await reg.dispatch({ id: '1', name: 'mcp.fake.echo', args: { text: 'hi' } })
71
- expect(out.ok).toBe(true)
72
- expect(out.data).toMatchObject({ content: [{ type: 'text', text: 'hi' }] })
73
-
74
- mgr.closeAll()
75
- })
76
-
77
- it('reports failed servers without throwing', async () => {
78
- const mgr = new McpManager([{ name: 'broken', type: 'stdio', command: '/does/not/exist' }])
79
- const reg = new ToolRegistry()
80
- const result = await mgr.registerAll(def => reg.register(def))
81
- expect(result.registered).toBe(0)
82
- expect(result.failed).toHaveLength(1)
83
- expect(result.failed[0]!.server).toBe('broken')
84
- mgr.closeAll()
85
- })
86
-
87
- it('flags http servers as not yet supported', async () => {
88
- const mgr = new McpManager([
89
- { name: 'http-thing', type: 'http', url: 'https://example.com/mcp' },
90
- ])
91
- const reg = new ToolRegistry()
92
- const result = await mgr.registerAll(def => reg.register(def))
93
- expect(result.registered).toBe(0)
94
- expect(result.failed[0]!.error).toContain('http MCP')
95
- mgr.closeAll()
96
- })
97
- })
@@ -1,41 +0,0 @@
1
- import { expect, test } from 'bun:test'
2
- import { EditError, applyEdit } from './edit'
3
-
4
- test('add on empty body inserts content', () => {
5
- const out = applyEdit('', { action: 'add', newText: 'hello' })
6
- expect(out).toBe('hello')
7
- })
8
-
9
- test('add appends with double newline on existing body', () => {
10
- const out = applyEdit('first line', { action: 'add', newText: 'second line' })
11
- expect(out).toBe('first line\n\nsecond line\n')
12
- })
13
-
14
- test('replace substring', () => {
15
- const out = applyEdit('foo bar baz', {
16
- action: 'replace',
17
- oldText: 'bar',
18
- newText: 'qux',
19
- })
20
- expect(out).toBe('foo qux baz')
21
- })
22
-
23
- test('replace throws when oldText missing', () => {
24
- expect(() =>
25
- applyEdit('foo bar', { action: 'replace', oldText: 'missing', newText: 'x' }),
26
- ).toThrow(EditError)
27
- })
28
-
29
- test('replace throws when oldText is ambiguous', () => {
30
- expect(() => applyEdit('foo foo', { action: 'replace', oldText: 'foo', newText: 'x' })).toThrow(
31
- EditError,
32
- )
33
- })
34
-
35
- test('remove strips matching substring', () => {
36
- const out = applyEdit('leading payload trailing', {
37
- action: 'remove',
38
- oldText: 'payload ',
39
- })
40
- expect(out).toBe('leading trailing')
41
- })
@@ -1,68 +0,0 @@
1
- import { describe, expect, test } from 'bun:test'
2
- import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519'
3
- import {
4
- MEMORY_BLOB_VERSION,
5
- decryptMemoryBytes,
6
- deriveMemoryKey,
7
- encryptMemoryBytes,
8
- } from './encryption'
9
-
10
- const genSecret = (): string => Ed25519Keypair.generate().getSecretKey()
11
-
12
- describe('memory encryption', () => {
13
- test('round-trip: encrypt + decrypt with same agent key', () => {
14
- const agentPriv = genSecret()
15
- const key = deriveMemoryKey(agentPriv)
16
- const plaintext = new TextEncoder().encode('# identity\n\nagent-001 minted at block 42\n')
17
- const blob = encryptMemoryBytes(plaintext, key)
18
- expect(blob[0]).toBe(MEMORY_BLOB_VERSION)
19
- const out = decryptMemoryBytes(blob, key)
20
- expect(new TextDecoder().decode(out)).toBe(new TextDecoder().decode(plaintext))
21
- })
22
-
23
- test('different agent keys produce different ciphertexts (key separation)', () => {
24
- const a = deriveMemoryKey(genSecret())
25
- const b = deriveMemoryKey(genSecret())
26
- const pt = new TextEncoder().encode('hello')
27
- const ba = encryptMemoryBytes(pt, a)
28
- const bb = encryptMemoryBytes(pt, b)
29
- expect(Buffer.from(ba).equals(Buffer.from(bb))).toBe(false)
30
- expect(() => decryptMemoryBytes(ba, b)).toThrow()
31
- })
32
-
33
- test('two encrypts of same plaintext yield different ciphertexts (random IV)', () => {
34
- const key = deriveMemoryKey(genSecret())
35
- const pt = new TextEncoder().encode('same plaintext')
36
- const a = encryptMemoryBytes(pt, key)
37
- const b = encryptMemoryBytes(pt, key)
38
- expect(Buffer.from(a).equals(Buffer.from(b))).toBe(false)
39
- expect(new TextDecoder().decode(decryptMemoryBytes(a, key))).toBe('same plaintext')
40
- expect(new TextDecoder().decode(decryptMemoryBytes(b, key))).toBe('same plaintext')
41
- })
42
-
43
- test('tampered ciphertext fails GCM auth', () => {
44
- const key = deriveMemoryKey(genSecret())
45
- const blob = encryptMemoryBytes(new TextEncoder().encode('x'), key)
46
- blob[blob.length - 1] = (blob[blob.length - 1] ?? 0) ^ 0xff
47
- expect(() => decryptMemoryBytes(blob, key)).toThrow()
48
- })
49
-
50
- test('rejects unsupported version byte', () => {
51
- const key = deriveMemoryKey(genSecret())
52
- const blob = encryptMemoryBytes(new TextEncoder().encode('x'), key)
53
- blob[0] = 99
54
- expect(() => decryptMemoryBytes(blob, key)).toThrow(/unsupported memory blob version/)
55
- })
56
-
57
- test('rejects too-short blob', () => {
58
- const key = deriveMemoryKey(genSecret())
59
- expect(() => decryptMemoryBytes(new Uint8Array([1, 2, 3]), key)).toThrow(/too short/)
60
- })
61
-
62
- test('determinism: same agent privkey always derives same key', () => {
63
- const priv = genSecret()
64
- const k1 = deriveMemoryKey(priv)
65
- const k2 = deriveMemoryKey(priv)
66
- expect(k1.equals(k2)).toBe(true)
67
- })
68
- })
@@ -1,81 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
2
- import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
3
- import { tmpdir } from 'node:os'
4
- import { join } from 'node:path'
5
- import { ensureSyntheticIndexEntries } from './index-sync'
6
-
7
- describe('ensureSyntheticIndexEntries', () => {
8
- let memoryDir: string
9
-
10
- beforeEach(() => {
11
- memoryDir = mkdtempSync(join(tmpdir(), 'index-sync-'))
12
- mkdirSync(join(memoryDir, 'agent'), { recursive: true })
13
- mkdirSync(join(memoryDir, 'user'), { recursive: true })
14
- writeFileSync(join(memoryDir, 'MEMORY.md'), '# Memory\n\n', 'utf8')
15
- })
16
-
17
- afterEach(() => {
18
- if (memoryDir) rmSync(memoryDir, { recursive: true, force: true })
19
- })
20
-
21
- it('adds entries for files that exist on disk with frontmatter descriptions', async () => {
22
- writeFileSync(
23
- join(memoryDir, 'agent', 'identity.md'),
24
- '---\nname: identity\ndescription: Auto-written agent identity facts\ntype: agent-identity\n---\n# id',
25
- )
26
- writeFileSync(
27
- join(memoryDir, 'agent', 'persona.md'),
28
- '---\nname: persona\ndescription: Voice + behavior style\ntype: agent-persona\n---\nbody',
29
- )
30
- writeFileSync(
31
- join(memoryDir, 'user', 'profile.md'),
32
- '---\nname: profile\ndescription: User profile\ntype: user\n---\nbody',
33
- )
34
- const r = await ensureSyntheticIndexEntries(memoryDir)
35
- expect(r.added).toEqual(['agent/identity.md', 'agent/persona.md', 'user/profile.md'])
36
- expect(r.skipped).toEqual([])
37
- const idx = readFileSync(join(memoryDir, 'MEMORY.md'), 'utf8')
38
- expect(idx).toContain('agent/identity.md')
39
- expect(idx).toContain('Auto-written agent identity facts')
40
- expect(idx).toContain('agent/persona.md')
41
- expect(idx).toContain('user/profile.md')
42
- })
43
-
44
- it('is idempotent — second call adds nothing', async () => {
45
- writeFileSync(
46
- join(memoryDir, 'agent', 'identity.md'),
47
- '---\nname: identity\ndescription: id desc\ntype: agent-identity\n---\nbody',
48
- )
49
- await ensureSyntheticIndexEntries(memoryDir)
50
- const r2 = await ensureSyntheticIndexEntries(memoryDir)
51
- expect(r2.added).toEqual([])
52
- expect(r2.skipped).toContain('agent/identity.md')
53
- })
54
-
55
- it('skips files that do not exist on disk', async () => {
56
- // Only persona exists, no identity.md or profile.md.
57
- writeFileSync(
58
- join(memoryDir, 'agent', 'persona.md'),
59
- '---\nname: persona\ndescription: voice\ntype: agent-persona\n---\nbody',
60
- )
61
- const r = await ensureSyntheticIndexEntries(memoryDir)
62
- expect(r.added).toEqual(['agent/persona.md'])
63
- expect(r.skipped).toEqual(['agent/identity.md', 'user/profile.md'])
64
- })
65
-
66
- it('falls back to filename title when frontmatter is missing', async () => {
67
- writeFileSync(join(memoryDir, 'agent', 'identity.md'), 'no frontmatter here')
68
- const r = await ensureSyntheticIndexEntries(memoryDir)
69
- expect(r.added).toEqual(['agent/identity.md'])
70
- const idx = readFileSync(join(memoryDir, 'MEMORY.md'), 'utf8')
71
- expect(idx).toContain('agent/identity.md')
72
- expect(idx).toContain('identity')
73
- })
74
-
75
- it('no-ops gracefully when MEMORY.md is missing', async () => {
76
- rmSync(join(memoryDir, 'MEMORY.md'))
77
- const r = await ensureSyntheticIndexEntries(memoryDir)
78
- expect(r.added).toEqual([])
79
- expect(r.skipped.length).toBe(3)
80
- })
81
- })
@@ -1,90 +0,0 @@
1
- import { describe, expect, test } from 'bun:test'
2
- import { PACK_BLOB_VERSION, decodePackBlob, encodePackBlob, isV2Envelope } from './pack-blob'
3
-
4
- describe('pack-blob v2 envelope', () => {
5
- test('encode + decode round-trips single root', () => {
6
- const bytes = encodePackBlob({ root: '# MEMORY.md\n\nentries' })
7
- expect(isV2Envelope(bytes)).toBe(true)
8
- const blob = decodePackBlob(bytes)
9
- expect(blob.v).toBe(PACK_BLOB_VERSION)
10
- expect(blob.root).toBe('# MEMORY.md\n\nentries')
11
- expect(blob.files).toEqual({})
12
- })
13
-
14
- test('encode + decode round-trips with files map', () => {
15
- const bytes = encodePackBlob({
16
- root: '# profile',
17
- files: {
18
- 'operator-preferences.md': '# Operator Preferences\n\ndark mode',
19
- 'hackathon-deadline.md': 'May 16 2026',
20
- },
21
- })
22
- const blob = decodePackBlob(bytes)
23
- expect(blob.root).toBe('# profile')
24
- expect(blob.files['operator-preferences.md']).toContain('dark mode')
25
- expect(blob.files['hackathon-deadline.md']).toBe('May 16 2026')
26
- })
27
-
28
- test('encode rejects unsafe filenames', () => {
29
- expect(() => encodePackBlob({ root: 'x', files: { '../etc/passwd.md': 'hax' } })).toThrow(
30
- /unsafe filename/,
31
- )
32
- expect(() => encodePackBlob({ root: 'x', files: { 'no-dot-md': 'x' } })).toThrow(
33
- /unsafe filename/,
34
- )
35
- expect(() => encodePackBlob({ root: 'x', files: { 'UPPERCASE.md': 'x' } })).toThrow(
36
- /unsafe filename/,
37
- )
38
- expect(() => encodePackBlob({ root: 'x', files: { 'has spaces.md': 'x' } })).toThrow(
39
- /unsafe filename/,
40
- )
41
- expect(() => encodePackBlob({ root: 'x', files: { '.md': 'x' } })).toThrow(/unsafe filename/)
42
- })
43
-
44
- test('isV2Envelope rejects legacy markdown', () => {
45
- const md = new TextEncoder().encode('# Heading\n\ncontent')
46
- expect(isV2Envelope(md)).toBe(false)
47
- })
48
-
49
- test('isV2Envelope rejects v1 JSON without v:2', () => {
50
- const v1 = new TextEncoder().encode('{"version":1,"data":"x"}')
51
- expect(isV2Envelope(v1)).toBe(false)
52
- })
53
-
54
- test('isV2Envelope rejects garbage', () => {
55
- const junk = new Uint8Array([0xff, 0x00, 0xab, 0xcd])
56
- expect(isV2Envelope(junk)).toBe(false)
57
- expect(isV2Envelope(new Uint8Array(0))).toBe(false)
58
- })
59
-
60
- test('isV2Envelope tolerates leading whitespace', () => {
61
- const bytes = new TextEncoder().encode(' {"v":2,"root":"x","files":{}}')
62
- expect(isV2Envelope(bytes)).toBe(true)
63
- })
64
-
65
- test('decode rejects mismatched version', () => {
66
- const bytes = new TextEncoder().encode('{"v":999,"root":"x"}')
67
- expect(() => decodePackBlob(bytes)).toThrow(/expected v=2/)
68
- })
69
-
70
- test('decode tolerates unsafe filenames by dropping them', () => {
71
- const bytes = new TextEncoder().encode(
72
- '{"v":2,"root":"x","files":{"ok.md":"y","../bad.md":"hax"}}',
73
- )
74
- const blob = decodePackBlob(bytes)
75
- expect(blob.files['ok.md']).toBe('y')
76
- expect(blob.files['../bad.md']).toBeUndefined()
77
- })
78
-
79
- test('handles large blobs (~100 small files)', () => {
80
- const files: Record<string, string> = {}
81
- for (let i = 0; i < 100; i++) {
82
- files[`learned-${i}.md`] = `fact ${i}\n`.repeat(50)
83
- }
84
- const bytes = encodePackBlob({ root: '# MEMORY', files })
85
- expect(bytes.length).toBeGreaterThan(1000)
86
- const blob = decodePackBlob(bytes)
87
- expect(Object.keys(blob.files).length).toBe(100)
88
- expect(blob.files['learned-50.md']).toContain('fact 50')
89
- })
90
- })
@@ -1,110 +0,0 @@
1
- import { afterEach, beforeEach, expect, test } from 'bun:test'
2
- import { mkdtempSync, rmSync } from 'node:fs'
3
- import { mkdir, readFile, writeFile } from 'node:fs/promises'
4
- import { tmpdir } from 'node:os'
5
- import { join } from 'node:path'
6
- import { decodePackBlob, encodePackBlob } from './pack-blob'
7
- import { gatherAgentPack, gatherUserPack, writeAgentPack, writeUserPack } from './pack-gather'
8
-
9
- let tmp: string
10
-
11
- beforeEach(() => {
12
- tmp = mkdtempSync(join(tmpdir(), 'pack-gather-'))
13
- })
14
-
15
- afterEach(() => {
16
- rmSync(tmp, { recursive: true, force: true })
17
- })
18
-
19
- async function seed(rel: string, content: string): Promise<void> {
20
- const full = join(tmp, rel)
21
- await mkdir(join(full, '..'), { recursive: true })
22
- await writeFile(full, content)
23
- }
24
-
25
- test('gatherAgentPack returns root + non-excluded files', async () => {
26
- await seed('MEMORY.md', '# memory index')
27
- await seed('agent/identity.md', 'identity body')
28
- await seed('agent/persona.md', 'persona body')
29
- await seed('agent/learned-foo.md', 'foo content')
30
- await seed('agent/learned-bar.md', 'bar content')
31
-
32
- const res = await gatherAgentPack(tmp)
33
- expect(res.root).toBe('# memory index')
34
- expect(res.files['learned-foo.md']).toBe('foo content')
35
- expect(res.files['learned-bar.md']).toBe('bar content')
36
- // Identity + persona excluded — they have their own slots
37
- expect(res.files['identity.md']).toBeUndefined()
38
- expect(res.files['persona.md']).toBeUndefined()
39
- })
40
-
41
- test('gatherUserPack returns profile.md as root + other user files', async () => {
42
- await seed('user/profile.md', '# user profile')
43
- await seed('user/operator-preferences.md', 'dark mode')
44
- await seed('user/0g-hackathon.md', 'deadline')
45
- const res = await gatherUserPack(tmp)
46
- expect(res.root).toBe('# user profile')
47
- expect(res.files['operator-preferences.md']).toBe('dark mode')
48
- expect(res.files['0g-hackathon.md']).toBe('deadline')
49
- // profile.md is the root, not in files
50
- expect(res.files['profile.md']).toBeUndefined()
51
- })
52
-
53
- test('gather handles missing partition dir', async () => {
54
- await seed('MEMORY.md', 'just an index')
55
- const res = await gatherAgentPack(tmp)
56
- expect(res.root).toBe('just an index')
57
- expect(res.files).toEqual({})
58
- })
59
-
60
- test('gather handles missing root file', async () => {
61
- await seed('agent/learned-x.md', 'content')
62
- const res = await gatherAgentPack(tmp)
63
- expect(res.root).toBe('')
64
- expect(res.files['learned-x.md']).toBe('content')
65
- })
66
-
67
- test('gather skips non-md files + empty .md', async () => {
68
- await seed('agent/learned-x.md', 'content')
69
- await seed('agent/junk.txt', 'not md')
70
- await seed('agent/empty.md', '')
71
- const res = await gatherAgentPack(tmp)
72
- expect(res.files['learned-x.md']).toBe('content')
73
- expect(res.files['junk.txt']).toBeUndefined()
74
- expect(res.files['empty.md']).toBeUndefined()
75
- })
76
-
77
- test('writeAgentPack reverses gatherAgentPack', async () => {
78
- await seed('MEMORY.md', 'index v1')
79
- await seed('agent/learned-a.md', 'a')
80
- await seed('agent/learned-b.md', 'b')
81
- const gathered = await gatherAgentPack(tmp)
82
- const bytes = encodePackBlob(gathered)
83
- const decoded = decodePackBlob(bytes)
84
-
85
- const dst = mkdtempSync(join(tmpdir(), 'pack-write-'))
86
- try {
87
- await writeAgentPack(dst, decoded)
88
- expect(await readFile(join(dst, 'MEMORY.md'), 'utf8')).toBe('index v1')
89
- expect(await readFile(join(dst, 'agent', 'learned-a.md'), 'utf8')).toBe('a')
90
- expect(await readFile(join(dst, 'agent', 'learned-b.md'), 'utf8')).toBe('b')
91
- } finally {
92
- rmSync(dst, { recursive: true, force: true })
93
- }
94
- })
95
-
96
- test('writeUserPack creates user dir + profile + sibling files', async () => {
97
- const dst = mkdtempSync(join(tmpdir(), 'pack-write-'))
98
- try {
99
- await writeUserPack(dst, {
100
- v: 2,
101
- root: '# profile',
102
- files: { 'operator-preferences.md': 'prefs', 'hack-2026.md': 'deadline' },
103
- })
104
- expect(await readFile(join(dst, 'user', 'profile.md'), 'utf8')).toBe('# profile')
105
- expect(await readFile(join(dst, 'user', 'operator-preferences.md'), 'utf8')).toBe('prefs')
106
- expect(await readFile(join(dst, 'user', 'hack-2026.md'), 'utf8')).toBe('deadline')
107
- } finally {
108
- rmSync(dst, { recursive: true, force: true })
109
- }
110
- })
@@ -1,29 +0,0 @@
1
- import { expect, test } from 'bun:test'
2
- import { parseTopic, stringifyTopic } from './parser'
3
-
4
- const SAMPLE = `---
5
- name: user feedback
6
- description: elpabl0 prefers terse replies
7
- type: feedback
8
- ---
9
- User confirmed no wall-of-text responses in conversations.
10
- `
11
-
12
- test('parseTopic reads frontmatter + body', () => {
13
- const topic = parseTopic('user', 'feedback-terse', SAMPLE)
14
- expect(topic.frontmatter.name).toBe('user feedback')
15
- expect(topic.frontmatter.type).toBe('feedback')
16
- expect(topic.body.startsWith('User confirmed')).toBe(true)
17
- })
18
-
19
- test('stringifyTopic round-trips', () => {
20
- const topic = parseTopic('user', 'feedback-terse', SAMPLE)
21
- const out = stringifyTopic(topic)
22
- const reparsed = parseTopic('user', 'feedback-terse', out)
23
- expect(reparsed.frontmatter.name).toBe(topic.frontmatter.name)
24
- expect(reparsed.body.trim()).toBe(topic.body.trim())
25
- })
26
-
27
- test('parseTopic throws on missing frontmatter', () => {
28
- expect(() => parseTopic('user', 'broken', '# no frontmatter\ntext')).toThrow()
29
- })
@@ -1,71 +0,0 @@
1
- import { afterAll, beforeAll, describe, expect, it } from 'bun:test'
2
- import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
3
- import { tmpdir } from 'node:os'
4
- import { join } from 'node:path'
5
- import { makeMemoryReadTool } from './read-tool'
6
- import { makeMemorySaveTool } from './save-tool'
7
-
8
- // v0.23.0 Bundle A regression test.
9
- //
10
- // Gateway daemon writes restored memory under `${TMPDIR}/lyra-gateway/<id>/`
11
- // while `agentPaths.agent(id).memoryDir` resolves to `~/.lyra/agents/<id>/`.
12
- // Before the fix, memory.read / memory.save resolved against agentPaths
13
- // unconditionally, so files the gateway just restored to disk were invisible
14
- // to the brain and any new save landed in the wrong tree. The `agentDir`
15
- // override threads the gateway's true root through so both tools resolve
16
- // against the same path the runtime is using.
17
- describe('memory.read / memory.save honor agentDir override', () => {
18
- let tmpAgentDir: string
19
- const fakeAgentId = '0000000000000001'
20
-
21
- beforeAll(() => {
22
- tmpAgentDir = mkdtempSync(join(tmpdir(), 'memdrift-'))
23
- mkdirSync(join(tmpAgentDir, 'memory', 'agent'), { recursive: true })
24
- mkdirSync(join(tmpAgentDir, 'memory', 'user'), { recursive: true })
25
- writeFileSync(
26
- join(tmpAgentDir, 'memory', 'MEMORY.md'),
27
- '# Memory\n\n- [identity](./agent/identity.md) seed\n',
28
- )
29
- writeFileSync(
30
- join(tmpAgentDir, 'memory', 'agent', 'identity.md'),
31
- '---\nname: identity\ntype: agent-identity\n---\nseeded from override path',
32
- )
33
- })
34
-
35
- afterAll(() => {
36
- if (tmpAgentDir) rmSync(tmpAgentDir, { recursive: true, force: true })
37
- })
38
-
39
- it('memory.read resolves against agentDir, not agentPaths', async () => {
40
- const tool = makeMemoryReadTool({ agentId: fakeAgentId, agentDir: tmpAgentDir })
41
- const r = await tool.handler({ name: 'identity' })
42
- expect(r.ok).toBe(true)
43
- if (r.ok) {
44
- const data = r.data as { path: string; content: string }
45
- expect(data.path).toBe('agent/identity.md')
46
- expect(data.content).toContain('seeded from override path')
47
- }
48
- })
49
-
50
- it('memory.save lands the file under agentDir', async () => {
51
- const tool = makeMemorySaveTool({ agentId: fakeAgentId, agentDir: tmpAgentDir })
52
- const r = await tool.handler({
53
- name: 'override save test',
54
- description: 'Test that save honors agentDir parameter',
55
- type: 'user',
56
- content: 'body content',
57
- })
58
- expect(r.ok).toBe(true)
59
- if (r.ok) {
60
- const data = r.data as { file: string }
61
- expect(data.file).toBe('user/override-save-test.md')
62
- }
63
- const written = readFileSync(
64
- join(tmpAgentDir, 'memory', 'user', 'override-save-test.md'),
65
- 'utf8',
66
- )
67
- expect(written).toContain('body content')
68
- const index = readFileSync(join(tmpAgentDir, 'memory', 'MEMORY.md'), 'utf8')
69
- expect(index).toContain('user/override-save-test.md')
70
- })
71
- })
@@ -1,54 +0,0 @@
1
- import { afterAll, beforeAll, describe, expect, it } from 'bun:test'
2
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
3
- import { tmpdir } from 'node:os'
4
- import { join } from 'node:path'
5
- import { agentPaths } from '../paths'
6
- import { makeMemoryReadTool } from './read-tool'
7
-
8
- describe('memory.read token-overlap fallback', () => {
9
- let tmpRoot: string
10
- const fakeAgentId = '0000000000000001'
11
-
12
- beforeAll(() => {
13
- tmpRoot = mkdtempSync(join(tmpdir(), 'memread-fallback-'))
14
- process.env.LYRA_HOME = tmpRoot
15
- const paths = agentPaths.agent(fakeAgentId)
16
- mkdirSync(paths.memoryDir, { recursive: true })
17
- mkdirSync(join(paths.memoryDir, 'user'), { recursive: true })
18
- writeFileSync(
19
- paths.memoryIndex,
20
- `# Memory
21
-
22
- - [Tool test session 2026-05-12](./user/tool-test-session.md) — Full tool verification
23
- - [Operator profile](./user/operator-profile.md) — Operator known as elpabl0
24
- `,
25
- )
26
- writeFileSync(
27
- join(paths.memoryDir, 'user/tool-test-session.md'),
28
- '---\nname: Tool test session\ntype: user\n---\nContent.',
29
- )
30
- })
31
-
32
- afterAll(() => {
33
- if (tmpRoot) rmSync(tmpRoot, { recursive: true, force: true })
34
- })
35
-
36
- it('paraphrased query finds entry via token-overlap', async () => {
37
- const tool = makeMemoryReadTool({ agentId: fakeAgentId })
38
- const r = await tool.handler({ name: 'tool test run' })
39
- expect(r.ok).toBe(true)
40
- if (r.ok) expect((r.data as { path: string }).path).toContain('tool-test-session')
41
- })
42
-
43
- it('unrelated query returns not-found', async () => {
44
- const tool = makeMemoryReadTool({ agentId: fakeAgentId })
45
- const r = await tool.handler({ name: 'banana cake unrelated' })
46
- expect(r.ok).toBe(false)
47
- })
48
-
49
- it('exact title match still works', async () => {
50
- const tool = makeMemoryReadTool({ agentId: fakeAgentId })
51
- const r = await tool.handler({ name: 'Tool test session' })
52
- expect(r.ok).toBe(true)
53
- })
54
- })