@swarmclawai/swarmclaw 0.7.8 → 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 -15
- 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 +22 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +26 -1
- 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/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/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 +73 -24
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +44 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- 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/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/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 +7 -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 +191 -95
- package/src/components/tasks/task-board.tsx +273 -2
- package/src/components/tasks/task-card.tsx +38 -9
- 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 +11 -0
- package/src/lib/server/capability-router.ts +26 -1
- 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 +353 -72
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +362 -63
- 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 +1 -1
- 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 +189 -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 +15 -10
- 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/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 +2 -2
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +205 -5
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +262 -0
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +293 -61
- 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 +52 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +348 -61
- package/src/lib/server/session-tools/context.ts +12 -3
- package/src/lib/server/session-tools/crud.ts +221 -10
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate.ts +64 -8
- 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/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +546 -79
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- 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 +162 -1
- package/src/lib/server/session-tools/web.ts +468 -64
- 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 +419 -9
- package/src/lib/server/stream-agent-chat.ts +887 -83
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- 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.ts +4 -2
- 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-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 +210 -14
|
@@ -0,0 +1,1147 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { spawnSync } from 'node:child_process'
|
|
6
|
+
import { describe, it } from 'node:test'
|
|
7
|
+
|
|
8
|
+
import { sanitizeConnectorOutboundContent } from './manager'
|
|
9
|
+
|
|
10
|
+
const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../../..')
|
|
11
|
+
|
|
12
|
+
function runWithTempDataDir(script: string) {
|
|
13
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-manager-test-'))
|
|
14
|
+
try {
|
|
15
|
+
const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
|
|
16
|
+
cwd: repoRoot,
|
|
17
|
+
env: {
|
|
18
|
+
...process.env,
|
|
19
|
+
DATA_DIR: tempDir,
|
|
20
|
+
WORKSPACE_DIR: path.join(tempDir, 'workspace'),
|
|
21
|
+
},
|
|
22
|
+
encoding: 'utf-8',
|
|
23
|
+
})
|
|
24
|
+
assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
|
|
25
|
+
const lines = (result.stdout || '')
|
|
26
|
+
.trim()
|
|
27
|
+
.split('\n')
|
|
28
|
+
.map((line) => line.trim())
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
|
|
31
|
+
return JSON.parse(jsonLine || '{}')
|
|
32
|
+
} finally {
|
|
33
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('sanitizeConnectorOutboundContent', () => {
|
|
38
|
+
it('strips hidden control tokens from captions without suppressing the media send itself', () => {
|
|
39
|
+
assert.deepEqual(
|
|
40
|
+
sanitizeConnectorOutboundContent({
|
|
41
|
+
text: 'Here is the attachment',
|
|
42
|
+
caption: 'NO_MESSAGE',
|
|
43
|
+
}),
|
|
44
|
+
{
|
|
45
|
+
sanitizedText: 'Here is the attachment',
|
|
46
|
+
suppressHiddenText: false,
|
|
47
|
+
sanitizedCaptionText: '',
|
|
48
|
+
sanitizedCaption: undefined,
|
|
49
|
+
},
|
|
50
|
+
)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('suppresses pure hidden-control text payloads', () => {
|
|
54
|
+
assert.deepEqual(
|
|
55
|
+
sanitizeConnectorOutboundContent({
|
|
56
|
+
text: 'HEARTBEAT_OK',
|
|
57
|
+
caption: 'Looks good',
|
|
58
|
+
}),
|
|
59
|
+
{
|
|
60
|
+
sanitizedText: '',
|
|
61
|
+
suppressHiddenText: true,
|
|
62
|
+
sanitizedCaptionText: 'Looks good',
|
|
63
|
+
sanitizedCaption: 'Looks good',
|
|
64
|
+
},
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('mirrors direct WhatsApp inbound and assistant replies into the session transcript', () => {
|
|
69
|
+
const output = runWithTempDataDir(`
|
|
70
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
71
|
+
const managerMod = await import('./src/lib/server/connectors/manager.ts')
|
|
72
|
+
const providersMod = await import('./src/lib/providers/index.ts')
|
|
73
|
+
const storage = storageMod.default || storageMod
|
|
74
|
+
const manager = managerMod.default || managerMod
|
|
75
|
+
const providers = providersMod.default || providersMod
|
|
76
|
+
|
|
77
|
+
const now = Date.now()
|
|
78
|
+
providers.PROVIDERS['test-provider'] = {
|
|
79
|
+
id: 'test-provider',
|
|
80
|
+
name: 'Test Provider',
|
|
81
|
+
models: ['test-model'],
|
|
82
|
+
requiresApiKey: false,
|
|
83
|
+
requiresEndpoint: false,
|
|
84
|
+
handler: {
|
|
85
|
+
streamChat: async (opts) => {
|
|
86
|
+
opts.write('data: ' + JSON.stringify({ t: 'r', text: 'Roger that via WhatsApp' }) + '\\n')
|
|
87
|
+
return ''
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
storage.saveSettings({})
|
|
93
|
+
storage.saveAgents({
|
|
94
|
+
agent_1: {
|
|
95
|
+
id: 'agent_1',
|
|
96
|
+
name: 'Molly',
|
|
97
|
+
provider: 'test-provider',
|
|
98
|
+
model: 'test-model',
|
|
99
|
+
plugins: [],
|
|
100
|
+
threadSessionId: 'agent_thread',
|
|
101
|
+
createdAt: now,
|
|
102
|
+
updatedAt: now,
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
storage.saveConnectors({
|
|
106
|
+
conn_1: {
|
|
107
|
+
id: 'conn_1',
|
|
108
|
+
name: 'WhatsApp',
|
|
109
|
+
platform: 'whatsapp',
|
|
110
|
+
agentId: 'agent_1',
|
|
111
|
+
credentialId: null,
|
|
112
|
+
config: { inboundDebounceMs: 0 },
|
|
113
|
+
isEnabled: true,
|
|
114
|
+
status: 'running',
|
|
115
|
+
createdAt: now,
|
|
116
|
+
updatedAt: now,
|
|
117
|
+
},
|
|
118
|
+
})
|
|
119
|
+
storage.saveSessions({
|
|
120
|
+
agent_thread: {
|
|
121
|
+
id: 'agent_thread',
|
|
122
|
+
name: 'Molly',
|
|
123
|
+
cwd: process.env.WORKSPACE_DIR,
|
|
124
|
+
user: 'default',
|
|
125
|
+
provider: 'test-provider',
|
|
126
|
+
model: 'test-model',
|
|
127
|
+
claudeSessionId: null,
|
|
128
|
+
messages: [],
|
|
129
|
+
createdAt: now,
|
|
130
|
+
lastActiveAt: now,
|
|
131
|
+
sessionType: 'human',
|
|
132
|
+
agentId: 'agent_1',
|
|
133
|
+
plugins: [],
|
|
134
|
+
},
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const connector = storage.loadConnectors().conn_1
|
|
138
|
+
const response = await manager.routeConnectorMessageForTest(connector, {
|
|
139
|
+
platform: 'whatsapp',
|
|
140
|
+
channelId: '15550001111@s.whatsapp.net',
|
|
141
|
+
senderId: '15550001111@s.whatsapp.net',
|
|
142
|
+
senderName: 'Alice',
|
|
143
|
+
text: 'Hello from WhatsApp',
|
|
144
|
+
messageId: 'in-1',
|
|
145
|
+
isGroup: false,
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
const sessions = storage.loadSessions()
|
|
149
|
+
const directSession = Object.values(sessions).find((entry) => entry.id !== 'agent_thread')
|
|
150
|
+
const mainSession = sessions.agent_thread
|
|
151
|
+
console.log(JSON.stringify({ response, directSession, mainSession }))
|
|
152
|
+
`)
|
|
153
|
+
|
|
154
|
+
assert.equal(output.response, 'Roger that via WhatsApp')
|
|
155
|
+
assert.equal(output.directSession.messages.length, 2)
|
|
156
|
+
assert.equal(output.directSession.messages[0].role, 'user')
|
|
157
|
+
assert.equal(output.directSession.messages[0].source.platform, 'whatsapp')
|
|
158
|
+
assert.equal(output.directSession.messages[0].source.messageId, 'in-1')
|
|
159
|
+
assert.equal(output.directSession.messages[1].role, 'assistant')
|
|
160
|
+
assert.equal(output.directSession.messages[1].text, 'Roger that via WhatsApp')
|
|
161
|
+
assert.equal(output.directSession.messages[1].source.platform, 'whatsapp')
|
|
162
|
+
assert.equal(output.directSession.messages[1].source.replyToMessageId, 'in-1')
|
|
163
|
+
assert.equal(output.mainSession.messages.length, 2)
|
|
164
|
+
assert.equal(output.mainSession.messages[0].source.platform, 'whatsapp')
|
|
165
|
+
assert.equal(output.mainSession.messages[0].source.senderName, 'Alice')
|
|
166
|
+
assert.equal(output.mainSession.messages[0].historyExcluded, true)
|
|
167
|
+
assert.equal(output.mainSession.messages[1].source.platform, 'whatsapp')
|
|
168
|
+
assert.equal(output.mainSession.messages[1].text, 'Roger that via WhatsApp')
|
|
169
|
+
assert.equal(output.mainSession.messages[1].historyExcluded, true)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('mirrors same-channel connector_message_tool sends when the agent suppresses visible text', () => {
|
|
173
|
+
const output = runWithTempDataDir(`
|
|
174
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
175
|
+
const managerMod = await import('./src/lib/server/connectors/manager.ts')
|
|
176
|
+
const providersMod = await import('./src/lib/providers/index.ts')
|
|
177
|
+
const storage = storageMod.default || storageMod
|
|
178
|
+
const manager = managerMod.default || managerMod
|
|
179
|
+
const providers = providersMod.default || providersMod
|
|
180
|
+
|
|
181
|
+
const now = Date.now()
|
|
182
|
+
providers.PROVIDERS['test-provider'] = {
|
|
183
|
+
id: 'test-provider',
|
|
184
|
+
name: 'Test Provider',
|
|
185
|
+
models: ['test-model'],
|
|
186
|
+
requiresApiKey: false,
|
|
187
|
+
requiresEndpoint: false,
|
|
188
|
+
handler: {
|
|
189
|
+
streamChat: async (opts) => {
|
|
190
|
+
opts.write('data: ' + JSON.stringify({ t: 'r', text: 'NO_MESSAGE' }) + '\\n')
|
|
191
|
+
return ''
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
}
|
|
195
|
+
storage.saveSettings({})
|
|
196
|
+
storage.saveAgents({
|
|
197
|
+
agent_1: {
|
|
198
|
+
id: 'agent_1',
|
|
199
|
+
name: 'Molly',
|
|
200
|
+
provider: 'test-provider',
|
|
201
|
+
model: 'test-model',
|
|
202
|
+
plugins: ['connector_message_tool'],
|
|
203
|
+
createdAt: now,
|
|
204
|
+
updatedAt: now,
|
|
205
|
+
},
|
|
206
|
+
})
|
|
207
|
+
storage.saveConnectors({
|
|
208
|
+
conn_1: {
|
|
209
|
+
id: 'conn_1',
|
|
210
|
+
name: 'WhatsApp',
|
|
211
|
+
platform: 'whatsapp',
|
|
212
|
+
agentId: 'agent_1',
|
|
213
|
+
credentialId: null,
|
|
214
|
+
config: { inboundDebounceMs: 0 },
|
|
215
|
+
isEnabled: true,
|
|
216
|
+
status: 'running',
|
|
217
|
+
createdAt: now,
|
|
218
|
+
updatedAt: now,
|
|
219
|
+
},
|
|
220
|
+
})
|
|
221
|
+
storage.saveSessions({})
|
|
222
|
+
|
|
223
|
+
manager.setStreamAgentChatForTest(async (opts) => {
|
|
224
|
+
opts.write('data: ' + JSON.stringify({
|
|
225
|
+
t: 'tool_call',
|
|
226
|
+
toolName: 'connector_message_tool',
|
|
227
|
+
toolInput: JSON.stringify({
|
|
228
|
+
action: 'send',
|
|
229
|
+
to: '15550001111@s.whatsapp.net',
|
|
230
|
+
message: 'Sent from tool path',
|
|
231
|
+
}),
|
|
232
|
+
toolCallId: 'call-1',
|
|
233
|
+
}) + '\\n')
|
|
234
|
+
opts.write('data: ' + JSON.stringify({
|
|
235
|
+
t: 'tool_result',
|
|
236
|
+
toolName: 'connector_message_tool',
|
|
237
|
+
toolOutput: JSON.stringify({
|
|
238
|
+
status: 'sent',
|
|
239
|
+
to: '15550001111@s.whatsapp.net',
|
|
240
|
+
messageId: 'wa-out-1',
|
|
241
|
+
}),
|
|
242
|
+
toolCallId: 'call-1',
|
|
243
|
+
}) + '\\n')
|
|
244
|
+
return {
|
|
245
|
+
fullText: 'NO_MESSAGE',
|
|
246
|
+
finalResponse: 'NO_MESSAGE',
|
|
247
|
+
toolEvents: [],
|
|
248
|
+
}
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
const connector = storage.loadConnectors().conn_1
|
|
253
|
+
const response = await manager.routeConnectorMessageForTest(connector, {
|
|
254
|
+
platform: 'whatsapp',
|
|
255
|
+
channelId: '15550001111@s.whatsapp.net',
|
|
256
|
+
senderId: '15550001111@s.whatsapp.net',
|
|
257
|
+
senderName: 'Alice',
|
|
258
|
+
text: 'Send it on WhatsApp',
|
|
259
|
+
messageId: 'in-2',
|
|
260
|
+
isGroup: false,
|
|
261
|
+
})
|
|
262
|
+
const sessions = storage.loadSessions()
|
|
263
|
+
const directSession = Object.values(sessions).find((entry) => String(entry.name || '').startsWith('connector:'))
|
|
264
|
+
const mainSession = Object.values(sessions).find((entry) => entry.id !== directSession?.id)
|
|
265
|
+
console.log(JSON.stringify({ response, directSession, mainSession }))
|
|
266
|
+
} finally {
|
|
267
|
+
manager.setStreamAgentChatForTest(null)
|
|
268
|
+
}
|
|
269
|
+
`)
|
|
270
|
+
|
|
271
|
+
assert.equal(output.response, 'NO_MESSAGE')
|
|
272
|
+
assert.equal(output.directSession.messages.length, 2)
|
|
273
|
+
assert.equal(output.directSession.messages[1].role, 'assistant')
|
|
274
|
+
assert.equal(output.directSession.messages[1].text, 'Sent from tool path')
|
|
275
|
+
assert.equal(output.directSession.messages[1].source.platform, 'whatsapp')
|
|
276
|
+
assert.equal(output.directSession.messages[1].source.messageId, 'wa-out-1')
|
|
277
|
+
assert.equal(output.directSession.connectorContext.lastOutboundMessageId, 'wa-out-1')
|
|
278
|
+
assert.equal(output.mainSession.messages.length, 2)
|
|
279
|
+
assert.equal(output.mainSession.messages.every((entry) => entry.historyExcluded === true), true)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('accepts WhatsApp allowlist matches through senderIdAlt when the primary sender id is a lid', () => {
|
|
283
|
+
const output = runWithTempDataDir(`
|
|
284
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
285
|
+
const managerMod = await import('./src/lib/server/connectors/manager.ts')
|
|
286
|
+
const providersMod = await import('./src/lib/providers/index.ts')
|
|
287
|
+
const storage = storageMod.default || storageMod
|
|
288
|
+
const manager = managerMod.default || managerMod
|
|
289
|
+
const providers = providersMod.default || providersMod
|
|
290
|
+
|
|
291
|
+
const now = Date.now()
|
|
292
|
+
providers.PROVIDERS['test-provider'] = {
|
|
293
|
+
id: 'test-provider',
|
|
294
|
+
name: 'Test Provider',
|
|
295
|
+
models: ['test-model'],
|
|
296
|
+
requiresApiKey: false,
|
|
297
|
+
requiresEndpoint: false,
|
|
298
|
+
handler: {
|
|
299
|
+
streamChat: async (opts) => {
|
|
300
|
+
opts.write('data: ' + JSON.stringify({ t: 'r', text: 'Allowlist matched' }) + '\\n')
|
|
301
|
+
return ''
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
storage.saveSettings({})
|
|
307
|
+
storage.saveAgents({
|
|
308
|
+
agent_1: {
|
|
309
|
+
id: 'agent_1',
|
|
310
|
+
name: 'Molly',
|
|
311
|
+
provider: 'test-provider',
|
|
312
|
+
model: 'test-model',
|
|
313
|
+
plugins: [],
|
|
314
|
+
createdAt: now,
|
|
315
|
+
updatedAt: now,
|
|
316
|
+
},
|
|
317
|
+
})
|
|
318
|
+
storage.saveConnectors({
|
|
319
|
+
conn_1: {
|
|
320
|
+
id: 'conn_1',
|
|
321
|
+
name: 'WhatsApp',
|
|
322
|
+
platform: 'whatsapp',
|
|
323
|
+
agentId: 'agent_1',
|
|
324
|
+
credentialId: null,
|
|
325
|
+
config: {
|
|
326
|
+
inboundDebounceMs: 0,
|
|
327
|
+
dmPolicy: 'allowlist',
|
|
328
|
+
allowFrom: '15550001111',
|
|
329
|
+
},
|
|
330
|
+
isEnabled: true,
|
|
331
|
+
status: 'running',
|
|
332
|
+
createdAt: now,
|
|
333
|
+
updatedAt: now,
|
|
334
|
+
},
|
|
335
|
+
})
|
|
336
|
+
storage.saveSessions({})
|
|
337
|
+
|
|
338
|
+
const connector = storage.loadConnectors().conn_1
|
|
339
|
+
const response = await manager.routeConnectorMessageForTest(connector, {
|
|
340
|
+
platform: 'whatsapp',
|
|
341
|
+
channelId: '199900000001@lid',
|
|
342
|
+
channelIdAlt: '15550001111@s.whatsapp.net',
|
|
343
|
+
senderId: '199900000001@lid',
|
|
344
|
+
senderIdAlt: '15550001111@s.whatsapp.net',
|
|
345
|
+
senderName: 'Alice',
|
|
346
|
+
text: 'Hello from a lid sender',
|
|
347
|
+
messageId: 'in-3',
|
|
348
|
+
isGroup: false,
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
const session = Object.values(storage.loadSessions())[0]
|
|
352
|
+
console.log(JSON.stringify({ response, session }))
|
|
353
|
+
`)
|
|
354
|
+
|
|
355
|
+
assert.equal(output.response, 'Allowlist matched')
|
|
356
|
+
assert.equal(output.session.messages[0].source.senderId, '199900000001@lid')
|
|
357
|
+
assert.equal(output.session.messages[0].source.messageId, 'in-3')
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
it('routes send_voice_note to the current connector conversation when an audio file already exists', () => {
|
|
361
|
+
const output = runWithTempDataDir(`
|
|
362
|
+
const fs = await import('node:fs')
|
|
363
|
+
const path = await import('node:path')
|
|
364
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
365
|
+
const managerMod = await import('./src/lib/server/connectors/manager.ts')
|
|
366
|
+
const pluginsMod = await import('./src/lib/server/plugins.ts')
|
|
367
|
+
const toolsMod = await import('./src/lib/server/session-tools/index.ts')
|
|
368
|
+
const storage = storageMod.default || storageMod
|
|
369
|
+
const manager = managerMod.default || managerMod
|
|
370
|
+
const plugins = pluginsMod.default || pluginsMod
|
|
371
|
+
const toolsApi = toolsMod.default || toolsMod
|
|
372
|
+
|
|
373
|
+
const now = Date.now()
|
|
374
|
+
const sent = []
|
|
375
|
+
plugins.getPluginManager().registerBuiltin('test-voice-connector-plugin', {
|
|
376
|
+
name: 'Test Voice Connector Plugin',
|
|
377
|
+
connectors: [{
|
|
378
|
+
id: 'test-voice',
|
|
379
|
+
name: 'Test Voice',
|
|
380
|
+
description: 'Test voice connector',
|
|
381
|
+
startListener: async () => async () => {},
|
|
382
|
+
sendMessage: async (channelId, text, options) => {
|
|
383
|
+
sent.push({ channelId, text, options })
|
|
384
|
+
return { messageId: 'voice-out-1' }
|
|
385
|
+
},
|
|
386
|
+
}],
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
storage.saveSettings({})
|
|
390
|
+
storage.saveAgents({
|
|
391
|
+
agent_1: {
|
|
392
|
+
id: 'agent_1',
|
|
393
|
+
name: 'Molly',
|
|
394
|
+
provider: 'anthropic',
|
|
395
|
+
model: 'claude-test',
|
|
396
|
+
plugins: ['manage_connectors'],
|
|
397
|
+
createdAt: now,
|
|
398
|
+
updatedAt: now,
|
|
399
|
+
},
|
|
400
|
+
})
|
|
401
|
+
storage.saveConnectors({
|
|
402
|
+
conn_voice: {
|
|
403
|
+
id: 'conn_voice',
|
|
404
|
+
name: 'Test Voice Connector',
|
|
405
|
+
platform: 'test-voice',
|
|
406
|
+
agentId: 'agent_1',
|
|
407
|
+
credentialId: null,
|
|
408
|
+
config: { botToken: 'test-token' },
|
|
409
|
+
isEnabled: true,
|
|
410
|
+
status: 'stopped',
|
|
411
|
+
createdAt: now,
|
|
412
|
+
updatedAt: now,
|
|
413
|
+
},
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
const voicePath = path.join(process.env.DATA_DIR, 'gran-voice.mp3')
|
|
417
|
+
fs.writeFileSync(voicePath, Buffer.from('fake-mp3'))
|
|
418
|
+
|
|
419
|
+
storage.saveSessions({
|
|
420
|
+
session_1: {
|
|
421
|
+
id: 'session_1',
|
|
422
|
+
name: 'Molly',
|
|
423
|
+
cwd: process.env.WORKSPACE_DIR,
|
|
424
|
+
user: 'default',
|
|
425
|
+
provider: 'anthropic',
|
|
426
|
+
model: 'claude-test',
|
|
427
|
+
claudeSessionId: null,
|
|
428
|
+
messages: [{
|
|
429
|
+
role: 'user',
|
|
430
|
+
text: 'Send my gran a voice note',
|
|
431
|
+
time: now,
|
|
432
|
+
source: {
|
|
433
|
+
platform: 'whatsapp',
|
|
434
|
+
connectorId: 'conn_voice',
|
|
435
|
+
connectorName: 'Test Voice Connector',
|
|
436
|
+
channelId: '278200000001@s.whatsapp.net',
|
|
437
|
+
senderId: '278200000001@s.whatsapp.net',
|
|
438
|
+
senderName: 'Gran',
|
|
439
|
+
},
|
|
440
|
+
}],
|
|
441
|
+
createdAt: now,
|
|
442
|
+
lastActiveAt: now,
|
|
443
|
+
sessionType: 'human',
|
|
444
|
+
agentId: 'agent_1',
|
|
445
|
+
plugins: ['manage_connectors'],
|
|
446
|
+
connectorContext: {
|
|
447
|
+
connectorId: 'conn_voice',
|
|
448
|
+
platform: 'whatsapp',
|
|
449
|
+
channelId: '278200000001@s.whatsapp.net',
|
|
450
|
+
senderId: '278200000001@s.whatsapp.net',
|
|
451
|
+
senderName: 'Gran',
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
await manager.startConnector('conn_voice')
|
|
457
|
+
const built = await toolsApi.buildSessionTools(process.cwd(), ['manage_connectors'], {
|
|
458
|
+
sessionId: 'session_1',
|
|
459
|
+
agentId: 'agent_1',
|
|
460
|
+
platformAssignScope: 'self',
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
try {
|
|
464
|
+
const connectorTool = built.tools.find((tool) => tool.name === 'connector_message_tool')
|
|
465
|
+
const raw = await connectorTool.invoke({
|
|
466
|
+
action: 'send_voice_note',
|
|
467
|
+
connectorId: 'conn_voice',
|
|
468
|
+
mediaPath: voicePath,
|
|
469
|
+
})
|
|
470
|
+
console.log(JSON.stringify({ result: JSON.parse(String(raw)), sent }))
|
|
471
|
+
} finally {
|
|
472
|
+
await built.cleanup()
|
|
473
|
+
await manager.stopConnector('conn_voice')
|
|
474
|
+
}
|
|
475
|
+
`)
|
|
476
|
+
|
|
477
|
+
assert.equal(output.result.status, 'voice_sent')
|
|
478
|
+
assert.equal(output.result.to, '278200000001@s.whatsapp.net')
|
|
479
|
+
assert.equal(output.sent.length, 1)
|
|
480
|
+
assert.equal(output.sent[0].channelId, '278200000001@s.whatsapp.net')
|
|
481
|
+
assert.match(output.sent[0].text, /gran-voice\.mp3/)
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
it('restarts a stale connector automatically when an outbound send fails with connection closed', () => {
|
|
485
|
+
const output = runWithTempDataDir(`
|
|
486
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
487
|
+
const managerMod = await import('./src/lib/server/connectors/manager.ts')
|
|
488
|
+
const pluginsMod = await import('./src/lib/server/plugins.ts')
|
|
489
|
+
const storage = storageMod.default || storageMod
|
|
490
|
+
const manager = managerMod.default || managerMod
|
|
491
|
+
const plugins = pluginsMod.default || pluginsMod
|
|
492
|
+
|
|
493
|
+
const now = Date.now()
|
|
494
|
+
let startCount = 0
|
|
495
|
+
const attempts = []
|
|
496
|
+
plugins.getPluginManager().registerBuiltin('test-recover-connector-plugin', {
|
|
497
|
+
name: 'Test Recover Connector Plugin',
|
|
498
|
+
connectors: [{
|
|
499
|
+
id: 'test-recover',
|
|
500
|
+
name: 'Test Recover',
|
|
501
|
+
description: 'Test connector with recoverable send failure',
|
|
502
|
+
startListener: async () => {
|
|
503
|
+
startCount += 1
|
|
504
|
+
return async () => {}
|
|
505
|
+
},
|
|
506
|
+
sendMessage: async (channelId, text, options) => {
|
|
507
|
+
attempts.push({ channelId, text, options })
|
|
508
|
+
if (attempts.length === 1) throw new Error('Connection Closed')
|
|
509
|
+
return { messageId: 'recover-1' }
|
|
510
|
+
},
|
|
511
|
+
}],
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
storage.saveSettings({})
|
|
515
|
+
storage.saveConnectors({
|
|
516
|
+
conn_recover: {
|
|
517
|
+
id: 'conn_recover',
|
|
518
|
+
name: 'Recover Connector',
|
|
519
|
+
platform: 'test-recover',
|
|
520
|
+
agentId: 'agent_1',
|
|
521
|
+
credentialId: null,
|
|
522
|
+
config: { botToken: 'test-token' },
|
|
523
|
+
isEnabled: true,
|
|
524
|
+
status: 'stopped',
|
|
525
|
+
createdAt: now,
|
|
526
|
+
updatedAt: now,
|
|
527
|
+
},
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
try {
|
|
531
|
+
await manager.startConnector('conn_recover')
|
|
532
|
+
const result = await manager.sendConnectorMessage({
|
|
533
|
+
connectorId: 'conn_recover',
|
|
534
|
+
channelId: '15550001111',
|
|
535
|
+
text: 'hello after restart',
|
|
536
|
+
})
|
|
537
|
+
const health = Object.values(storage.loadConnectorHealth()).filter((entry) => entry.connectorId === 'conn_recover')
|
|
538
|
+
console.log(JSON.stringify({ result, attempts, startCount, health }))
|
|
539
|
+
} finally {
|
|
540
|
+
await manager.stopConnector('conn_recover')
|
|
541
|
+
}
|
|
542
|
+
`)
|
|
543
|
+
|
|
544
|
+
assert.equal(output.result.messageId, 'recover-1')
|
|
545
|
+
assert.equal(output.attempts.length, 2)
|
|
546
|
+
assert.equal(output.startCount, 2)
|
|
547
|
+
assert.equal(output.health.some((entry: { event?: string }) => entry.event === 'disconnected'), true)
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
it('blocks ambiguous connector sends when a thread references multiple people on the same connector', () => {
|
|
551
|
+
const output = runWithTempDataDir(`
|
|
552
|
+
const fs = await import('node:fs')
|
|
553
|
+
const path = await import('node:path')
|
|
554
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
555
|
+
const managerMod = await import('./src/lib/server/connectors/manager.ts')
|
|
556
|
+
const pluginsMod = await import('./src/lib/server/plugins.ts')
|
|
557
|
+
const toolsMod = await import('./src/lib/server/session-tools/index.ts')
|
|
558
|
+
const storage = storageMod.default || storageMod
|
|
559
|
+
const manager = managerMod.default || managerMod
|
|
560
|
+
const plugins = pluginsMod.default || pluginsMod
|
|
561
|
+
const toolsApi = toolsMod.default || toolsMod
|
|
562
|
+
|
|
563
|
+
const now = Date.now()
|
|
564
|
+
plugins.getPluginManager().registerBuiltin('test-ambiguous-connector-plugin', {
|
|
565
|
+
name: 'Test Ambiguous Connector Plugin',
|
|
566
|
+
connectors: [{
|
|
567
|
+
id: 'test-voice',
|
|
568
|
+
name: 'Test Voice',
|
|
569
|
+
description: 'Test voice connector',
|
|
570
|
+
startListener: async () => async () => {},
|
|
571
|
+
sendMessage: async () => ({ messageId: 'should-not-send' }),
|
|
572
|
+
}],
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
storage.saveSettings({})
|
|
576
|
+
storage.saveAgents({
|
|
577
|
+
agent_1: {
|
|
578
|
+
id: 'agent_1',
|
|
579
|
+
name: 'Molly',
|
|
580
|
+
provider: 'anthropic',
|
|
581
|
+
model: 'claude-test',
|
|
582
|
+
plugins: ['manage_connectors'],
|
|
583
|
+
createdAt: now,
|
|
584
|
+
updatedAt: now,
|
|
585
|
+
},
|
|
586
|
+
})
|
|
587
|
+
storage.saveConnectors({
|
|
588
|
+
conn_voice: {
|
|
589
|
+
id: 'conn_voice',
|
|
590
|
+
name: 'Test Voice Connector',
|
|
591
|
+
platform: 'test-voice',
|
|
592
|
+
agentId: 'agent_1',
|
|
593
|
+
credentialId: null,
|
|
594
|
+
config: { botToken: 'test-token' },
|
|
595
|
+
isEnabled: true,
|
|
596
|
+
status: 'stopped',
|
|
597
|
+
createdAt: now,
|
|
598
|
+
updatedAt: now,
|
|
599
|
+
},
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
const voicePath = path.join(process.env.DATA_DIR, 'gran-voice.mp3')
|
|
603
|
+
fs.writeFileSync(voicePath, Buffer.from('fake-mp3'))
|
|
604
|
+
|
|
605
|
+
storage.saveSessions({
|
|
606
|
+
session_1: {
|
|
607
|
+
id: 'session_1',
|
|
608
|
+
name: 'Molly',
|
|
609
|
+
cwd: process.env.WORKSPACE_DIR,
|
|
610
|
+
user: 'default',
|
|
611
|
+
provider: 'anthropic',
|
|
612
|
+
model: 'claude-test',
|
|
613
|
+
claudeSessionId: null,
|
|
614
|
+
messages: [
|
|
615
|
+
{
|
|
616
|
+
role: 'user',
|
|
617
|
+
text: 'Alice said hello',
|
|
618
|
+
time: now - 1000,
|
|
619
|
+
source: {
|
|
620
|
+
platform: 'whatsapp',
|
|
621
|
+
connectorId: 'conn_voice',
|
|
622
|
+
connectorName: 'Test Voice Connector',
|
|
623
|
+
channelId: '15550001111@s.whatsapp.net',
|
|
624
|
+
senderId: '15550001111@s.whatsapp.net',
|
|
625
|
+
senderName: 'Alice',
|
|
626
|
+
},
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
role: 'user',
|
|
630
|
+
text: 'Gran replied after that',
|
|
631
|
+
time: now,
|
|
632
|
+
source: {
|
|
633
|
+
platform: 'whatsapp',
|
|
634
|
+
connectorId: 'conn_voice',
|
|
635
|
+
connectorName: 'Test Voice Connector',
|
|
636
|
+
channelId: '278200000001@s.whatsapp.net',
|
|
637
|
+
senderId: '278200000001@s.whatsapp.net',
|
|
638
|
+
senderName: 'Gran',
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
],
|
|
642
|
+
createdAt: now,
|
|
643
|
+
lastActiveAt: now,
|
|
644
|
+
sessionType: 'human',
|
|
645
|
+
agentId: 'agent_1',
|
|
646
|
+
plugins: ['manage_connectors'],
|
|
647
|
+
},
|
|
648
|
+
})
|
|
649
|
+
|
|
650
|
+
await manager.startConnector('conn_voice')
|
|
651
|
+
const built = await toolsApi.buildSessionTools(process.cwd(), ['manage_connectors'], {
|
|
652
|
+
sessionId: 'session_1',
|
|
653
|
+
agentId: 'agent_1',
|
|
654
|
+
platformAssignScope: 'self',
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
try {
|
|
658
|
+
const connectorTool = built.tools.find((tool) => tool.name === 'connector_message_tool')
|
|
659
|
+
const raw = await connectorTool.invoke({
|
|
660
|
+
action: 'send_voice_note',
|
|
661
|
+
connectorId: 'conn_voice',
|
|
662
|
+
mediaPath: voicePath,
|
|
663
|
+
})
|
|
664
|
+
console.log(JSON.stringify({ raw: String(raw) }))
|
|
665
|
+
} finally {
|
|
666
|
+
await built.cleanup()
|
|
667
|
+
await manager.stopConnector('conn_voice')
|
|
668
|
+
}
|
|
669
|
+
`)
|
|
670
|
+
|
|
671
|
+
assert.match(output.raw, /multiple connector recipients/)
|
|
672
|
+
assert.match(output.raw, /Alice/)
|
|
673
|
+
assert.match(output.raw, /Gran/)
|
|
674
|
+
})
|
|
675
|
+
|
|
676
|
+
it('keeps direct connector sessions isolated across four inbound senders for the same agent and mirrors their metadata into the main thread', () => {
|
|
677
|
+
const output = runWithTempDataDir(`
|
|
678
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
679
|
+
const managerMod = await import('./src/lib/server/connectors/manager.ts')
|
|
680
|
+
const providersMod = await import('./src/lib/providers/index.ts')
|
|
681
|
+
const storage = storageMod.default || storageMod
|
|
682
|
+
const manager = managerMod.default || managerMod
|
|
683
|
+
const providers = providersMod.default || providersMod
|
|
684
|
+
|
|
685
|
+
const now = Date.now()
|
|
686
|
+
providers.PROVIDERS['test-provider'] = {
|
|
687
|
+
id: 'test-provider',
|
|
688
|
+
name: 'Test Provider',
|
|
689
|
+
models: ['test-model'],
|
|
690
|
+
requiresApiKey: false,
|
|
691
|
+
requiresEndpoint: false,
|
|
692
|
+
handler: {
|
|
693
|
+
streamChat: async (opts) => {
|
|
694
|
+
const text = String(opts.message || '')
|
|
695
|
+
const match = text.match(/\\[(.*?)\\]/)
|
|
696
|
+
const name = match?.[1] || 'Unknown'
|
|
697
|
+
opts.write('data: ' + JSON.stringify({ t: 'r', text: 'Replying to ' + name }) + '\\n')
|
|
698
|
+
return ''
|
|
699
|
+
},
|
|
700
|
+
},
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
storage.saveSettings({})
|
|
704
|
+
storage.saveAgents({
|
|
705
|
+
agent_1: {
|
|
706
|
+
id: 'agent_1',
|
|
707
|
+
name: 'Molly',
|
|
708
|
+
provider: 'test-provider',
|
|
709
|
+
model: 'test-model',
|
|
710
|
+
plugins: [],
|
|
711
|
+
threadSessionId: 'agent_thread',
|
|
712
|
+
createdAt: now,
|
|
713
|
+
updatedAt: now,
|
|
714
|
+
},
|
|
715
|
+
})
|
|
716
|
+
storage.saveConnectors({
|
|
717
|
+
conn_1: {
|
|
718
|
+
id: 'conn_1',
|
|
719
|
+
name: 'WhatsApp',
|
|
720
|
+
platform: 'whatsapp',
|
|
721
|
+
agentId: 'agent_1',
|
|
722
|
+
credentialId: null,
|
|
723
|
+
config: { inboundDebounceMs: 0 },
|
|
724
|
+
isEnabled: true,
|
|
725
|
+
status: 'running',
|
|
726
|
+
createdAt: now,
|
|
727
|
+
updatedAt: now,
|
|
728
|
+
},
|
|
729
|
+
})
|
|
730
|
+
storage.saveSessions({
|
|
731
|
+
agent_thread: {
|
|
732
|
+
id: 'agent_thread',
|
|
733
|
+
name: 'Molly',
|
|
734
|
+
cwd: process.env.WORKSPACE_DIR,
|
|
735
|
+
user: 'default',
|
|
736
|
+
provider: 'test-provider',
|
|
737
|
+
model: 'test-model',
|
|
738
|
+
claudeSessionId: null,
|
|
739
|
+
messages: [],
|
|
740
|
+
createdAt: now,
|
|
741
|
+
lastActiveAt: now,
|
|
742
|
+
sessionType: 'human',
|
|
743
|
+
agentId: 'agent_1',
|
|
744
|
+
plugins: [],
|
|
745
|
+
},
|
|
746
|
+
})
|
|
747
|
+
|
|
748
|
+
const connector = storage.loadConnectors().conn_1
|
|
749
|
+
const inbound = async (senderId, senderName, messageId, text) => manager.routeConnectorMessageForTest(connector, {
|
|
750
|
+
platform: 'whatsapp',
|
|
751
|
+
channelId: senderId,
|
|
752
|
+
senderId,
|
|
753
|
+
senderName,
|
|
754
|
+
text,
|
|
755
|
+
messageId,
|
|
756
|
+
isGroup: false,
|
|
757
|
+
})
|
|
758
|
+
|
|
759
|
+
await inbound('15550001111@s.whatsapp.net', 'Alice', 'in-a', 'Hello from Alice')
|
|
760
|
+
await inbound('16660002222@s.whatsapp.net', 'Bob', 'in-b', 'Hello from Bob')
|
|
761
|
+
await inbound('278200000001@s.whatsapp.net', 'Gran', 'in-c', 'Hello from Gran')
|
|
762
|
+
await inbound('447700900123@s.whatsapp.net', 'Wayde', 'in-d', 'Hello from Wayde')
|
|
763
|
+
|
|
764
|
+
const sessions = storage.loadSessions()
|
|
765
|
+
const directSessions = Object.values(sessions)
|
|
766
|
+
.filter((entry) => String(entry.name || '').startsWith('connector:'))
|
|
767
|
+
.map((entry) => ({
|
|
768
|
+
id: entry.id,
|
|
769
|
+
name: entry.name,
|
|
770
|
+
senderName: entry.connectorContext?.senderName || null,
|
|
771
|
+
channelId: entry.connectorContext?.channelId || null,
|
|
772
|
+
texts: (entry.messages || []).map((m) => ({ role: m.role, text: m.text, source: m.source || null })),
|
|
773
|
+
}))
|
|
774
|
+
.sort((a, b) => String(a.senderName).localeCompare(String(b.senderName)))
|
|
775
|
+
const thread = sessions.agent_thread
|
|
776
|
+
console.log(JSON.stringify({ directSessions, threadMessages: thread.messages }))
|
|
777
|
+
`)
|
|
778
|
+
|
|
779
|
+
assert.equal(output.directSessions.length, 4)
|
|
780
|
+
assert.deepEqual(output.directSessions.map((entry) => entry.senderName), ['Alice', 'Bob', 'Gran', 'Wayde'])
|
|
781
|
+
assert.equal(output.directSessions.every((entry) => entry.texts.length === 2), true)
|
|
782
|
+
assert.equal(output.directSessions.every((entry) => entry.texts[0].source?.senderName === entry.senderName), true)
|
|
783
|
+
assert.equal(output.directSessions.every((entry) => entry.texts[1].text === `Replying to ${entry.senderName}`), true)
|
|
784
|
+
assert.equal(output.threadMessages.length, 8)
|
|
785
|
+
assert.deepEqual(
|
|
786
|
+
output.threadMessages.filter((msg) => msg.role === 'user').map((msg) => msg.source?.senderName),
|
|
787
|
+
['Alice', 'Bob', 'Gran', 'Wayde'],
|
|
788
|
+
)
|
|
789
|
+
assert.deepEqual(
|
|
790
|
+
output.threadMessages.filter((msg) => msg.role === 'assistant').map((msg) => ({
|
|
791
|
+
text: msg.text,
|
|
792
|
+
senderName: msg.source?.senderName,
|
|
793
|
+
connectorId: msg.source?.connectorId,
|
|
794
|
+
})),
|
|
795
|
+
[
|
|
796
|
+
{ text: 'Replying to Alice', senderName: 'Alice', connectorId: 'conn_1' },
|
|
797
|
+
{ text: 'Replying to Bob', senderName: 'Bob', connectorId: 'conn_1' },
|
|
798
|
+
{ text: 'Replying to Gran', senderName: 'Gran', connectorId: 'conn_1' },
|
|
799
|
+
{ text: 'Replying to Wayde', senderName: 'Wayde', connectorId: 'conn_1' },
|
|
800
|
+
],
|
|
801
|
+
)
|
|
802
|
+
assert.equal(output.threadMessages.every((msg) => msg.historyExcluded === true), true)
|
|
803
|
+
})
|
|
804
|
+
|
|
805
|
+
it('excludes mirrored connector transcript entries from direct agent-thread history', () => {
|
|
806
|
+
const output = runWithTempDataDir(`
|
|
807
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
808
|
+
const managerMod = await import('./src/lib/server/connectors/manager.ts')
|
|
809
|
+
const chatExecMod = await import('./src/lib/server/chat-execution.ts')
|
|
810
|
+
const providersMod = await import('./src/lib/providers/index.ts')
|
|
811
|
+
const storage = storageMod.default || storageMod
|
|
812
|
+
const manager = managerMod.default || managerMod
|
|
813
|
+
const chatExec = chatExecMod.default || chatExecMod
|
|
814
|
+
const providers = providersMod.default || providersMod
|
|
815
|
+
|
|
816
|
+
const now = Date.now()
|
|
817
|
+
providers.PROVIDERS['test-provider'] = {
|
|
818
|
+
id: 'test-provider',
|
|
819
|
+
name: 'Test Provider',
|
|
820
|
+
models: ['test-model'],
|
|
821
|
+
requiresApiKey: false,
|
|
822
|
+
requiresEndpoint: false,
|
|
823
|
+
handler: {
|
|
824
|
+
streamChat: async (opts) => {
|
|
825
|
+
const history = typeof opts.loadHistory === 'function' ? opts.loadHistory(opts.session.id) : []
|
|
826
|
+
return JSON.stringify({
|
|
827
|
+
historyCount: history.length,
|
|
828
|
+
texts: history.map((entry) => entry.text),
|
|
829
|
+
senderNames: history.map((entry) => entry.source?.senderName || null),
|
|
830
|
+
})
|
|
831
|
+
},
|
|
832
|
+
},
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
storage.saveSettings({})
|
|
836
|
+
storage.saveAgents({
|
|
837
|
+
agent_1: {
|
|
838
|
+
id: 'agent_1',
|
|
839
|
+
name: 'Molly',
|
|
840
|
+
provider: 'test-provider',
|
|
841
|
+
model: 'test-model',
|
|
842
|
+
plugins: [],
|
|
843
|
+
threadSessionId: 'agent_thread',
|
|
844
|
+
createdAt: now,
|
|
845
|
+
updatedAt: now,
|
|
846
|
+
},
|
|
847
|
+
})
|
|
848
|
+
storage.saveConnectors({
|
|
849
|
+
conn_1: {
|
|
850
|
+
id: 'conn_1',
|
|
851
|
+
name: 'WhatsApp',
|
|
852
|
+
platform: 'whatsapp',
|
|
853
|
+
agentId: 'agent_1',
|
|
854
|
+
credentialId: null,
|
|
855
|
+
config: { inboundDebounceMs: 0 },
|
|
856
|
+
isEnabled: true,
|
|
857
|
+
status: 'running',
|
|
858
|
+
createdAt: now,
|
|
859
|
+
updatedAt: now,
|
|
860
|
+
},
|
|
861
|
+
})
|
|
862
|
+
storage.saveSessions({
|
|
863
|
+
agent_thread: {
|
|
864
|
+
id: 'agent_thread',
|
|
865
|
+
name: 'Molly',
|
|
866
|
+
cwd: process.env.WORKSPACE_DIR,
|
|
867
|
+
user: 'default',
|
|
868
|
+
provider: 'test-provider',
|
|
869
|
+
model: 'test-model',
|
|
870
|
+
claudeSessionId: null,
|
|
871
|
+
messages: [],
|
|
872
|
+
createdAt: now,
|
|
873
|
+
lastActiveAt: now,
|
|
874
|
+
sessionType: 'human',
|
|
875
|
+
agentId: 'agent_1',
|
|
876
|
+
plugins: [],
|
|
877
|
+
},
|
|
878
|
+
})
|
|
879
|
+
|
|
880
|
+
const connector = storage.loadConnectors().conn_1
|
|
881
|
+
await manager.routeConnectorMessageForTest(connector, {
|
|
882
|
+
platform: 'whatsapp',
|
|
883
|
+
channelId: '15550001111@s.whatsapp.net',
|
|
884
|
+
senderId: '15550001111@s.whatsapp.net',
|
|
885
|
+
senderName: 'Alice',
|
|
886
|
+
text: 'Hello from Alice',
|
|
887
|
+
messageId: 'in-a',
|
|
888
|
+
isGroup: false,
|
|
889
|
+
})
|
|
890
|
+
await manager.routeConnectorMessageForTest(connector, {
|
|
891
|
+
platform: 'whatsapp',
|
|
892
|
+
channelId: '278200000001@s.whatsapp.net',
|
|
893
|
+
senderId: '278200000001@s.whatsapp.net',
|
|
894
|
+
senderName: 'Gran',
|
|
895
|
+
text: 'Hello from Gran',
|
|
896
|
+
messageId: 'in-g',
|
|
897
|
+
isGroup: false,
|
|
898
|
+
})
|
|
899
|
+
|
|
900
|
+
const result = await chatExec.executeSessionChatTurn({
|
|
901
|
+
sessionId: 'agent_thread',
|
|
902
|
+
message: 'This is Wayde in the app.',
|
|
903
|
+
})
|
|
904
|
+
|
|
905
|
+
const thread = storage.loadSessions().agent_thread
|
|
906
|
+
console.log(JSON.stringify({
|
|
907
|
+
reply: JSON.parse(result.text),
|
|
908
|
+
threadMessages: thread.messages,
|
|
909
|
+
}))
|
|
910
|
+
`)
|
|
911
|
+
|
|
912
|
+
assert.equal(output.reply.senderNames.includes('Alice'), false)
|
|
913
|
+
assert.equal(output.reply.senderNames.includes('Gran'), false)
|
|
914
|
+
assert.equal(output.reply.texts.some((entry) => /Alice|Gran/.test(String(entry))), false)
|
|
915
|
+
assert.equal(output.reply.historyCount >= 1, true)
|
|
916
|
+
assert.equal(output.threadMessages.some((msg) => msg.historyExcluded === true && msg.source?.connectorId === 'conn_1'), true)
|
|
917
|
+
})
|
|
918
|
+
|
|
919
|
+
it('creates one reusable connector-sender approval for unknown allowlist senders and allows them after approval', () => {
|
|
920
|
+
const output = runWithTempDataDir(`
|
|
921
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
922
|
+
const managerMod = await import('./src/lib/server/connectors/manager.ts')
|
|
923
|
+
const approvalsMod = await import('./src/lib/server/approvals.ts')
|
|
924
|
+
const pairingMod = await import('./src/lib/server/connectors/pairing.ts')
|
|
925
|
+
const providersMod = await import('./src/lib/providers/index.ts')
|
|
926
|
+
const storage = storageMod.default || storageMod
|
|
927
|
+
const manager = managerMod.default || managerMod
|
|
928
|
+
const approvals = approvalsMod.default || approvalsMod
|
|
929
|
+
const pairing = pairingMod.default || pairingMod
|
|
930
|
+
const providers = providersMod.default || providersMod
|
|
931
|
+
|
|
932
|
+
const now = Date.now()
|
|
933
|
+
providers.PROVIDERS['test-provider'] = {
|
|
934
|
+
id: 'test-provider',
|
|
935
|
+
name: 'Test Provider',
|
|
936
|
+
models: ['test-model'],
|
|
937
|
+
requiresApiKey: false,
|
|
938
|
+
requiresEndpoint: false,
|
|
939
|
+
handler: {
|
|
940
|
+
streamChat: async (opts) => {
|
|
941
|
+
const match = String(opts.message || '').match(/\\[(.*?)\\]/)
|
|
942
|
+
const name = match?.[1] || 'Unknown'
|
|
943
|
+
opts.write('data: ' + JSON.stringify({ t: 'r', text: 'Approved hello to ' + name }) + '\\n')
|
|
944
|
+
return ''
|
|
945
|
+
},
|
|
946
|
+
},
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
storage.saveSettings({ approvalsEnabled: true })
|
|
950
|
+
storage.saveAgents({
|
|
951
|
+
agent_1: {
|
|
952
|
+
id: 'agent_1',
|
|
953
|
+
name: 'Molly',
|
|
954
|
+
provider: 'test-provider',
|
|
955
|
+
model: 'test-model',
|
|
956
|
+
plugins: [],
|
|
957
|
+
threadSessionId: 'agent_thread',
|
|
958
|
+
createdAt: now,
|
|
959
|
+
updatedAt: now,
|
|
960
|
+
},
|
|
961
|
+
})
|
|
962
|
+
storage.saveConnectors({
|
|
963
|
+
conn_1: {
|
|
964
|
+
id: 'conn_1',
|
|
965
|
+
name: 'WhatsApp',
|
|
966
|
+
platform: 'whatsapp',
|
|
967
|
+
agentId: 'agent_1',
|
|
968
|
+
credentialId: null,
|
|
969
|
+
config: {
|
|
970
|
+
inboundDebounceMs: 0,
|
|
971
|
+
dmPolicy: 'allowlist',
|
|
972
|
+
allowFrom: '15550001111',
|
|
973
|
+
},
|
|
974
|
+
isEnabled: true,
|
|
975
|
+
status: 'running',
|
|
976
|
+
createdAt: now,
|
|
977
|
+
updatedAt: now,
|
|
978
|
+
},
|
|
979
|
+
})
|
|
980
|
+
storage.saveSessions({
|
|
981
|
+
agent_thread: {
|
|
982
|
+
id: 'agent_thread',
|
|
983
|
+
name: 'Molly',
|
|
984
|
+
cwd: process.env.WORKSPACE_DIR,
|
|
985
|
+
user: 'default',
|
|
986
|
+
provider: 'test-provider',
|
|
987
|
+
model: 'test-model',
|
|
988
|
+
claudeSessionId: null,
|
|
989
|
+
messages: [],
|
|
990
|
+
createdAt: now,
|
|
991
|
+
lastActiveAt: now,
|
|
992
|
+
sessionType: 'human',
|
|
993
|
+
agentId: 'agent_1',
|
|
994
|
+
plugins: [],
|
|
995
|
+
},
|
|
996
|
+
})
|
|
997
|
+
|
|
998
|
+
const connector = storage.loadConnectors().conn_1
|
|
999
|
+
const inbound = async (messageId, text) => manager.routeConnectorMessageForTest(connector, {
|
|
1000
|
+
platform: 'whatsapp',
|
|
1001
|
+
channelId: '16660002222@s.whatsapp.net',
|
|
1002
|
+
senderId: '16660002222@s.whatsapp.net',
|
|
1003
|
+
senderName: 'Bob',
|
|
1004
|
+
text,
|
|
1005
|
+
messageId,
|
|
1006
|
+
isGroup: false,
|
|
1007
|
+
})
|
|
1008
|
+
|
|
1009
|
+
const first = await inbound('in-b1', 'Hello from Bob')
|
|
1010
|
+
const second = await inbound('in-b2', 'Following up before approval')
|
|
1011
|
+
const pendingBefore = Object.values(storage.loadApprovals())
|
|
1012
|
+
await approvals.submitDecision(pendingBefore[0].id, true)
|
|
1013
|
+
const allowed = pairing.listStoredAllowedSenders('conn_1')
|
|
1014
|
+
const third = await inbound('in-b3', 'Hello after approval')
|
|
1015
|
+
const approvalsAfter = Object.values(storage.loadApprovals())
|
|
1016
|
+
const sessions = storage.loadSessions()
|
|
1017
|
+
const thread = sessions.agent_thread
|
|
1018
|
+
console.log(JSON.stringify({
|
|
1019
|
+
first,
|
|
1020
|
+
second,
|
|
1021
|
+
pendingBefore: pendingBefore.map((entry) => ({
|
|
1022
|
+
id: entry.id,
|
|
1023
|
+
category: entry.category,
|
|
1024
|
+
status: entry.status,
|
|
1025
|
+
title: entry.title,
|
|
1026
|
+
data: entry.data,
|
|
1027
|
+
})),
|
|
1028
|
+
allowed,
|
|
1029
|
+
third,
|
|
1030
|
+
approvalsAfter: approvalsAfter.map((entry) => ({ id: entry.id, status: entry.status })),
|
|
1031
|
+
threadMessages: thread.messages,
|
|
1032
|
+
}))
|
|
1033
|
+
`)
|
|
1034
|
+
|
|
1035
|
+
assert.match(output.first, /pending approval/i)
|
|
1036
|
+
assert.match(output.second, /pending approval/i)
|
|
1037
|
+
assert.equal(output.pendingBefore.length, 1)
|
|
1038
|
+
assert.equal(output.pendingBefore[0].category, 'connector_sender')
|
|
1039
|
+
assert.equal(output.pendingBefore[0].data.senderId, '16660002222@s.whatsapp.net')
|
|
1040
|
+
assert.deepEqual(output.allowed, ['16660002222@s.whatsapp.net'])
|
|
1041
|
+
assert.equal(output.third, 'Approved hello to Bob')
|
|
1042
|
+
assert.equal(output.approvalsAfter.length, 1)
|
|
1043
|
+
assert.equal(output.approvalsAfter[0].status, 'approved')
|
|
1044
|
+
assert.equal(output.threadMessages.some((msg) => msg.source?.senderName === 'Bob' && msg.historyExcluded === true), true)
|
|
1045
|
+
})
|
|
1046
|
+
|
|
1047
|
+
it('returns a friendly retry message instead of blank no-response when connector chat aborts', () => {
|
|
1048
|
+
const output = runWithTempDataDir(`
|
|
1049
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
1050
|
+
const managerMod = await import('./src/lib/server/connectors/manager.ts')
|
|
1051
|
+
const providersMod = await import('./src/lib/providers/index.ts')
|
|
1052
|
+
const storage = storageMod.default || storageMod
|
|
1053
|
+
const manager = managerMod.default || managerMod
|
|
1054
|
+
const providers = providersMod.default || providersMod
|
|
1055
|
+
|
|
1056
|
+
const now = Date.now()
|
|
1057
|
+
providers.PROVIDERS['test-provider'] = {
|
|
1058
|
+
id: 'test-provider',
|
|
1059
|
+
name: 'Test Provider',
|
|
1060
|
+
models: ['test-model'],
|
|
1061
|
+
requiresApiKey: false,
|
|
1062
|
+
requiresEndpoint: false,
|
|
1063
|
+
handler: {
|
|
1064
|
+
streamChat: async () => '',
|
|
1065
|
+
},
|
|
1066
|
+
}
|
|
1067
|
+
storage.saveSettings({})
|
|
1068
|
+
storage.saveAgents({
|
|
1069
|
+
agent_1: {
|
|
1070
|
+
id: 'agent_1',
|
|
1071
|
+
name: 'Molly',
|
|
1072
|
+
provider: 'test-provider',
|
|
1073
|
+
model: 'test-model',
|
|
1074
|
+
plugins: ['manage_connectors'],
|
|
1075
|
+
threadSessionId: 'agent_thread',
|
|
1076
|
+
createdAt: now,
|
|
1077
|
+
updatedAt: now,
|
|
1078
|
+
},
|
|
1079
|
+
})
|
|
1080
|
+
storage.saveConnectors({
|
|
1081
|
+
conn_1: {
|
|
1082
|
+
id: 'conn_1',
|
|
1083
|
+
name: 'WhatsApp',
|
|
1084
|
+
platform: 'whatsapp',
|
|
1085
|
+
agentId: 'agent_1',
|
|
1086
|
+
credentialId: null,
|
|
1087
|
+
config: { inboundDebounceMs: 0 },
|
|
1088
|
+
isEnabled: true,
|
|
1089
|
+
status: 'running',
|
|
1090
|
+
createdAt: now,
|
|
1091
|
+
updatedAt: now,
|
|
1092
|
+
},
|
|
1093
|
+
})
|
|
1094
|
+
storage.saveSessions({
|
|
1095
|
+
agent_thread: {
|
|
1096
|
+
id: 'agent_thread',
|
|
1097
|
+
name: 'Molly',
|
|
1098
|
+
cwd: process.env.WORKSPACE_DIR,
|
|
1099
|
+
user: 'default',
|
|
1100
|
+
provider: 'test-provider',
|
|
1101
|
+
model: 'test-model',
|
|
1102
|
+
claudeSessionId: null,
|
|
1103
|
+
messages: [],
|
|
1104
|
+
createdAt: now,
|
|
1105
|
+
lastActiveAt: now,
|
|
1106
|
+
sessionType: 'human',
|
|
1107
|
+
agentId: 'agent_1',
|
|
1108
|
+
plugins: ['manage_connectors'],
|
|
1109
|
+
},
|
|
1110
|
+
})
|
|
1111
|
+
|
|
1112
|
+
manager.setStreamAgentChatForTest(async (opts) => {
|
|
1113
|
+
opts.write('data: ' + JSON.stringify({ t: 'err', text: 'Abort' }) + '\\n')
|
|
1114
|
+
return {
|
|
1115
|
+
fullText: '',
|
|
1116
|
+
finalResponse: '',
|
|
1117
|
+
toolEvents: [],
|
|
1118
|
+
}
|
|
1119
|
+
})
|
|
1120
|
+
|
|
1121
|
+
try {
|
|
1122
|
+
const connector = storage.loadConnectors().conn_1
|
|
1123
|
+
const response = await manager.routeConnectorMessageForTest(connector, {
|
|
1124
|
+
platform: 'whatsapp',
|
|
1125
|
+
channelId: '15550001111@s.whatsapp.net',
|
|
1126
|
+
senderId: '15550001111@s.whatsapp.net',
|
|
1127
|
+
senderName: 'Alice',
|
|
1128
|
+
text: 'Hello?',
|
|
1129
|
+
messageId: 'in-hello',
|
|
1130
|
+
isGroup: false,
|
|
1131
|
+
})
|
|
1132
|
+
const sessions = storage.loadSessions()
|
|
1133
|
+
const directSession = Object.values(sessions).find((entry) => String(entry.name || '').startsWith('connector:'))
|
|
1134
|
+
const mainSession = sessions.agent_thread
|
|
1135
|
+
console.log(JSON.stringify({ response, directSession, mainSession }))
|
|
1136
|
+
} finally {
|
|
1137
|
+
manager.setStreamAgentChatForTest(null)
|
|
1138
|
+
}
|
|
1139
|
+
`)
|
|
1140
|
+
|
|
1141
|
+
assert.equal(output.response, 'Sorry, I hit a temporary issue while responding. Please try again.')
|
|
1142
|
+
assert.equal(output.directSession.messages.at(-1).role, 'assistant')
|
|
1143
|
+
assert.equal(output.directSession.messages.at(-1).text, 'Sorry, I hit a temporary issue while responding. Please try again.')
|
|
1144
|
+
assert.equal(output.mainSession.messages.at(-1).historyExcluded, true)
|
|
1145
|
+
assert.equal(output.mainSession.messages.at(-1).text, 'Sorry, I hit a temporary issue while responding. Please try again.')
|
|
1146
|
+
})
|
|
1147
|
+
})
|