@swarmclawai/swarmclaw 0.7.7 → 0.8.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/README.md +12 -14
- package/next.config.ts +13 -2
- package/package.json +4 -2
- package/src/app/api/agents/[id]/thread/route.ts +9 -0
- package/src/app/api/agents/route.ts +4 -0
- package/src/app/api/agents/thread-route.test.ts +133 -0
- package/src/app/api/approvals/route.test.ts +148 -0
- package/src/app/api/canvas/[sessionId]/route.ts +3 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
- package/src/app/api/chats/[id]/devserver/route.ts +48 -7
- package/src/app/api/chats/[id]/messages/route.ts +42 -18
- package/src/app/api/chats/[id]/route.ts +1 -1
- package/src/app/api/chats/[id]/stop/route.ts +5 -4
- package/src/app/api/chats/route.ts +23 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +46 -3
- package/src/app/api/connectors/route.ts +12 -8
- package/src/app/api/external-agents/route.test.ts +165 -0
- package/src/app/api/gateways/[id]/health/route.ts +27 -12
- package/src/app/api/gateways/[id]/route.ts +2 -0
- package/src/app/api/gateways/health-route.test.ts +135 -0
- package/src/app/api/gateways/route.ts +2 -0
- package/src/app/api/mcp-servers/route.test.ts +130 -0
- package/src/app/api/openclaw/deploy/route.ts +38 -5
- package/src/app/api/plugins/install/route.ts +46 -6
- package/src/app/api/plugins/marketplace/route.ts +48 -15
- package/src/app/api/preview-server/route.ts +26 -11
- package/src/app/api/projects/[id]/route.ts +6 -2
- package/src/app/api/projects/route.ts +4 -3
- package/src/app/api/schedules/[id]/run/route.ts +4 -0
- package/src/app/api/schedules/route.test.ts +86 -0
- package/src/app/api/schedules/route.ts +6 -1
- package/src/app/api/secrets/[id]/route.ts +1 -0
- package/src/app/api/secrets/route.ts +2 -1
- package/src/app/api/settings/route.ts +2 -0
- package/src/app/api/setup/check-provider/route.test.ts +19 -0
- package/src/app/api/setup/check-provider/route.ts +40 -10
- package/src/app/api/skills/[id]/route.ts +12 -0
- package/src/app/api/skills/import/route.ts +14 -12
- package/src/app/api/skills/route.ts +13 -1
- package/src/app/api/tasks/[id]/route.ts +10 -1
- package/src/app/api/tasks/import/github/route.test.ts +65 -0
- package/src/app/api/tasks/import/github/route.ts +337 -0
- package/src/app/api/wallets/[id]/approve/route.ts +17 -3
- package/src/app/api/wallets/[id]/route.ts +79 -33
- package/src/app/api/wallets/[id]/send/route.ts +19 -33
- package/src/app/api/wallets/route.ts +78 -61
- package/src/app/api/webhooks/[id]/route.ts +33 -6
- package/src/app/api/webhooks/route.test.ts +272 -0
- package/src/cli/index.js +1 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-card.tsx +9 -2
- package/src/components/agents/agent-chat-list.tsx +18 -2
- package/src/components/agents/agent-list.tsx +1 -0
- package/src/components/agents/agent-sheet.tsx +257 -38
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-area.tsx +36 -19
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +48 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- package/src/components/chat/delegation-banner.test.ts +14 -1
- package/src/components/chat/delegation-banner.tsx +1 -1
- package/src/components/chat/message-bubble.tsx +208 -145
- package/src/components/chat/message-list.tsx +48 -19
- package/src/components/chatrooms/chatroom-message.tsx +2 -2
- package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
- package/src/components/connectors/connector-health.tsx +1 -1
- package/src/components/connectors/connector-list.tsx +7 -2
- package/src/components/connectors/connector-sheet.tsx +337 -148
- package/src/components/gateways/gateway-sheet.tsx +2 -2
- package/src/components/layout/app-layout.tsx +40 -23
- package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
- package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
- package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
- package/src/components/plugins/plugin-list.tsx +45 -9
- package/src/components/plugins/plugin-sheet.tsx +55 -7
- package/src/components/projects/project-detail.tsx +217 -0
- package/src/components/projects/project-sheet.tsx +176 -4
- package/src/components/providers/provider-list.tsx +2 -1
- package/src/components/providers/provider-sheet.tsx +21 -2
- package/src/components/schedules/schedule-card.tsx +25 -1
- package/src/components/schedules/schedule-sheet.tsx +44 -2
- package/src/components/secrets/secret-sheet.tsx +21 -2
- package/src/components/shared/agent-switch-dialog.tsx +12 -1
- package/src/components/shared/bottom-sheet.tsx +13 -3
- package/src/components/shared/command-palette.tsx +8 -1
- package/src/components/shared/confirm-dialog.tsx +19 -4
- package/src/components/shared/connector-platform-icon.test.ts +28 -0
- package/src/components/shared/connector-platform-icon.tsx +39 -6
- package/src/components/shared/settings/plugin-manager.tsx +29 -6
- package/src/components/shared/settings/section-capability-policy.tsx +45 -3
- package/src/components/shared/settings/section-voice.tsx +11 -3
- package/src/components/skills/skill-list.tsx +25 -0
- package/src/components/skills/skill-sheet.tsx +84 -12
- package/src/components/tasks/approvals-panel.tsx +289 -34
- package/src/components/tasks/task-board.tsx +410 -25
- package/src/components/tasks/task-card.tsx +66 -8
- package/src/components/tasks/task-sheet.tsx +16 -4
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
- package/src/components/wallets/wallet-panel.tsx +435 -90
- package/src/components/wallets/wallet-section.tsx +198 -48
- package/src/components/webhooks/webhook-sheet.tsx +22 -2
- package/src/lib/approval-display.ts +20 -0
- package/src/lib/canvas-content.ts +198 -0
- package/src/lib/chat-artifact-summary.ts +165 -0
- package/src/lib/chat-display.test.ts +91 -0
- package/src/lib/chat-display.ts +58 -0
- package/src/lib/chat-streaming-state.test.ts +47 -1
- package/src/lib/chat-streaming-state.ts +42 -0
- package/src/lib/ollama-model.ts +10 -0
- package/src/lib/openclaw-endpoint.test.ts +8 -0
- package/src/lib/openclaw-endpoint.ts +6 -1
- package/src/lib/plugin-install-cors.ts +46 -0
- package/src/lib/plugin-sources.test.ts +43 -0
- package/src/lib/plugin-sources.ts +77 -0
- package/src/lib/providers/ollama.ts +16 -6
- package/src/lib/providers/openclaw.test.ts +54 -0
- package/src/lib/providers/openclaw.ts +127 -11
- package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
- package/src/lib/schedule-dedupe.test.ts +66 -1
- package/src/lib/schedule-dedupe.ts +169 -12
- package/src/lib/schedule-origin.test.ts +20 -0
- package/src/lib/schedule-origin.ts +15 -0
- package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
- package/src/lib/server/agent-availability.ts +16 -0
- package/src/lib/server/agent-runtime-config.ts +12 -4
- package/src/lib/server/agent-thread-session.test.ts +51 -0
- package/src/lib/server/agent-thread-session.ts +7 -0
- package/src/lib/server/approval-match.ts +205 -0
- package/src/lib/server/approvals-auto-approve.test.ts +538 -1
- package/src/lib/server/approvals.ts +214 -1
- package/src/lib/server/assistant-control.test.ts +29 -0
- package/src/lib/server/assistant-control.ts +23 -0
- package/src/lib/server/build-llm.test.ts +79 -0
- package/src/lib/server/build-llm.ts +14 -4
- package/src/lib/server/canvas-content.test.ts +32 -0
- package/src/lib/server/canvas-content.ts +6 -0
- package/src/lib/server/capability-router.test.ts +33 -0
- package/src/lib/server/capability-router.ts +80 -19
- package/src/lib/server/chat-execution-advanced.test.ts +651 -0
- package/src/lib/server/chat-execution-disabled.test.ts +94 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
- package/src/lib/server/chat-execution.ts +378 -73
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +461 -137
- package/src/lib/server/connectors/pairing.ts +26 -5
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.test.ts +134 -0
- package/src/lib/server/connectors/whatsapp.ts +271 -47
- package/src/lib/server/context-manager.ts +6 -1
- package/src/lib/server/daemon-state.ts +84 -47
- package/src/lib/server/data-dir.test.ts +37 -0
- package/src/lib/server/data-dir.ts +20 -1
- package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
- package/src/lib/server/devserver-launch.test.ts +60 -0
- package/src/lib/server/devserver-launch.ts +85 -0
- package/src/lib/server/elevenlabs.test.ts +247 -1
- package/src/lib/server/elevenlabs.ts +147 -43
- package/src/lib/server/ethereum.ts +590 -0
- package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
- package/src/lib/server/eval/agent-regression.test.ts +18 -1
- package/src/lib/server/eval/agent-regression.ts +383 -11
- package/src/lib/server/evm-swap.ts +475 -0
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
- package/src/lib/server/heartbeat-service.ts +20 -11
- package/src/lib/server/heartbeat-wake.test.ts +112 -0
- package/src/lib/server/heartbeat-wake.ts +338 -57
- package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
- package/src/lib/server/main-agent-loop.test.ts +260 -0
- package/src/lib/server/main-agent-loop.ts +559 -14
- package/src/lib/server/mcp-client.test.ts +16 -0
- package/src/lib/server/mcp-client.ts +25 -0
- package/src/lib/server/memory-integration.test.ts +719 -0
- package/src/lib/server/memory-policy.test.ts +43 -0
- package/src/lib/server/memory-policy.ts +132 -0
- package/src/lib/server/memory-tiers.test.ts +60 -0
- package/src/lib/server/memory-tiers.ts +16 -0
- package/src/lib/server/ollama-runtime.ts +58 -0
- package/src/lib/server/openclaw-deploy.test.ts +109 -1
- package/src/lib/server/openclaw-deploy.ts +557 -81
- package/src/lib/server/openclaw-gateway.test.ts +131 -0
- package/src/lib/server/openclaw-gateway.ts +10 -4
- package/src/lib/server/openclaw-health.test.ts +35 -0
- package/src/lib/server/openclaw-health.ts +215 -47
- package/src/lib/server/orchestrator-lg.ts +3 -2
- package/src/lib/server/orchestrator.ts +2 -0
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +211 -6
- package/src/lib/server/project-context.ts +162 -0
- package/src/lib/server/project-utils.ts +150 -0
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +409 -2
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +527 -68
- package/src/lib/server/scheduler.ts +29 -1
- package/src/lib/server/session-note.test.ts +36 -0
- package/src/lib/server/session-note.ts +42 -0
- package/src/lib/server/session-run-manager.ts +83 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +366 -54
- package/src/lib/server/session-tools/context.ts +17 -3
- package/src/lib/server/session-tools/crud.ts +484 -84
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
- package/src/lib/server/session-tools/delegate.ts +102 -10
- package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
- package/src/lib/server/session-tools/discovery.ts +80 -12
- package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
- package/src/lib/server/session-tools/file.ts +43 -4
- package/src/lib/server/session-tools/human-loop.ts +35 -5
- package/src/lib/server/session-tools/index.ts +44 -9
- package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
- package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
- package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
- package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
- package/src/lib/server/session-tools/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +554 -75
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- package/src/lib/server/session-tools/platform-access.test.ts +58 -0
- package/src/lib/server/session-tools/platform.ts +60 -19
- package/src/lib/server/session-tools/plugin-creator.ts +57 -1
- package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
- package/src/lib/server/session-tools/schedule.ts +6 -1
- package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
- package/src/lib/server/session-tools/shell.ts +22 -3
- package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
- package/src/lib/server/session-tools/wallet.ts +1374 -139
- package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
- package/src/lib/server/session-tools/web.ts +621 -70
- package/src/lib/server/skill-discovery.ts +128 -0
- package/src/lib/server/skill-eligibility.test.ts +84 -0
- package/src/lib/server/skill-eligibility.ts +95 -0
- package/src/lib/server/skill-prompt-budget.test.ts +102 -0
- package/src/lib/server/skill-prompt-budget.ts +125 -0
- package/src/lib/server/skills-normalize.test.ts +54 -0
- package/src/lib/server/skills-normalize.ts +372 -26
- package/src/lib/server/solana.ts +214 -29
- package/src/lib/server/storage.ts +65 -36
- package/src/lib/server/stream-agent-chat.test.ts +437 -2
- package/src/lib/server/stream-agent-chat.ts +957 -79
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-aliases.ts +2 -0
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- package/src/lib/server/tool-capability-policy.test.ts +24 -0
- package/src/lib/server/tool-capability-policy.ts +29 -1
- package/src/lib/server/tool-loop-detection.test.ts +105 -0
- package/src/lib/server/tool-loop-detection.ts +260 -0
- package/src/lib/server/tool-planning.test.ts +44 -0
- package/src/lib/server/tool-planning.ts +271 -0
- package/src/lib/server/wallet-execution.test.ts +198 -0
- package/src/lib/server/wallet-portfolio.test.ts +98 -0
- package/src/lib/server/wallet-portfolio.ts +724 -0
- package/src/lib/server/wallet-service.test.ts +57 -0
- package/src/lib/server/wallet-service.ts +213 -0
- package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
- package/src/lib/server/watch-jobs.ts +17 -2
- package/src/lib/server/workspace-context.ts +111 -0
- package/src/lib/skill-save-payload.test.ts +39 -0
- package/src/lib/skill-save-payload.ts +37 -0
- package/src/lib/tasks.ts +28 -0
- package/src/lib/tool-definitions.ts +2 -1
- package/src/lib/tool-event-summary.test.ts +30 -0
- package/src/lib/tool-event-summary.ts +37 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/wallet-transactions.test.ts +75 -0
- package/src/lib/wallet-transactions.ts +43 -0
- package/src/lib/wallet.test.ts +17 -0
- package/src/lib/wallet.ts +183 -0
- package/src/proxy.test.ts +31 -0
- package/src/proxy.ts +34 -2
- package/src/stores/use-chat-store.ts +15 -1
- package/src/types/index.ts +249 -14
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
import { after, before, describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import type { Session } from '@/types'
|
|
7
|
+
|
|
8
|
+
const originalEnv = {
|
|
9
|
+
DATA_DIR: process.env.DATA_DIR,
|
|
10
|
+
WORKSPACE_DIR: process.env.WORKSPACE_DIR,
|
|
11
|
+
SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let tempDir = ''
|
|
15
|
+
let memDb: ReturnType<Awaited<typeof import('./memory-db')>['getMemoryDb']>
|
|
16
|
+
let executeMemoryAction: Awaited<typeof import('./session-tools/memory')>['executeMemoryAction']
|
|
17
|
+
let memoryPolicy: typeof import('./memory-policy')
|
|
18
|
+
|
|
19
|
+
before(async () => {
|
|
20
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-memory-int-'))
|
|
21
|
+
process.env.DATA_DIR = path.join(tempDir, 'data')
|
|
22
|
+
process.env.WORKSPACE_DIR = path.join(tempDir, 'workspace')
|
|
23
|
+
process.env.SWARMCLAW_BUILD_MODE = '1'
|
|
24
|
+
fs.mkdirSync(process.env.DATA_DIR, { recursive: true })
|
|
25
|
+
fs.mkdirSync(process.env.WORKSPACE_DIR, { recursive: true })
|
|
26
|
+
|
|
27
|
+
const memDbMod = await import('./memory-db')
|
|
28
|
+
memDb = memDbMod.getMemoryDb()
|
|
29
|
+
|
|
30
|
+
const memoryMod = await import('./session-tools/memory')
|
|
31
|
+
executeMemoryAction = memoryMod.executeMemoryAction
|
|
32
|
+
|
|
33
|
+
memoryPolicy = await import('./memory-policy')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
after(() => {
|
|
37
|
+
if (originalEnv.DATA_DIR === undefined) delete process.env.DATA_DIR
|
|
38
|
+
else process.env.DATA_DIR = originalEnv.DATA_DIR
|
|
39
|
+
if (originalEnv.WORKSPACE_DIR === undefined) delete process.env.WORKSPACE_DIR
|
|
40
|
+
else process.env.WORKSPACE_DIR = originalEnv.WORKSPACE_DIR
|
|
41
|
+
if (originalEnv.SWARMCLAW_BUILD_MODE === undefined) delete process.env.SWARMCLAW_BUILD_MODE
|
|
42
|
+
else process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
|
|
43
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// ─── Memory CRUD Lifecycle ──────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
describe('Memory CRUD lifecycle via executeMemoryAction', () => {
|
|
49
|
+
let storedId = ''
|
|
50
|
+
|
|
51
|
+
it('stores a memory and returns confirmation', async () => {
|
|
52
|
+
const result = await executeMemoryAction(
|
|
53
|
+
{ action: 'store', key: 'test-crud', value: 'CRUD content', category: 'note' },
|
|
54
|
+
{ agentId: 'agent-crud' },
|
|
55
|
+
)
|
|
56
|
+
assert.match(String(result), /Stored memory/)
|
|
57
|
+
const idMatch = String(result).match(/\(id: ([^)]+)\)/)
|
|
58
|
+
assert.ok(idMatch, 'result should contain an id')
|
|
59
|
+
storedId = idMatch[1]
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('gets the stored memory by id', async () => {
|
|
63
|
+
const result = await executeMemoryAction(
|
|
64
|
+
{ action: 'get', id: storedId },
|
|
65
|
+
{ agentId: 'agent-crud' },
|
|
66
|
+
)
|
|
67
|
+
assert.match(String(result), /test-crud/)
|
|
68
|
+
assert.match(String(result), /CRUD content/)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('searches for the memory by query', async () => {
|
|
72
|
+
const result = await executeMemoryAction(
|
|
73
|
+
{ action: 'search', query: 'CRUD content' },
|
|
74
|
+
{ agentId: 'agent-crud' },
|
|
75
|
+
)
|
|
76
|
+
assert.match(String(result), /CRUD content/)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('lists all memories and includes it', async () => {
|
|
80
|
+
const result = await executeMemoryAction(
|
|
81
|
+
{ action: 'list' },
|
|
82
|
+
{ agentId: 'agent-crud' },
|
|
83
|
+
)
|
|
84
|
+
assert.match(String(result), /test-crud/)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('updates title and content', async () => {
|
|
88
|
+
const result = await executeMemoryAction(
|
|
89
|
+
{ action: 'update', id: storedId, title: 'updated-title', value: 'updated-content' },
|
|
90
|
+
{ agentId: 'agent-crud' },
|
|
91
|
+
)
|
|
92
|
+
assert.match(String(result), /Updated memory/)
|
|
93
|
+
assert.match(String(result), /updated-title/)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('deletes and confirms gone', async () => {
|
|
97
|
+
const deleteResult = await executeMemoryAction(
|
|
98
|
+
{ action: 'delete', id: storedId },
|
|
99
|
+
{ agentId: 'agent-crud' },
|
|
100
|
+
)
|
|
101
|
+
assert.match(String(deleteResult), /Deleted/)
|
|
102
|
+
const getResult = await executeMemoryAction(
|
|
103
|
+
{ action: 'get', id: storedId },
|
|
104
|
+
{ agentId: 'agent-crud' },
|
|
105
|
+
)
|
|
106
|
+
assert.match(String(getResult), /not found|access denied/i)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('falls back to the latest user fact when store omits value', async () => {
|
|
110
|
+
const sessionContext: Partial<Session> = {
|
|
111
|
+
id: 'session-implicit',
|
|
112
|
+
name: 'Implicit store',
|
|
113
|
+
agentId: 'agent-crud',
|
|
114
|
+
messages: [
|
|
115
|
+
{ role: 'user', text: 'Remember this exactly: Project Kodiak uses amber-fox and the freeze date is April 21, 2026.', time: Date.now() },
|
|
116
|
+
],
|
|
117
|
+
}
|
|
118
|
+
const result = await executeMemoryAction(
|
|
119
|
+
{ action: 'store', key: 'implicit-fact-store' },
|
|
120
|
+
sessionContext,
|
|
121
|
+
)
|
|
122
|
+
assert.match(String(result), /Stored memory/)
|
|
123
|
+
|
|
124
|
+
const search = await executeMemoryAction(
|
|
125
|
+
{ action: 'search', query: 'amber-fox April 21 2026' },
|
|
126
|
+
{ agentId: 'agent-crud' },
|
|
127
|
+
)
|
|
128
|
+
assert.match(String(search), /April 21, 2026/)
|
|
129
|
+
assert.match(String(search), /amber-fox/)
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
// ─── Memory Linking & Graph ─────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
describe('Memory linking and graph', () => {
|
|
136
|
+
let idA = ''
|
|
137
|
+
let idB = ''
|
|
138
|
+
let idC = ''
|
|
139
|
+
|
|
140
|
+
before(async () => {
|
|
141
|
+
const a = memDb.add({ agentId: 'agent-link', category: 'note', title: 'Node A', content: 'alpha unique content' })
|
|
142
|
+
const b = memDb.add({ agentId: 'agent-link', category: 'note', title: 'Node B', content: 'beta unique content' })
|
|
143
|
+
const c = memDb.add({ agentId: 'agent-link', category: 'note', title: 'Node C', content: 'gamma unique content' })
|
|
144
|
+
idA = a.id
|
|
145
|
+
idB = b.id
|
|
146
|
+
idC = c.id
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('links A→B and B→C with bidirectional links', () => {
|
|
150
|
+
memDb.link(idA, [idB], true)
|
|
151
|
+
memDb.link(idB, [idC], true)
|
|
152
|
+
|
|
153
|
+
const a = memDb.get(idA)!
|
|
154
|
+
const b = memDb.get(idB)!
|
|
155
|
+
const c = memDb.get(idC)!
|
|
156
|
+
|
|
157
|
+
assert.ok(a.linkedMemoryIds?.includes(idB), 'A should link to B')
|
|
158
|
+
assert.ok(b.linkedMemoryIds?.includes(idA), 'B should link back to A')
|
|
159
|
+
assert.ok(b.linkedMemoryIds?.includes(idC), 'B should link to C')
|
|
160
|
+
assert.ok(c.linkedMemoryIds?.includes(idB), 'C should link back to B')
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('unlinks A→B bidirectionally', () => {
|
|
164
|
+
memDb.unlink(idA, [idB], true)
|
|
165
|
+
|
|
166
|
+
const a = memDb.get(idA)!
|
|
167
|
+
const b = memDb.get(idB)!
|
|
168
|
+
|
|
169
|
+
const aLinks = a.linkedMemoryIds || []
|
|
170
|
+
const bLinks = b.linkedMemoryIds || []
|
|
171
|
+
assert.ok(!aLinks.includes(idB), 'A should no longer link to B')
|
|
172
|
+
assert.ok(!bLinks.includes(idA), 'B should no longer link to A')
|
|
173
|
+
// B↔C should still exist
|
|
174
|
+
assert.ok(bLinks.includes(idC), 'B should still link to C')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('deleting C cleans up B linkedMemoryIds', () => {
|
|
178
|
+
memDb.delete(idC)
|
|
179
|
+
const b = memDb.get(idB)!
|
|
180
|
+
const bLinks = b.linkedMemoryIds || []
|
|
181
|
+
assert.ok(!bLinks.includes(idC), 'B should no longer reference deleted C')
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
// ─── Scope Filtering ────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
describe('Scope filtering', () => {
|
|
188
|
+
before(() => {
|
|
189
|
+
memDb.add({ agentId: 'agent-a', category: 'note', title: 'A-only', content: 'scope test agent a' })
|
|
190
|
+
memDb.add({ agentId: 'agent-b', category: 'note', title: 'B-only', content: 'scope test agent b' })
|
|
191
|
+
memDb.add({ agentId: null, category: 'note', title: 'Shared global', content: 'scope test global' })
|
|
192
|
+
memDb.add({ agentId: 'agent-c', category: 'note', title: 'Shared with B', content: 'scope shared with b', sharedWith: ['agent-b'] })
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('agent scope shows only that agent memories', async () => {
|
|
196
|
+
const result = await executeMemoryAction(
|
|
197
|
+
{ action: 'list', scope: 'agent' },
|
|
198
|
+
{ agentId: 'agent-a' },
|
|
199
|
+
)
|
|
200
|
+
assert.match(String(result), /A-only/)
|
|
201
|
+
assert.doesNotMatch(String(result), /B-only/)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('global scope shows only shared memories (no agentId)', async () => {
|
|
205
|
+
const result = await executeMemoryAction(
|
|
206
|
+
{ action: 'list', scope: 'global' },
|
|
207
|
+
{ agentId: 'agent-a' },
|
|
208
|
+
)
|
|
209
|
+
assert.match(String(result), /Shared global/)
|
|
210
|
+
assert.doesNotMatch(String(result), /A-only/)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('sharedWith memories visible to target agent in agent scope', async () => {
|
|
214
|
+
const result = await executeMemoryAction(
|
|
215
|
+
{ action: 'list', scope: 'agent' },
|
|
216
|
+
{ agentId: 'agent-b' },
|
|
217
|
+
)
|
|
218
|
+
assert.match(String(result), /Shared with B/)
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
describe('Search source filtering', () => {
|
|
223
|
+
before(() => {
|
|
224
|
+
memDb.add({
|
|
225
|
+
agentId: 'agent-source-filter',
|
|
226
|
+
category: 'projects/decisions',
|
|
227
|
+
title: 'Kodiak durable fact',
|
|
228
|
+
content: 'Project Kodiak uses amber-fox and the freeze date is April 21, 2026.',
|
|
229
|
+
})
|
|
230
|
+
memDb.add({
|
|
231
|
+
agentId: 'agent-source-filter',
|
|
232
|
+
sessionId: 'archive-session-1',
|
|
233
|
+
category: 'session_archive',
|
|
234
|
+
title: 'Session archive: kodiak stale',
|
|
235
|
+
content: 'Transcript excerpt: Project Kodiak freeze date was April 18, 2026.',
|
|
236
|
+
metadata: { tier: 'archive' },
|
|
237
|
+
})
|
|
238
|
+
memDb.add({
|
|
239
|
+
agentId: 'agent-source-filter',
|
|
240
|
+
category: 'operations/execution',
|
|
241
|
+
title: 'Auto execution note',
|
|
242
|
+
content: 'assistant_outcome: during a previous run I mentioned April 18, 2026 while fixing Project Kodiak memory.',
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('search defaults to durable memories', async () => {
|
|
247
|
+
const result = await executeMemoryAction(
|
|
248
|
+
{ action: 'search', query: 'Project Kodiak amber-fox freeze date' },
|
|
249
|
+
{ agentId: 'agent-source-filter', sessionId: 'agent-source-filter', messages: [] },
|
|
250
|
+
)
|
|
251
|
+
assert.match(String(result), /Kodiak durable fact/)
|
|
252
|
+
assert.doesNotMatch(String(result), /Session archive: kodiak stale/)
|
|
253
|
+
assert.doesNotMatch(String(result), /Auto execution note/)
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('search can explicitly include archives and working memories', async () => {
|
|
257
|
+
const archiveResult = await executeMemoryAction(
|
|
258
|
+
{ action: 'search', query: 'Project Kodiak freeze date', sources: ['durable', 'archive', 'working'] },
|
|
259
|
+
{ agentId: 'agent-source-filter', sessionId: 'archive-session-1', messages: [] },
|
|
260
|
+
)
|
|
261
|
+
assert.match(String(archiveResult), /Kodiak durable fact/)
|
|
262
|
+
assert.match(String(archiveResult), /Session archive: kodiak stale/)
|
|
263
|
+
|
|
264
|
+
const workingResult = await executeMemoryAction(
|
|
265
|
+
{ action: 'search', query: 'assistant_outcome previous run April 18, 2026', sources: ['working'] },
|
|
266
|
+
{ agentId: 'agent-source-filter', sessionId: 'archive-session-1', messages: [] },
|
|
267
|
+
)
|
|
268
|
+
assert.match(String(workingResult), /Auto execution note/)
|
|
269
|
+
})
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
describe('Canonical memory correction', () => {
|
|
273
|
+
it('update without an explicit id resolves and corrects the canonical durable memory', async () => {
|
|
274
|
+
const stale = memDb.add({
|
|
275
|
+
agentId: 'agent-canonical',
|
|
276
|
+
category: 'projects/decisions',
|
|
277
|
+
title: 'Project Kodiak codename and freeze date',
|
|
278
|
+
content: 'Project Kodiak uses the codename "amber-fox" and the freeze date is April 18, 2026.',
|
|
279
|
+
})
|
|
280
|
+
memDb.add({
|
|
281
|
+
agentId: 'agent-canonical',
|
|
282
|
+
category: 'note',
|
|
283
|
+
title: '[auto-consolidated] Project Kodiak note',
|
|
284
|
+
content: 'Stored earlier: Project Kodiak codename amber-fox freeze date April 18, 2026.',
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
const result = await executeMemoryAction(
|
|
288
|
+
{
|
|
289
|
+
action: 'update',
|
|
290
|
+
title: 'Project Kodiak freeze date correction',
|
|
291
|
+
value: 'Project Kodiak uses the codename "amber-fox" and the freeze date is April 21, 2026.',
|
|
292
|
+
},
|
|
293
|
+
{ agentId: 'agent-canonical', sessionId: 'agent-canonical', messages: [] },
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
assert.match(String(result), /Updated memory/)
|
|
297
|
+
const corrected = memDb.get(stale.id)
|
|
298
|
+
assert.ok(corrected)
|
|
299
|
+
assert.match(String(corrected?.content), /April 21, 2026/)
|
|
300
|
+
|
|
301
|
+
const recall = await executeMemoryAction(
|
|
302
|
+
{ action: 'search', query: 'Project Kodiak amber-fox freeze date' },
|
|
303
|
+
{ agentId: 'agent-canonical', sessionId: 'agent-canonical', messages: [] },
|
|
304
|
+
)
|
|
305
|
+
assert.match(String(recall), /April 21, 2026/)
|
|
306
|
+
assert.doesNotMatch(String(recall), /auto-consolidated/i)
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it('store merges into an existing canonical durable memory instead of appending a conflicting duplicate', async () => {
|
|
310
|
+
const base = memDb.add({
|
|
311
|
+
agentId: 'agent-canonical-store',
|
|
312
|
+
category: 'projects/context',
|
|
313
|
+
title: 'Project Kodiak details',
|
|
314
|
+
content: 'Project Kodiak: codename amber-fox, freeze date April 18 2026',
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
const result = await executeMemoryAction(
|
|
318
|
+
{
|
|
319
|
+
action: 'store',
|
|
320
|
+
title: 'Project Kodiak details',
|
|
321
|
+
value: 'Project Kodiak: codename amber-fox, freeze date April 21 2026',
|
|
322
|
+
category: 'projects/context',
|
|
323
|
+
},
|
|
324
|
+
{ agentId: 'agent-canonical-store', sessionId: 'agent-canonical-store', messages: [] },
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
assert.match(String(result), /updating the canonical entry/i)
|
|
328
|
+
const updated = memDb.get(base.id)
|
|
329
|
+
assert.ok(updated)
|
|
330
|
+
assert.match(String(updated?.content), /April 21 2026/)
|
|
331
|
+
|
|
332
|
+
const durableRows = memDb.list('agent-canonical-store', 20)
|
|
333
|
+
.filter((entry) => /Project Kodiak/.test(`${entry.title} ${entry.content}`))
|
|
334
|
+
.filter((entry) => entry.category !== 'session_archive')
|
|
335
|
+
assert.equal(durableRows.filter((entry) => entry.id === base.id).length, 1)
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
it('parses structured JSON payloads that arrive inside query or value fields', async () => {
|
|
339
|
+
const base = memDb.add({
|
|
340
|
+
agentId: 'agent-structured-payload',
|
|
341
|
+
category: 'projects/decisions',
|
|
342
|
+
title: 'Project Kodiak codename and freeze date',
|
|
343
|
+
content: 'Project Kodiak uses the codename "amber-fox" and the freeze date is April 18, 2026.',
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
const result = await executeMemoryAction(
|
|
347
|
+
{
|
|
348
|
+
action: 'update',
|
|
349
|
+
query: JSON.stringify({
|
|
350
|
+
title: 'Project Kodiak codename and freeze date',
|
|
351
|
+
category: 'projects/decisions',
|
|
352
|
+
content: 'Project Kodiak uses the codename "amber-fox" and the freeze date is April 21, 2026.',
|
|
353
|
+
}),
|
|
354
|
+
},
|
|
355
|
+
{ agentId: 'agent-structured-payload', sessionId: 'agent-structured-payload', messages: [] },
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
assert.match(String(result), /Updated memory/)
|
|
359
|
+
const updated = memDb.get(base.id)
|
|
360
|
+
assert.ok(updated)
|
|
361
|
+
assert.match(String(updated?.content), /April 21, 2026/)
|
|
362
|
+
assert.doesNotMatch(String(updated?.content), /"title"/)
|
|
363
|
+
})
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
// ─── Pinned Memories ────────────────────────────────────────────────
|
|
367
|
+
|
|
368
|
+
describe('Pinned memories', () => {
|
|
369
|
+
before(() => {
|
|
370
|
+
memDb.add({ agentId: 'agent-pin', category: 'note', title: 'Normal 1', content: 'not pinned one', pinned: false })
|
|
371
|
+
memDb.add({ agentId: 'agent-pin', category: 'note', title: 'Normal 2', content: 'not pinned two', pinned: false })
|
|
372
|
+
memDb.add({ agentId: 'agent-pin', category: 'note', title: 'Normal 3', content: 'not pinned three', pinned: false })
|
|
373
|
+
memDb.add({ agentId: 'agent-pin', category: 'note', title: 'Pinned 1', content: 'pinned content one', pinned: true })
|
|
374
|
+
memDb.add({ agentId: 'agent-pin', category: 'note', title: 'Pinned 2', content: 'pinned content two', pinned: true })
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
it('listPinned returns only pinned memories', () => {
|
|
378
|
+
const pinned = memDb.listPinned('agent-pin')
|
|
379
|
+
assert.ok(pinned.length >= 2, `expected at least 2 pinned, got ${pinned.length}`)
|
|
380
|
+
for (const entry of pinned) {
|
|
381
|
+
assert.ok(entry.pinned, `entry "${entry.title}" should be pinned`)
|
|
382
|
+
}
|
|
383
|
+
})
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
// ─── Category Normalization ─────────────────────────────────────────
|
|
387
|
+
|
|
388
|
+
describe('Category normalization (comprehensive)', () => {
|
|
389
|
+
const norm = (cat: string, title?: string, content?: string) =>
|
|
390
|
+
memoryPolicy.normalizeMemoryCategory(cat, title ?? null, content ?? null)
|
|
391
|
+
|
|
392
|
+
it('maps flat categories to hierarchical', () => {
|
|
393
|
+
assert.equal(norm('preference'), 'identity/preferences')
|
|
394
|
+
assert.equal(norm('decision'), 'projects/decisions')
|
|
395
|
+
assert.equal(norm('error'), 'execution/errors')
|
|
396
|
+
assert.equal(norm('project'), 'projects/context')
|
|
397
|
+
assert.equal(norm('learning'), 'projects/learnings')
|
|
398
|
+
assert.equal(norm('breadcrumb'), 'operations/execution')
|
|
399
|
+
assert.equal(norm('fact'), 'knowledge/facts')
|
|
400
|
+
assert.equal(norm('working'), 'working/scratch')
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
it('infers category from content when explicit is "note"', () => {
|
|
404
|
+
assert.equal(norm('note', 'user prefers dark mode', ''), 'identity/preferences')
|
|
405
|
+
assert.equal(norm('note', 'decided to ship Docker', ''), 'projects/decisions')
|
|
406
|
+
assert.equal(norm('note', 'root cause was a null pointer', ''), 'projects/learnings')
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
it('passes through already-hierarchical categories', () => {
|
|
410
|
+
assert.equal(norm('identity/profile'), 'identity/profile')
|
|
411
|
+
assert.equal(norm('custom/bucket'), 'custom/bucket')
|
|
412
|
+
})
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
// ─── Memory Doctor Report ───────────────────────────────────────────
|
|
416
|
+
|
|
417
|
+
describe('Memory doctor report', () => {
|
|
418
|
+
it('builds report with correct counts', () => {
|
|
419
|
+
const entries = [
|
|
420
|
+
{ id: '1', agentId: 'a', category: 'identity/preferences', title: '', content: '', pinned: true, linkedMemoryIds: ['2'], createdAt: 0, updatedAt: 0 },
|
|
421
|
+
{ id: '2', agentId: 'a', category: 'projects/decisions', title: '', content: '', pinned: false, linkedMemoryIds: ['1'], sharedWith: ['b'], createdAt: 0, updatedAt: 0 },
|
|
422
|
+
{ id: '3', agentId: 'a', category: 'knowledge/facts', title: '', content: '', pinned: true, createdAt: 0, updatedAt: 0 },
|
|
423
|
+
{ id: '4', agentId: null, category: 'operations/execution', title: '', content: '', pinned: false, sharedWith: ['a'], createdAt: 0, updatedAt: 0 },
|
|
424
|
+
] as unknown as import('@/types').MemoryEntry[]
|
|
425
|
+
|
|
426
|
+
const report = memoryPolicy.buildMemoryDoctorReport(entries, 'a')
|
|
427
|
+
assert.match(report, /Visible memories: 4/)
|
|
428
|
+
assert.match(report, /Pinned: 2/)
|
|
429
|
+
assert.match(report, /Linked: 2/)
|
|
430
|
+
assert.match(report, /Shared: 2/)
|
|
431
|
+
assert.match(report, /identity/)
|
|
432
|
+
assert.match(report, /projects/)
|
|
433
|
+
assert.match(report, /knowledge/)
|
|
434
|
+
assert.match(report, /operations/)
|
|
435
|
+
})
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
// ─── Auto-capture Policy ────────────────────────────────────────────
|
|
439
|
+
|
|
440
|
+
describe('Auto-capture policy', () => {
|
|
441
|
+
it('shouldInjectMemoryContext: short ack → false', () => {
|
|
442
|
+
assert.equal(memoryPolicy.shouldInjectMemoryContext('ok'), false)
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
it('shouldInjectMemoryContext: greeting → false', () => {
|
|
446
|
+
assert.equal(memoryPolicy.shouldInjectMemoryContext('hello'), false)
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
it('shouldInjectMemoryContext: short memory meta → false', () => {
|
|
450
|
+
assert.equal(memoryPolicy.shouldInjectMemoryContext('remember this'), false)
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
it('shouldInjectMemoryContext: substantive message → true', () => {
|
|
454
|
+
assert.equal(
|
|
455
|
+
memoryPolicy.shouldInjectMemoryContext('Compare the current deployment plan with what we decided yesterday'),
|
|
456
|
+
true,
|
|
457
|
+
)
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
it('shouldAutoCaptureMemoryTurn: short messages → false', () => {
|
|
461
|
+
assert.equal(memoryPolicy.shouldAutoCaptureMemoryTurn('hi', 'hello!'), false)
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
it('shouldAutoCaptureMemoryTurn: ack + response → false', () => {
|
|
465
|
+
assert.equal(
|
|
466
|
+
memoryPolicy.shouldAutoCaptureMemoryTurn('thanks', 'You are welcome, happy to help with that!'),
|
|
467
|
+
false,
|
|
468
|
+
)
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
it('shouldAutoCaptureMemoryTurn: error response → false', () => {
|
|
472
|
+
assert.equal(
|
|
473
|
+
memoryPolicy.shouldAutoCaptureMemoryTurn(
|
|
474
|
+
'Please deploy the production environment now with all the settings',
|
|
475
|
+
"sorry, I can't do that because I don't have the credentials needed.",
|
|
476
|
+
),
|
|
477
|
+
false,
|
|
478
|
+
)
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
it('shouldAutoCaptureMemoryTurn: substantive exchange → true', () => {
|
|
482
|
+
assert.equal(
|
|
483
|
+
memoryPolicy.shouldAutoCaptureMemoryTurn(
|
|
484
|
+
'We decided to use the shared staging environment and keep the worker count at 2 for now.',
|
|
485
|
+
'Decision captured: shared staging, worker count 2, and we will revisit after load testing next week.',
|
|
486
|
+
),
|
|
487
|
+
true,
|
|
488
|
+
)
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
it('shouldAutoCaptureMemoryTurn: HEARTBEAT_OK response → false', () => {
|
|
492
|
+
assert.equal(
|
|
493
|
+
memoryPolicy.shouldAutoCaptureMemoryTurn(
|
|
494
|
+
'This is a real substantive question about the project and architecture',
|
|
495
|
+
'HEARTBEAT_OK all systems nominal',
|
|
496
|
+
),
|
|
497
|
+
false,
|
|
498
|
+
)
|
|
499
|
+
})
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
// ─── inferAutomaticMemoryCategory ───────────────────────────────────
|
|
503
|
+
|
|
504
|
+
describe('inferAutomaticMemoryCategory', () => {
|
|
505
|
+
it('infers identity/preferences from preference-like content', () => {
|
|
506
|
+
assert.equal(
|
|
507
|
+
memoryPolicy.inferAutomaticMemoryCategory('user prefers dark mode', 'noted'),
|
|
508
|
+
'identity/preferences',
|
|
509
|
+
)
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
it('infers projects/decisions from decision-like content', () => {
|
|
513
|
+
assert.equal(
|
|
514
|
+
memoryPolicy.inferAutomaticMemoryCategory('decided to ship Docker first', 'locked in'),
|
|
515
|
+
'projects/decisions',
|
|
516
|
+
)
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
it('infers projects/learnings from learning-like content', () => {
|
|
520
|
+
assert.equal(
|
|
521
|
+
memoryPolicy.inferAutomaticMemoryCategory('root cause was a null pointer bug', 'fixed now'),
|
|
522
|
+
'projects/learnings',
|
|
523
|
+
)
|
|
524
|
+
})
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
// ─── Memory Deduplication ───────────────────────────────────────────
|
|
528
|
+
|
|
529
|
+
describe('Memory deduplication via contentHash', () => {
|
|
530
|
+
it('storing same content twice reinforces instead of duplicating', () => {
|
|
531
|
+
const first = memDb.add({ agentId: 'agent-dedup', category: 'note', title: 'Dup test', content: 'exact duplicate content for dedup test' })
|
|
532
|
+
const second = memDb.add({ agentId: 'agent-dedup', category: 'note', title: 'Dup test', content: 'exact duplicate content for dedup test' })
|
|
533
|
+
assert.equal(first.id, second.id, 'second add should return same id')
|
|
534
|
+
assert.ok((second.reinforcementCount ?? 0) >= 1, 'reinforcement count should be bumped')
|
|
535
|
+
})
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
// ─── Unknown Action ─────────────────────────────────────────────────
|
|
539
|
+
|
|
540
|
+
describe('Unknown action', () => {
|
|
541
|
+
it('returns unknown action message', async () => {
|
|
542
|
+
const result = await executeMemoryAction({ action: 'invalid' }, null)
|
|
543
|
+
assert.match(String(result), /Unknown action/)
|
|
544
|
+
})
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
// ─── Edge Cases ─────────────────────────────────────────────────────
|
|
548
|
+
|
|
549
|
+
describe('Edge cases', () => {
|
|
550
|
+
it('store with empty value is rejected when no fallback fact exists', async () => {
|
|
551
|
+
const result = await executeMemoryAction(
|
|
552
|
+
{ action: 'store', key: 'empty-val', value: '', category: 'note' },
|
|
553
|
+
{ agentId: 'agent-edge' },
|
|
554
|
+
)
|
|
555
|
+
assert.match(String(result), /requires a non-empty value/i)
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
it('store with missing key defaults title to Untitled', async () => {
|
|
559
|
+
const result = await executeMemoryAction(
|
|
560
|
+
{ action: 'store', value: 'some content without key', category: 'note' },
|
|
561
|
+
{ agentId: 'agent-edge' },
|
|
562
|
+
)
|
|
563
|
+
assert.match(String(result), /Stored memory/)
|
|
564
|
+
assert.match(String(result), /Untitled/)
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
it('store with null context still works', async () => {
|
|
568
|
+
const result = await executeMemoryAction(
|
|
569
|
+
{ action: 'store', key: 'null-ctx', value: 'null context test', category: 'note' },
|
|
570
|
+
null,
|
|
571
|
+
)
|
|
572
|
+
assert.match(String(result), /Stored memory/)
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
it('store with imagePath that does not exist still stores', async () => {
|
|
576
|
+
const result = await executeMemoryAction(
|
|
577
|
+
{ action: 'store', key: 'no-image', value: 'image missing', category: 'note', imagePath: '/tmp/nonexistent-image.png' },
|
|
578
|
+
{ agentId: 'agent-edge' },
|
|
579
|
+
)
|
|
580
|
+
assert.match(String(result), /Stored memory/)
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
it('update non-existent memory → not found', async () => {
|
|
584
|
+
const result = await executeMemoryAction(
|
|
585
|
+
{ action: 'update', id: 'nonexistent-id-xyz', value: 'updated' },
|
|
586
|
+
{ agentId: 'agent-edge' },
|
|
587
|
+
)
|
|
588
|
+
assert.match(String(result), /not found/i)
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
it('get with non-existent id → not found', async () => {
|
|
592
|
+
const result = await executeMemoryAction(
|
|
593
|
+
{ action: 'get', id: 'missing-id-abc' },
|
|
594
|
+
{ agentId: 'agent-edge' },
|
|
595
|
+
)
|
|
596
|
+
assert.match(String(result), /not found/i)
|
|
597
|
+
})
|
|
598
|
+
|
|
599
|
+
it('link requires targetIds', async () => {
|
|
600
|
+
const entry = memDb.add({ agentId: 'agent-edge', category: 'note', title: 'Link test', content: 'link target test' })
|
|
601
|
+
const result = await executeMemoryAction(
|
|
602
|
+
{ action: 'link', id: entry.id },
|
|
603
|
+
{ agentId: 'agent-edge' },
|
|
604
|
+
)
|
|
605
|
+
assert.match(String(result), /requires targetIds/i)
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
it('unlink requires targetIds', async () => {
|
|
609
|
+
const entry = memDb.add({ agentId: 'agent-edge', category: 'note', title: 'Unlink test', content: 'unlink target test' })
|
|
610
|
+
const result = await executeMemoryAction(
|
|
611
|
+
{ action: 'unlink', id: entry.id },
|
|
612
|
+
{ agentId: 'agent-edge' },
|
|
613
|
+
)
|
|
614
|
+
assert.match(String(result), /requires targetIds/i)
|
|
615
|
+
})
|
|
616
|
+
|
|
617
|
+
it('delete non-existent memory → not found', async () => {
|
|
618
|
+
const result = await executeMemoryAction(
|
|
619
|
+
{ action: 'delete', id: 'phantom-id-999' },
|
|
620
|
+
{ agentId: 'agent-edge' },
|
|
621
|
+
)
|
|
622
|
+
assert.match(String(result), /not found/i)
|
|
623
|
+
})
|
|
624
|
+
})
|
|
625
|
+
|
|
626
|
+
// ─── Doctor via executeMemoryAction ─────────────────────────────────
|
|
627
|
+
|
|
628
|
+
describe('Doctor action via executeMemoryAction', () => {
|
|
629
|
+
it('returns a doctor report', async () => {
|
|
630
|
+
const result = await executeMemoryAction(
|
|
631
|
+
{ action: 'doctor' },
|
|
632
|
+
{ agentId: 'agent-crud' },
|
|
633
|
+
)
|
|
634
|
+
assert.match(String(result), /Memory Doctor/)
|
|
635
|
+
assert.match(String(result), /Visible memories/)
|
|
636
|
+
})
|
|
637
|
+
})
|
|
638
|
+
|
|
639
|
+
// ─── Direct memDb CRUD ──────────────────────────────────────────────
|
|
640
|
+
|
|
641
|
+
describe('Direct memDb CRUD', () => {
|
|
642
|
+
it('add, get, update, delete cycle', () => {
|
|
643
|
+
const entry = memDb.add({ agentId: 'direct-agent', category: 'note', title: 'Direct test', content: 'direct content' })
|
|
644
|
+
assert.ok(entry.id)
|
|
645
|
+
assert.equal(entry.title, 'Direct test')
|
|
646
|
+
|
|
647
|
+
const fetched = memDb.get(entry.id)
|
|
648
|
+
assert.ok(fetched)
|
|
649
|
+
assert.equal(fetched.content, 'direct content')
|
|
650
|
+
|
|
651
|
+
const updated = memDb.update(entry.id, { title: 'Updated direct', content: 'updated direct content' })
|
|
652
|
+
assert.ok(updated)
|
|
653
|
+
assert.equal(updated.title, 'Updated direct')
|
|
654
|
+
|
|
655
|
+
memDb.delete(entry.id)
|
|
656
|
+
const gone = memDb.get(entry.id)
|
|
657
|
+
assert.equal(gone, null)
|
|
658
|
+
})
|
|
659
|
+
|
|
660
|
+
it('list returns entries and respects updatedAt ordering', () => {
|
|
661
|
+
const a = memDb.add({ agentId: 'list-agent', category: 'note', title: 'First', content: 'first entry for list test' })
|
|
662
|
+
const b = memDb.add({ agentId: 'list-agent', category: 'note', title: 'Second', content: 'second entry for list test' })
|
|
663
|
+
const entries = memDb.list('list-agent', 10)
|
|
664
|
+
assert.ok(entries.length >= 2, 'should list at least 2 entries')
|
|
665
|
+
assert.ok(entries.some((e) => e.id === a.id), 'should include entry a')
|
|
666
|
+
assert.ok(entries.some((e) => e.id === b.id), 'should include entry b')
|
|
667
|
+
// Verify entries are sorted by updatedAt descending (ties allowed)
|
|
668
|
+
for (let i = 1; i < entries.length; i++) {
|
|
669
|
+
assert.ok(entries[i - 1].updatedAt >= entries[i].updatedAt, 'list should be ordered by updatedAt desc')
|
|
670
|
+
}
|
|
671
|
+
})
|
|
672
|
+
|
|
673
|
+
it('search via FTS finds matching entries', () => {
|
|
674
|
+
memDb.add({ agentId: 'search-agent', category: 'note', title: 'Kubernetes deployment', content: 'helm chart configuration for kubernetes cluster' })
|
|
675
|
+
const results = memDb.search('kubernetes helm chart', 'search-agent')
|
|
676
|
+
assert.ok(results.length >= 1, 'FTS should find the kubernetes entry')
|
|
677
|
+
assert.ok(results.some((r) => r.title === 'Kubernetes deployment'))
|
|
678
|
+
})
|
|
679
|
+
|
|
680
|
+
it('update returns null for non-existent id', () => {
|
|
681
|
+
const result = memDb.update('missing-xyz', { title: 'no' })
|
|
682
|
+
assert.equal(result, null)
|
|
683
|
+
})
|
|
684
|
+
})
|
|
685
|
+
|
|
686
|
+
// ─── Link and Unlink via executeMemoryAction ────────────────────────
|
|
687
|
+
|
|
688
|
+
describe('Link and unlink via executeMemoryAction', () => {
|
|
689
|
+
let id1 = ''
|
|
690
|
+
let id2 = ''
|
|
691
|
+
|
|
692
|
+
before(() => {
|
|
693
|
+
const entry1 = memDb.add({ agentId: 'agent-act-link', category: 'note', title: 'Link A', content: 'link action A' })
|
|
694
|
+
const entry2 = memDb.add({ agentId: 'agent-act-link', category: 'note', title: 'Link B', content: 'link action B' })
|
|
695
|
+
id1 = entry1.id
|
|
696
|
+
id2 = entry2.id
|
|
697
|
+
})
|
|
698
|
+
|
|
699
|
+
it('links memories via action', async () => {
|
|
700
|
+
const result = await executeMemoryAction(
|
|
701
|
+
{ action: 'link', id: id1, targetIds: [id2] },
|
|
702
|
+
{ agentId: 'agent-act-link' },
|
|
703
|
+
)
|
|
704
|
+
assert.match(String(result), /Linked/)
|
|
705
|
+
const entry = memDb.get(id1)!
|
|
706
|
+
assert.ok(entry.linkedMemoryIds?.includes(id2))
|
|
707
|
+
})
|
|
708
|
+
|
|
709
|
+
it('unlinks memories via action', async () => {
|
|
710
|
+
const result = await executeMemoryAction(
|
|
711
|
+
{ action: 'unlink', id: id1, targetIds: [id2] },
|
|
712
|
+
{ agentId: 'agent-act-link' },
|
|
713
|
+
)
|
|
714
|
+
assert.match(String(result), /Unlinked/)
|
|
715
|
+
const entry = memDb.get(id1)!
|
|
716
|
+
const links = entry.linkedMemoryIds || []
|
|
717
|
+
assert.ok(!links.includes(id2))
|
|
718
|
+
})
|
|
719
|
+
})
|