@zooid/transport-matrix 0.13.0 → 0.14.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/dist/index.d.ts +152 -4
- package/dist/index.js +898 -94
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/context-provider.test.ts +61 -4
- package/src/context-provider.ts +40 -2
- package/src/index.ts +23 -3
- package/src/invocation-registry.test.ts +22 -0
- package/src/invocation-registry.ts +32 -0
- package/src/matrix-client.ts +58 -34
- package/src/router.test.ts +129 -10
- package/src/router.ts +78 -2
- package/src/task-completion.test.ts +23 -0
- package/src/task-completion.ts +37 -0
- package/src/task-dispatch.test.ts +53 -0
- package/src/task-dispatch.ts +68 -0
- package/src/task-envelope.test.ts +27 -0
- package/src/task-registry.test.ts +50 -0
- package/src/task-registry.ts +152 -0
- package/src/transport.test.ts +403 -114
- package/src/transport.ts +719 -110
package/src/transport.test.ts
CHANGED
|
@@ -91,6 +91,29 @@ async function postTxn(
|
|
|
91
91
|
})
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
// A real agent turn ends with dev.zooid.turn.end, and that is what releases a
|
|
95
|
+
// held return. Tests that inject agent replies must carry the boundary too.
|
|
96
|
+
let turnEndCounter = 0
|
|
97
|
+
function postTurnEnd(
|
|
98
|
+
app: ReturnType<typeof makeTransport>['transport']['app'],
|
|
99
|
+
o: { agent: string; root: string },
|
|
100
|
+
) {
|
|
101
|
+
return postTxn(app, {
|
|
102
|
+
events: [
|
|
103
|
+
{
|
|
104
|
+
type: 'dev.zooid.turn.end',
|
|
105
|
+
event_id: `$turn-end-${++turnEndCounter}`,
|
|
106
|
+
room_id: '!r:example.com',
|
|
107
|
+
sender: `@${o.agent}:example.com`,
|
|
108
|
+
content: {
|
|
109
|
+
agent_id: o.agent,
|
|
110
|
+
'm.relates_to': { rel_type: 'm.thread', event_id: o.root },
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
94
117
|
// runTurn is fire-and-forget; let any pending microtasks drain before
|
|
95
118
|
// asserting on side-effects driven by the agent prompt.
|
|
96
119
|
async function settleTurn(): Promise<void> {
|
|
@@ -139,14 +162,22 @@ describe('matrix transport /transactions', () => {
|
|
|
139
162
|
expect(res.status).toBe(200)
|
|
140
163
|
await settleTurn()
|
|
141
164
|
// Agent-promotion: sessionKey is the inbound event_id, NOT the room.
|
|
142
|
-
expect(agents.ensureSession).toHaveBeenCalledWith(
|
|
165
|
+
expect(agents.ensureSession).toHaveBeenCalledWith(
|
|
166
|
+
'architect',
|
|
167
|
+
'$root',
|
|
168
|
+
'!r:example.com',
|
|
169
|
+
'$root',
|
|
170
|
+
)
|
|
143
171
|
// The reply threads against the user's message.
|
|
144
172
|
expect(client.sendMessage).toHaveBeenCalledWith(
|
|
145
173
|
expect.objectContaining({
|
|
146
174
|
roomId: '!r:example.com',
|
|
147
175
|
asUserId: '@architect:example.com',
|
|
148
176
|
threadRoot: '$root',
|
|
149
|
-
content: expect.objectContaining({
|
|
177
|
+
content: expect.objectContaining({
|
|
178
|
+
msgtype: 'm.notice',
|
|
179
|
+
body: 'hello back',
|
|
180
|
+
}),
|
|
150
181
|
}),
|
|
151
182
|
)
|
|
152
183
|
})
|
|
@@ -156,7 +187,10 @@ describe('matrix transport /transactions', () => {
|
|
|
156
187
|
// response for a normal turn; opencode flushes a trailing chunk just after
|
|
157
188
|
// the stopReason. The post-turn drain must wait for it instead of sending
|
|
158
189
|
// the truncated buffer.
|
|
159
|
-
const { transport, agents, client } = makeTransport({
|
|
190
|
+
const { transport, agents, client } = makeTransport({
|
|
191
|
+
drainQuietMs: 20,
|
|
192
|
+
drainMaxMs: 500,
|
|
193
|
+
})
|
|
160
194
|
const events = [
|
|
161
195
|
{
|
|
162
196
|
type: 'm.room.message',
|
|
@@ -229,7 +263,12 @@ describe('matrix transport /transactions', () => {
|
|
|
229
263
|
})
|
|
230
264
|
await postTxn(transport.app, { events })
|
|
231
265
|
await settleTurn()
|
|
232
|
-
expect(agents.ensureSession).toHaveBeenCalledWith(
|
|
266
|
+
expect(agents.ensureSession).toHaveBeenCalledWith(
|
|
267
|
+
'architect',
|
|
268
|
+
'$root',
|
|
269
|
+
'!r:example.com',
|
|
270
|
+
'$root',
|
|
271
|
+
)
|
|
233
272
|
expect(client.sendMessage).toHaveBeenCalledWith(
|
|
234
273
|
expect.objectContaining({ threadRoot: '$root' }),
|
|
235
274
|
)
|
|
@@ -343,7 +382,9 @@ describe('matrix transport /transactions', () => {
|
|
|
343
382
|
})
|
|
344
383
|
await settleTurn()
|
|
345
384
|
expect(client.sendMessage).toHaveBeenCalledWith(
|
|
346
|
-
expect.objectContaining({
|
|
385
|
+
expect.objectContaining({
|
|
386
|
+
content: expect.objectContaining({ msgtype: 'm.notice' }),
|
|
387
|
+
}),
|
|
347
388
|
)
|
|
348
389
|
})
|
|
349
390
|
|
|
@@ -367,7 +408,7 @@ describe('matrix transport /transactions', () => {
|
|
|
367
408
|
await settleTurn()
|
|
368
409
|
approvals.emit('registered', {
|
|
369
410
|
approvalId: 'a1',
|
|
370
|
-
sessionId: 'sess-$root',
|
|
411
|
+
sessionId: 'sess-$root', // session is keyed on the promoted thread root
|
|
371
412
|
toolCallId: 't1',
|
|
372
413
|
options: [{ optionId: 'allow_once', name: 'Allow', kind: 'allow_once' }],
|
|
373
414
|
})
|
|
@@ -398,11 +439,10 @@ describe('matrix transport /transactions', () => {
|
|
|
398
439
|
},
|
|
399
440
|
],
|
|
400
441
|
})
|
|
401
|
-
expect(approvals.resolve).toHaveBeenCalledWith(
|
|
402
|
-
'
|
|
403
|
-
'
|
|
404
|
-
|
|
405
|
-
)
|
|
442
|
+
expect(approvals.resolve).toHaveBeenCalledWith('sess-$root', 'a1', {
|
|
443
|
+
decision: 'allow',
|
|
444
|
+
optionId: 'allow_once',
|
|
445
|
+
})
|
|
406
446
|
})
|
|
407
447
|
})
|
|
408
448
|
|
|
@@ -454,7 +494,9 @@ describe('dev.zooid.turn.end', () => {
|
|
|
454
494
|
it('reports produced_output: false for a silent turn', async () => {
|
|
455
495
|
const { transport, agents, client } = makeTransport()
|
|
456
496
|
// The agent emits no chunks at all — this is exactly ZOD076's case.
|
|
457
|
-
agents.prompt.mockImplementation(async () => ({
|
|
497
|
+
agents.prompt.mockImplementation(async () => ({
|
|
498
|
+
stopReason: 'end_turn' as const,
|
|
499
|
+
}))
|
|
458
500
|
await postTxn(transport.app, { events: [topLevelMention()] })
|
|
459
501
|
await settleTurn()
|
|
460
502
|
|
|
@@ -482,7 +524,9 @@ describe('dev.zooid.turn.end', () => {
|
|
|
482
524
|
|
|
483
525
|
it('threads to the turn root', async () => {
|
|
484
526
|
const { transport, agents, client } = makeTransport()
|
|
485
|
-
agents.prompt.mockImplementation(async () => ({
|
|
527
|
+
agents.prompt.mockImplementation(async () => ({
|
|
528
|
+
stopReason: 'end_turn' as const,
|
|
529
|
+
}))
|
|
486
530
|
await postTxn(transport.app, { events: [topLevelMention()] })
|
|
487
531
|
await settleTurn()
|
|
488
532
|
|
|
@@ -497,7 +541,7 @@ describe('dev.zooid.turn.end', () => {
|
|
|
497
541
|
|
|
498
542
|
describe('thread implicit triggers', () => {
|
|
499
543
|
it('triggers the most-recent-posting agent for a bare reply in a thread', async () => {
|
|
500
|
-
const { transport, agents } = makeTransport()
|
|
544
|
+
const { transport, agents, finishPrompt } = makeTransport()
|
|
501
545
|
// Turn 1: user @mentions architect at top level. Agent-promotion makes
|
|
502
546
|
// $root the thread root and architect a thread participant.
|
|
503
547
|
agents.prompt.mockImplementation(async (_name: string, p: { threadId: string }) => {
|
|
@@ -545,13 +589,16 @@ describe('thread implicit triggers', () => {
|
|
|
545
589
|
await settleTurn()
|
|
546
590
|
|
|
547
591
|
// architect should be triggered implicitly because they posted in the thread.
|
|
548
|
-
expect(agents.ensureSession).toHaveBeenCalledWith(
|
|
592
|
+
expect(agents.ensureSession).toHaveBeenCalledWith(
|
|
593
|
+
'architect',
|
|
594
|
+
'$root',
|
|
595
|
+
'!r:example.com',
|
|
596
|
+
'$root',
|
|
597
|
+
)
|
|
549
598
|
})
|
|
550
599
|
|
|
551
600
|
it("inherits the thread root's @mentions when no agent has posted yet", async () => {
|
|
552
|
-
const { transport, agents } = makeTransport()
|
|
553
|
-
// Have prompt() never resolve, so no agent reply has landed when turn 2 arrives.
|
|
554
|
-
agents.prompt.mockImplementation(() => new Promise(() => {}))
|
|
601
|
+
const { transport, agents, finishPrompt } = makeTransport()
|
|
555
602
|
await postTxn(transport.app, {
|
|
556
603
|
events: [
|
|
557
604
|
{
|
|
@@ -587,8 +634,17 @@ describe('thread implicit triggers', () => {
|
|
|
587
634
|
],
|
|
588
635
|
})
|
|
589
636
|
await settleTurn()
|
|
590
|
-
//
|
|
591
|
-
expect(agents.ensureSession).
|
|
637
|
+
// Per-session FIFO holds a fast follow-up until the original turn ends.
|
|
638
|
+
expect(agents.ensureSession).not.toHaveBeenCalled()
|
|
639
|
+
finishPrompt()
|
|
640
|
+
await settleTurn()
|
|
641
|
+
// It then inherits the root's @mention of architect.
|
|
642
|
+
expect(agents.ensureSession).toHaveBeenCalledWith(
|
|
643
|
+
'architect',
|
|
644
|
+
'$root',
|
|
645
|
+
'!r:example.com',
|
|
646
|
+
'$root',
|
|
647
|
+
)
|
|
592
648
|
})
|
|
593
649
|
|
|
594
650
|
it('does not trigger any agent for a bare top-level message', async () => {
|
|
@@ -653,7 +709,12 @@ describe('thread implicit triggers', () => {
|
|
|
653
709
|
],
|
|
654
710
|
})
|
|
655
711
|
await settleTurn()
|
|
656
|
-
expect(agents.ensureSession).toHaveBeenCalledWith(
|
|
712
|
+
expect(agents.ensureSession).toHaveBeenCalledWith(
|
|
713
|
+
'architect',
|
|
714
|
+
'$root',
|
|
715
|
+
'!r:example.com',
|
|
716
|
+
'$root',
|
|
717
|
+
)
|
|
657
718
|
})
|
|
658
719
|
})
|
|
659
720
|
|
|
@@ -728,7 +789,9 @@ describe('dev.zooid.session_reset', () => {
|
|
|
728
789
|
event_id: '$reset',
|
|
729
790
|
room_id: '!r:example.com',
|
|
730
791
|
sender: '@alice:example.com',
|
|
731
|
-
content: {
|
|
792
|
+
content: {
|
|
793
|
+
'm.relates_to': { rel_type: 'm.thread', event_id: '$root' },
|
|
794
|
+
},
|
|
732
795
|
},
|
|
733
796
|
],
|
|
734
797
|
})
|
|
@@ -754,7 +817,12 @@ describe('dev.zooid.session_reset', () => {
|
|
|
754
817
|
],
|
|
755
818
|
})
|
|
756
819
|
await settleTurn()
|
|
757
|
-
expect(agents.ensureSession).toHaveBeenCalledWith(
|
|
820
|
+
expect(agents.ensureSession).toHaveBeenCalledWith(
|
|
821
|
+
'architect',
|
|
822
|
+
'$root',
|
|
823
|
+
'!r:example.com',
|
|
824
|
+
'$root',
|
|
825
|
+
)
|
|
758
826
|
})
|
|
759
827
|
})
|
|
760
828
|
|
|
@@ -847,10 +915,10 @@ describe('typing indicator lifecycle', () => {
|
|
|
847
915
|
room_id: '!r:example.com',
|
|
848
916
|
sender: '@user:example.com',
|
|
849
917
|
content: {
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
918
|
+
msgtype: 'm.text',
|
|
919
|
+
body: 'hi',
|
|
920
|
+
'm.mentions': { user_ids: ['@architect:example.com'] },
|
|
921
|
+
},
|
|
854
922
|
},
|
|
855
923
|
],
|
|
856
924
|
})
|
|
@@ -908,7 +976,9 @@ describe('presence lifecycle', () => {
|
|
|
908
976
|
expect(seenUnavailable).toBe(true)
|
|
909
977
|
finishPrompt()
|
|
910
978
|
await settleTurn()
|
|
911
|
-
const last = client.setPresence.mock.calls.at(-1)?.[0] as {
|
|
979
|
+
const last = client.setPresence.mock.calls.at(-1)?.[0] as {
|
|
980
|
+
presence: string
|
|
981
|
+
}
|
|
912
982
|
expect(last.presence).toBe('online')
|
|
913
983
|
})
|
|
914
984
|
|
|
@@ -1064,8 +1134,7 @@ describe('tool-call and plan event bridging', () => {
|
|
|
1064
1134
|
await settleTurn()
|
|
1065
1135
|
|
|
1066
1136
|
const call = client.sendCustomEvent.mock.calls.find(
|
|
1067
|
-
([arg]) =>
|
|
1068
|
-
(arg as { eventType: string }).eventType === 'dev.zooid.available_commands_update',
|
|
1137
|
+
([arg]) => (arg as { eventType: string }).eventType === 'dev.zooid.available_commands_update',
|
|
1069
1138
|
)
|
|
1070
1139
|
expect(call).toBeDefined()
|
|
1071
1140
|
expect((call![0] as { content: Record<string, unknown> }).content).toMatchObject({
|
|
@@ -1205,9 +1274,7 @@ describe('agent_message_chunk message-boundary buffering', () => {
|
|
|
1205
1274
|
(client.sendMessage.mock.calls[0]![0] as { content: { body: string } }).content.body
|
|
1206
1275
|
|
|
1207
1276
|
const sentBodies = (client: { sendMessage: { mock: { calls: unknown[][] } } }) =>
|
|
1208
|
-
client.sendMessage.mock.calls.map(
|
|
1209
|
-
(c) => (c[0] as { content: { body: string } }).content.body,
|
|
1210
|
-
)
|
|
1277
|
+
client.sendMessage.mock.calls.map((c) => (c[0] as { content: { body: string } }).content.body)
|
|
1211
1278
|
|
|
1212
1279
|
it('flushes a separate Matrix message when messageId changes (opencode run-on)', async () => {
|
|
1213
1280
|
// opencode streams "…it." under one message id, then "Filed:" under a NEW
|
|
@@ -1305,7 +1372,11 @@ describe('dev.zooid.interrupt handling', () => {
|
|
|
1305
1372
|
origin_server_ts: Date.now(),
|
|
1306
1373
|
room_id: '!r:example.com',
|
|
1307
1374
|
sender: '@user:example.com',
|
|
1308
|
-
content: {
|
|
1375
|
+
content: {
|
|
1376
|
+
msgtype: 'm.text',
|
|
1377
|
+
body: 'hi',
|
|
1378
|
+
'm.mentions': { user_ids: ['@architect:example.com'] },
|
|
1379
|
+
},
|
|
1309
1380
|
},
|
|
1310
1381
|
],
|
|
1311
1382
|
})
|
|
@@ -1338,7 +1409,11 @@ describe('dev.zooid.interrupt handling', () => {
|
|
|
1338
1409
|
origin_server_ts: Date.now(),
|
|
1339
1410
|
room_id: '!r:example.com',
|
|
1340
1411
|
sender: '@user:example.com',
|
|
1341
|
-
content: {
|
|
1412
|
+
content: {
|
|
1413
|
+
msgtype: 'm.text',
|
|
1414
|
+
body: 'hi',
|
|
1415
|
+
'm.mentions': { user_ids: ['@architect:example.com'] },
|
|
1416
|
+
},
|
|
1342
1417
|
},
|
|
1343
1418
|
],
|
|
1344
1419
|
})
|
|
@@ -1387,7 +1462,11 @@ describe('dev.zooid.interrupt handling', () => {
|
|
|
1387
1462
|
origin_server_ts: Date.now(),
|
|
1388
1463
|
room_id: '!r:example.com',
|
|
1389
1464
|
sender: '@user:example.com',
|
|
1390
|
-
content: {
|
|
1465
|
+
content: {
|
|
1466
|
+
msgtype: 'm.text',
|
|
1467
|
+
body: 'hi',
|
|
1468
|
+
'm.mentions': { user_ids: ['@architect:example.com'] },
|
|
1469
|
+
},
|
|
1391
1470
|
},
|
|
1392
1471
|
],
|
|
1393
1472
|
})
|
|
@@ -1423,7 +1502,11 @@ describe('dev.zooid.interrupt handling', () => {
|
|
|
1423
1502
|
origin_server_ts: Date.now(),
|
|
1424
1503
|
room_id: '!r:example.com',
|
|
1425
1504
|
sender: '@user:example.com',
|
|
1426
|
-
content: {
|
|
1505
|
+
content: {
|
|
1506
|
+
msgtype: 'm.text',
|
|
1507
|
+
body: 'hi',
|
|
1508
|
+
'm.mentions': { user_ids: ['@architect:example.com'] },
|
|
1509
|
+
},
|
|
1427
1510
|
},
|
|
1428
1511
|
],
|
|
1429
1512
|
})
|
|
@@ -1483,14 +1566,19 @@ describe('full loop integration', () => {
|
|
|
1483
1566
|
|
|
1484
1567
|
// Turn 1: top-level mention.
|
|
1485
1568
|
await postTxn(transport.app, {
|
|
1486
|
-
events: [
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1569
|
+
events: [
|
|
1570
|
+
{
|
|
1571
|
+
type: 'm.room.message',
|
|
1572
|
+
event_id: '$root',
|
|
1573
|
+
room_id: '!r:example.com',
|
|
1574
|
+
sender: '@alice:example.com',
|
|
1575
|
+
content: {
|
|
1576
|
+
msgtype: 'm.text',
|
|
1577
|
+
body: 'hi @architect',
|
|
1578
|
+
'm.mentions': { user_ids: ['@architect:example.com'] },
|
|
1579
|
+
},
|
|
1492
1580
|
},
|
|
1493
|
-
|
|
1581
|
+
],
|
|
1494
1582
|
})
|
|
1495
1583
|
await settleTurn()
|
|
1496
1584
|
expect(client.sendMessage).toHaveBeenCalledWith(
|
|
@@ -1501,17 +1589,27 @@ describe('full loop integration', () => {
|
|
|
1501
1589
|
agents.ensureSession.mockClear()
|
|
1502
1590
|
client.sendMessage.mockClear()
|
|
1503
1591
|
await postTxn(transport.app, {
|
|
1504
|
-
events: [
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1592
|
+
events: [
|
|
1593
|
+
{
|
|
1594
|
+
type: 'm.room.message',
|
|
1595
|
+
event_id: '$bare',
|
|
1596
|
+
room_id: '!r:example.com',
|
|
1597
|
+
sender: '@alice:example.com',
|
|
1598
|
+
content: {
|
|
1599
|
+
msgtype: 'm.text',
|
|
1600
|
+
body: 'follow up',
|
|
1601
|
+
'm.relates_to': { rel_type: 'm.thread', event_id: '$root' },
|
|
1602
|
+
},
|
|
1510
1603
|
},
|
|
1511
|
-
|
|
1604
|
+
],
|
|
1512
1605
|
})
|
|
1513
1606
|
await settleTurn()
|
|
1514
|
-
expect(agents.ensureSession).toHaveBeenCalledWith(
|
|
1607
|
+
expect(agents.ensureSession).toHaveBeenCalledWith(
|
|
1608
|
+
'architect',
|
|
1609
|
+
'$root',
|
|
1610
|
+
'!r:example.com',
|
|
1611
|
+
'$root',
|
|
1612
|
+
)
|
|
1515
1613
|
expect(client.sendMessage).toHaveBeenCalledWith(
|
|
1516
1614
|
expect.objectContaining({ threadRoot: '$root' }),
|
|
1517
1615
|
)
|
|
@@ -1526,7 +1624,10 @@ const TINY_PNG = Buffer.from(TINY_PNG_B64, 'base64')
|
|
|
1526
1624
|
|
|
1527
1625
|
function fakeMedia() {
|
|
1528
1626
|
return {
|
|
1529
|
-
download: vi.fn(async () => ({
|
|
1627
|
+
download: vi.fn(async () => ({
|
|
1628
|
+
data: new Uint8Array(TINY_PNG),
|
|
1629
|
+
contentType: 'image/png',
|
|
1630
|
+
})),
|
|
1530
1631
|
upload: vi.fn(async () => ({ content_uri: 'mxc://localhost/up1' })),
|
|
1531
1632
|
}
|
|
1532
1633
|
}
|
|
@@ -1537,10 +1638,12 @@ const workspaceBinding = {
|
|
|
1537
1638
|
agentWorkspacePath: '/workspace',
|
|
1538
1639
|
}
|
|
1539
1640
|
|
|
1540
|
-
function makeMediaTransport(
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1641
|
+
function makeMediaTransport(
|
|
1642
|
+
opts: {
|
|
1643
|
+
media?: ReturnType<typeof fakeMedia>
|
|
1644
|
+
writeAttachmentFn?: unknown
|
|
1645
|
+
} = {},
|
|
1646
|
+
) {
|
|
1544
1647
|
const { reg, finishPrompt } = fakeRegistry()
|
|
1545
1648
|
const approvals = fakeApprovals()
|
|
1546
1649
|
const client = fakeClient()
|
|
@@ -1557,13 +1660,15 @@ function makeMediaTransport(opts: {
|
|
|
1557
1660
|
return { transport, agents: reg, client, finishPrompt }
|
|
1558
1661
|
}
|
|
1559
1662
|
|
|
1560
|
-
function imageEvent(
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1663
|
+
function imageEvent(
|
|
1664
|
+
over: {
|
|
1665
|
+
size?: number
|
|
1666
|
+
mimetype?: string
|
|
1667
|
+
msgtype?: string
|
|
1668
|
+
body?: string
|
|
1669
|
+
eventId?: string
|
|
1670
|
+
} = {},
|
|
1671
|
+
) {
|
|
1567
1672
|
return {
|
|
1568
1673
|
type: 'm.room.message',
|
|
1569
1674
|
event_id: over.eventId ?? '$media1',
|
|
@@ -1616,7 +1721,11 @@ describe('inbound media', () => {
|
|
|
1616
1721
|
|
|
1617
1722
|
expect(agents.prompt).toHaveBeenCalledOnce()
|
|
1618
1723
|
const content = (agents.prompt.mock.calls[0][1] as { content: unknown[] }).content
|
|
1619
|
-
expect(content[0]).toMatchObject({
|
|
1724
|
+
expect(content[0]).toMatchObject({
|
|
1725
|
+
type: 'image',
|
|
1726
|
+
data: TINY_PNG_B64,
|
|
1727
|
+
mimeType: 'image/png',
|
|
1728
|
+
})
|
|
1620
1729
|
expect((content[1] as { type: string; text: string }).type).toBe('text')
|
|
1621
1730
|
expect(media.download).toHaveBeenCalledOnce()
|
|
1622
1731
|
})
|
|
@@ -1627,7 +1736,10 @@ describe('inbound media', () => {
|
|
|
1627
1736
|
hostPath: '/tmp/ws/.zooid/attachments/media1/dog.png',
|
|
1628
1737
|
agentPath: '/workspace/.zooid/attachments/media1/dog.png',
|
|
1629
1738
|
}))
|
|
1630
|
-
const { transport, agents } = makeMediaTransport({
|
|
1739
|
+
const { transport, agents } = makeMediaTransport({
|
|
1740
|
+
media,
|
|
1741
|
+
writeAttachmentFn,
|
|
1742
|
+
})
|
|
1631
1743
|
|
|
1632
1744
|
agents.prompt.mockResolvedValue({ stopReason: 'end_turn' as const })
|
|
1633
1745
|
await postTxn(transport.app, { events: [imageEvent({ size: 600_000 })] }) // > MAX_INLINE_IMAGE_BYTES
|
|
@@ -1651,11 +1763,20 @@ describe('inbound media', () => {
|
|
|
1651
1763
|
hostPath: '/tmp/ws/.zooid/attachments/media1/report.pdf',
|
|
1652
1764
|
agentPath: '/workspace/.zooid/attachments/media1/report.pdf',
|
|
1653
1765
|
}))
|
|
1654
|
-
const { transport, agents } = makeMediaTransport({
|
|
1766
|
+
const { transport, agents } = makeMediaTransport({
|
|
1767
|
+
media,
|
|
1768
|
+
writeAttachmentFn,
|
|
1769
|
+
})
|
|
1655
1770
|
|
|
1656
1771
|
agents.prompt.mockResolvedValue({ stopReason: 'end_turn' as const })
|
|
1657
1772
|
await postTxn(transport.app, {
|
|
1658
|
-
events: [
|
|
1773
|
+
events: [
|
|
1774
|
+
imageEvent({
|
|
1775
|
+
msgtype: 'm.file',
|
|
1776
|
+
body: 'report.pdf',
|
|
1777
|
+
mimetype: 'application/pdf',
|
|
1778
|
+
}),
|
|
1779
|
+
],
|
|
1659
1780
|
})
|
|
1660
1781
|
await postTxn(transport.app, { events: [mentionMsg('read it')] })
|
|
1661
1782
|
await settleTurn()
|
|
@@ -1721,7 +1842,10 @@ describe('outbound agent images', () => {
|
|
|
1721
1842
|
await new Promise((r) => setTimeout(r, 10))
|
|
1722
1843
|
|
|
1723
1844
|
expect(media.upload).toHaveBeenCalledWith(
|
|
1724
|
-
expect.objectContaining({
|
|
1845
|
+
expect.objectContaining({
|
|
1846
|
+
contentType: 'image/png',
|
|
1847
|
+
asUserId: '@architect:example.com',
|
|
1848
|
+
}),
|
|
1725
1849
|
)
|
|
1726
1850
|
expect(client.sendMessage).toHaveBeenCalledWith(
|
|
1727
1851
|
expect.objectContaining({
|
|
@@ -1729,7 +1853,10 @@ describe('outbound agent images', () => {
|
|
|
1729
1853
|
content: expect.objectContaining({
|
|
1730
1854
|
msgtype: 'm.image',
|
|
1731
1855
|
url: 'mxc://localhost/up1',
|
|
1732
|
-
info: expect.objectContaining({
|
|
1856
|
+
info: expect.objectContaining({
|
|
1857
|
+
mimetype: 'image/png',
|
|
1858
|
+
size: TINY_PNG.length,
|
|
1859
|
+
}),
|
|
1733
1860
|
}),
|
|
1734
1861
|
}),
|
|
1735
1862
|
)
|
|
@@ -1773,17 +1900,19 @@ describe('ad-hoc bot invite declines', () => {
|
|
|
1773
1900
|
|
|
1774
1901
|
it('declines an invite to the AS bot from a human, with a reason', async () => {
|
|
1775
1902
|
const { transport, client } = makeTransport()
|
|
1776
|
-
await postTxn(transport.app, {
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
)
|
|
1903
|
+
await postTxn(transport.app, {
|
|
1904
|
+
events: [invite('@zooid:example.com', '@zongshan:example.com')],
|
|
1905
|
+
})
|
|
1906
|
+
expect(client.leaveRoom).toHaveBeenCalledWith('!r:example.com', '@zooid:example.com', {
|
|
1907
|
+
reason: expect.stringContaining('zooid.yaml'),
|
|
1908
|
+
})
|
|
1782
1909
|
})
|
|
1783
1910
|
|
|
1784
1911
|
it('declines an invite to an agent from a human', async () => {
|
|
1785
1912
|
const { transport, client } = makeTransport()
|
|
1786
|
-
await postTxn(transport.app, {
|
|
1913
|
+
await postTxn(transport.app, {
|
|
1914
|
+
events: [invite('@architect:example.com', '@zongshan:example.com')],
|
|
1915
|
+
})
|
|
1787
1916
|
expect(client.leaveRoom).toHaveBeenCalledWith(
|
|
1788
1917
|
'!r:example.com',
|
|
1789
1918
|
'@architect:example.com',
|
|
@@ -1793,13 +1922,17 @@ describe('ad-hoc bot invite declines', () => {
|
|
|
1793
1922
|
|
|
1794
1923
|
it('does NOT decline a provisioning invite (inviter is our AS bot)', async () => {
|
|
1795
1924
|
const { transport, client } = makeTransport()
|
|
1796
|
-
await postTxn(transport.app, {
|
|
1925
|
+
await postTxn(transport.app, {
|
|
1926
|
+
events: [invite('@architect:example.com', '@zooid:example.com')],
|
|
1927
|
+
})
|
|
1797
1928
|
expect(client.leaveRoom).not.toHaveBeenCalled()
|
|
1798
1929
|
})
|
|
1799
1930
|
|
|
1800
1931
|
it('ignores an invite to a human (not one of our bots)', async () => {
|
|
1801
1932
|
const { transport, client } = makeTransport()
|
|
1802
|
-
await postTxn(transport.app, {
|
|
1933
|
+
await postTxn(transport.app, {
|
|
1934
|
+
events: [invite('@dave:example.com', '@zongshan:example.com')],
|
|
1935
|
+
})
|
|
1803
1936
|
expect(client.leaveRoom).not.toHaveBeenCalled()
|
|
1804
1937
|
})
|
|
1805
1938
|
})
|
|
@@ -1825,7 +1958,9 @@ describe('directional agent-to-agent handoffs', () => {
|
|
|
1825
1958
|
const approvals = fakeApprovals()
|
|
1826
1959
|
const client = fakeClient()
|
|
1827
1960
|
// Resolve prompts immediately so runTurn completes and `participants` append.
|
|
1828
|
-
reg.prompt.mockImplementation(async () => ({
|
|
1961
|
+
reg.prompt.mockImplementation(async () => ({
|
|
1962
|
+
stopReason: 'end_turn' as const,
|
|
1963
|
+
}))
|
|
1829
1964
|
const transport = createMatrixTransport({
|
|
1830
1965
|
agents: reg as never,
|
|
1831
1966
|
approvals: approvals as never,
|
|
@@ -1849,7 +1984,13 @@ describe('directional agent-to-agent handoffs', () => {
|
|
|
1849
1984
|
if (o.root) content['m.relates_to'] = { rel_type: 'm.thread', event_id: o.root }
|
|
1850
1985
|
return postTxn(transport.app, {
|
|
1851
1986
|
events: [
|
|
1852
|
-
{
|
|
1987
|
+
{
|
|
1988
|
+
type: 'm.room.message',
|
|
1989
|
+
event_id: o.id,
|
|
1990
|
+
room_id: '!r:example.com',
|
|
1991
|
+
sender: o.sender,
|
|
1992
|
+
content,
|
|
1993
|
+
},
|
|
1853
1994
|
],
|
|
1854
1995
|
})
|
|
1855
1996
|
}
|
|
@@ -1858,19 +1999,33 @@ describe('directional agent-to-agent handoffs', () => {
|
|
|
1858
1999
|
const { transport, agents } = makePairTransport()
|
|
1859
2000
|
|
|
1860
2001
|
// 1. Human @mentions parent (top-level) → thread root is $root.
|
|
1861
|
-
await post(transport, {
|
|
2002
|
+
await post(transport, {
|
|
2003
|
+
id: '$root',
|
|
2004
|
+
sender: '@alice:example.com',
|
|
2005
|
+
mentions: ['@parent:example.com'],
|
|
2006
|
+
})
|
|
1862
2007
|
await settleTurn()
|
|
1863
2008
|
|
|
1864
2009
|
// 2. Parent replies in-thread @mentioning sub → sub is called; caller[sub]=parent.
|
|
1865
2010
|
// [[ZOD071]]: this call opens a fresh handoff arc for sub, keyed by $p1.
|
|
1866
|
-
await post(transport, {
|
|
2011
|
+
await post(transport, {
|
|
2012
|
+
id: '$p1',
|
|
2013
|
+
sender: '@parent:example.com',
|
|
2014
|
+
root: '$root',
|
|
2015
|
+
mentions: ['@sub:example.com'],
|
|
2016
|
+
})
|
|
1867
2017
|
await settleTurn()
|
|
1868
2018
|
expect(agents.ensureSession).toHaveBeenCalledWith('sub', '$root|$p1', '!r:example.com', '$root')
|
|
1869
2019
|
|
|
1870
2020
|
agents.ensureSession.mockClear()
|
|
1871
2021
|
|
|
1872
2022
|
// 3. Sub replies bare → bubbles UP to its caller (parent), nobody else.
|
|
1873
|
-
await post(transport, {
|
|
2023
|
+
await post(transport, {
|
|
2024
|
+
id: '$s1',
|
|
2025
|
+
sender: '@sub:example.com',
|
|
2026
|
+
root: '$root',
|
|
2027
|
+
})
|
|
2028
|
+
await postTurnEnd(transport.app, { agent: 'sub', root: '$root' })
|
|
1874
2029
|
await settleTurn()
|
|
1875
2030
|
expect(agents.ensureSession).toHaveBeenCalledWith('parent', '$root', '!r:example.com', '$root')
|
|
1876
2031
|
expect(agents.ensureSession.mock.calls.map((c) => c[0])).not.toContain('sub')
|
|
@@ -1879,11 +2034,63 @@ describe('directional agent-to-agent handoffs', () => {
|
|
|
1879
2034
|
|
|
1880
2035
|
// 4. Parent replies bare (the "ack") → parent has no caller → routes to NOBODY.
|
|
1881
2036
|
// This is the loop guard: sub is NOT re-triggered.
|
|
1882
|
-
await post(transport, {
|
|
2037
|
+
await post(transport, {
|
|
2038
|
+
id: '$p2',
|
|
2039
|
+
sender: '@parent:example.com',
|
|
2040
|
+
root: '$root',
|
|
2041
|
+
})
|
|
1883
2042
|
await settleTurn()
|
|
1884
2043
|
expect(agents.ensureSession).not.toHaveBeenCalled()
|
|
1885
2044
|
})
|
|
1886
2045
|
|
|
2046
|
+
it('coalesces every message in a callee turn into one return, including an explicit @caller', async () => {
|
|
2047
|
+
const { transport, agents } = makePairTransport()
|
|
2048
|
+
|
|
2049
|
+
await post(transport, {
|
|
2050
|
+
id: '$root',
|
|
2051
|
+
sender: '@alice:example.com',
|
|
2052
|
+
mentions: ['@parent:example.com'],
|
|
2053
|
+
})
|
|
2054
|
+
await settleTurn()
|
|
2055
|
+
await post(transport, {
|
|
2056
|
+
id: '$p1',
|
|
2057
|
+
sender: '@parent:example.com',
|
|
2058
|
+
root: '$root',
|
|
2059
|
+
mentions: ['@sub:example.com'],
|
|
2060
|
+
})
|
|
2061
|
+
await settleTurn()
|
|
2062
|
+
agents.ensureSession.mockClear()
|
|
2063
|
+
agents.prompt.mockClear()
|
|
2064
|
+
|
|
2065
|
+
// Tool boundaries can flush several assistant messages in one turn. The
|
|
2066
|
+
// last one may explicitly address the caller; none may wake it early.
|
|
2067
|
+
await post(transport, { id: '$s1', sender: '@sub:example.com', root: '$root' })
|
|
2068
|
+
await post(transport, {
|
|
2069
|
+
id: '$s2',
|
|
2070
|
+
sender: '@sub:example.com',
|
|
2071
|
+
root: '$root',
|
|
2072
|
+
mentions: ['@parent:example.com'],
|
|
2073
|
+
})
|
|
2074
|
+
await settleTurn()
|
|
2075
|
+
expect(agents.ensureSession).not.toHaveBeenCalled()
|
|
2076
|
+
|
|
2077
|
+
await postTurnEnd(transport.app, { agent: 'sub', root: '$root' })
|
|
2078
|
+
await settleTurn()
|
|
2079
|
+
expect(agents.ensureSession).toHaveBeenCalledTimes(1)
|
|
2080
|
+
expect(agents.ensureSession).toHaveBeenCalledWith(
|
|
2081
|
+
'parent',
|
|
2082
|
+
'$root',
|
|
2083
|
+
'!r:example.com',
|
|
2084
|
+
'$root',
|
|
2085
|
+
)
|
|
2086
|
+
expect(agents.prompt).toHaveBeenCalledWith(
|
|
2087
|
+
'parent',
|
|
2088
|
+
expect.objectContaining({
|
|
2089
|
+
content: [expect.objectContaining({ type: 'text', text: 'x\n\nx' })],
|
|
2090
|
+
}),
|
|
2091
|
+
)
|
|
2092
|
+
})
|
|
2093
|
+
|
|
1887
2094
|
it('rebuildThreadState reconstructs caller[] from the timeline after a restart', async () => {
|
|
1888
2095
|
const client = {
|
|
1889
2096
|
// root: human @mentions parent (no caller — human sender).
|
|
@@ -1900,7 +2107,11 @@ describe('directional agent-to-agent handoffs', () => {
|
|
|
1900
2107
|
sender: '@parent:example.com',
|
|
1901
2108
|
content: { 'm.mentions': { user_ids: ['@sub:example.com'] } },
|
|
1902
2109
|
},
|
|
1903
|
-
{
|
|
2110
|
+
{
|
|
2111
|
+
type: 'm.room.message',
|
|
2112
|
+
sender: '@sub:example.com',
|
|
2113
|
+
content: { body: 'ack' },
|
|
2114
|
+
},
|
|
1904
2115
|
],
|
|
1905
2116
|
})),
|
|
1906
2117
|
}
|
|
@@ -1970,7 +2181,6 @@ describe('per-handoff session isolation ([[ZOD071]])', () => {
|
|
|
1970
2181
|
],
|
|
1971
2182
|
})
|
|
1972
2183
|
}
|
|
1973
|
-
|
|
1974
2184
|
const agentsCalled = (reg: ReturnType<typeof fakeRegistry>['reg']) =>
|
|
1975
2185
|
reg.ensureSession.mock.calls.map((c) => c[0] as string)
|
|
1976
2186
|
|
|
@@ -1985,12 +2195,7 @@ describe('per-handoff session isolation ([[ZOD071]])', () => {
|
|
|
1985
2195
|
mentions: ['@parent:example.com'],
|
|
1986
2196
|
})
|
|
1987
2197
|
await settleTurn()
|
|
1988
|
-
expect(agents.ensureSession).toHaveBeenCalledWith(
|
|
1989
|
-
'parent',
|
|
1990
|
-
'$root',
|
|
1991
|
-
'!r:example.com',
|
|
1992
|
-
'$root',
|
|
1993
|
-
)
|
|
2198
|
+
expect(agents.ensureSession).toHaveBeenCalledWith('parent', '$root', '!r:example.com', '$root')
|
|
1994
2199
|
|
|
1995
2200
|
// 2. Parent calls BOTH subs in ONE message ($p1). Each sub gets a fresh
|
|
1996
2201
|
// arc session keyed by the same call event id — the composed key
|
|
@@ -2026,14 +2231,14 @@ describe('per-handoff session isolation ([[ZOD071]])', () => {
|
|
|
2026
2231
|
// thread-level session; the sibling is untouched.
|
|
2027
2232
|
gates.get('rocksteady')!()
|
|
2028
2233
|
await settleTurn()
|
|
2029
|
-
await post(transport, {
|
|
2234
|
+
await post(transport, {
|
|
2235
|
+
id: '$r1',
|
|
2236
|
+
sender: '@rocksteady:example.com',
|
|
2237
|
+
root: '$root',
|
|
2238
|
+
})
|
|
2239
|
+
await postTurnEnd(transport.app, { agent: 'rocksteady', root: '$root' })
|
|
2030
2240
|
await settleTurn()
|
|
2031
|
-
expect(agents.ensureSession).toHaveBeenCalledWith(
|
|
2032
|
-
'parent',
|
|
2033
|
-
'$root',
|
|
2034
|
-
'!r:example.com',
|
|
2035
|
-
'$root',
|
|
2036
|
-
)
|
|
2241
|
+
expect(agents.ensureSession).toHaveBeenCalledWith('parent', '$root', '!r:example.com', '$root')
|
|
2037
2242
|
expect(agentsCalled(agents)).not.toContain('bebop')
|
|
2038
2243
|
|
|
2039
2244
|
agents.ensureSession.mockClear()
|
|
@@ -2043,14 +2248,14 @@ describe('per-handoff session isolation ([[ZOD071]])', () => {
|
|
|
2043
2248
|
// SAME thread-level session (it aggregates the two results).
|
|
2044
2249
|
gates.get('bebop')!()
|
|
2045
2250
|
await settleTurn()
|
|
2046
|
-
await post(transport, {
|
|
2251
|
+
await post(transport, {
|
|
2252
|
+
id: '$b1',
|
|
2253
|
+
sender: '@bebop:example.com',
|
|
2254
|
+
root: '$root',
|
|
2255
|
+
})
|
|
2256
|
+
await postTurnEnd(transport.app, { agent: 'bebop', root: '$root' })
|
|
2047
2257
|
await settleTurn()
|
|
2048
|
-
expect(agents.ensureSession).toHaveBeenCalledWith(
|
|
2049
|
-
'parent',
|
|
2050
|
-
'$root',
|
|
2051
|
-
'!r:example.com',
|
|
2052
|
-
'$root',
|
|
2053
|
-
)
|
|
2258
|
+
expect(agents.ensureSession).toHaveBeenCalledWith('parent', '$root', '!r:example.com', '$root')
|
|
2054
2259
|
expect(agentsCalled(agents)).not.toContain('rocksteady')
|
|
2055
2260
|
})
|
|
2056
2261
|
|
|
@@ -2074,7 +2279,12 @@ describe('per-handoff session isolation ([[ZOD071]])', () => {
|
|
|
2074
2279
|
await settleTurn()
|
|
2075
2280
|
gates.get('bebop')!()
|
|
2076
2281
|
await settleTurn()
|
|
2077
|
-
await post(transport, {
|
|
2282
|
+
await post(transport, {
|
|
2283
|
+
id: '$b1',
|
|
2284
|
+
sender: '@bebop:example.com',
|
|
2285
|
+
root: '$root',
|
|
2286
|
+
})
|
|
2287
|
+
await postTurnEnd(transport.app, { agent: 'bebop', root: '$root' })
|
|
2078
2288
|
await settleTurn()
|
|
2079
2289
|
|
|
2080
2290
|
// Second delegation: arc $p2 — a NEW session key, not a resume.
|
|
@@ -2117,7 +2327,11 @@ describe('per-handoff session isolation ([[ZOD071]])', () => {
|
|
|
2117
2327
|
|
|
2118
2328
|
// Human "why did you do X?" — routes to bebop (last poster) and lands in
|
|
2119
2329
|
// the session that DID the work, not a cold thread-level one.
|
|
2120
|
-
await post(transport, {
|
|
2330
|
+
await post(transport, {
|
|
2331
|
+
id: '$h1',
|
|
2332
|
+
sender: '@alice:example.com',
|
|
2333
|
+
root: '$root',
|
|
2334
|
+
})
|
|
2121
2335
|
await settleTurn()
|
|
2122
2336
|
expect(agents.ensureSession).toHaveBeenCalledWith(
|
|
2123
2337
|
'bebop',
|
|
@@ -2154,7 +2368,9 @@ describe('per-handoff session isolation ([[ZOD071]])', () => {
|
|
|
2154
2368
|
event_id: '$reset',
|
|
2155
2369
|
room_id: '!r:example.com',
|
|
2156
2370
|
sender: '@alice:example.com',
|
|
2157
|
-
content: {
|
|
2371
|
+
content: {
|
|
2372
|
+
'm.relates_to': { rel_type: 'm.thread', event_id: '$root' },
|
|
2373
|
+
},
|
|
2158
2374
|
},
|
|
2159
2375
|
],
|
|
2160
2376
|
})
|
|
@@ -2195,7 +2411,9 @@ describe('per-handoff session isolation ([[ZOD071]])', () => {
|
|
|
2195
2411
|
event_id: '$reset',
|
|
2196
2412
|
room_id: '!r:example.com',
|
|
2197
2413
|
sender: '@alice:example.com',
|
|
2198
|
-
content: {
|
|
2414
|
+
content: {
|
|
2415
|
+
'm.relates_to': { rel_type: 'm.thread', event_id: '$root' },
|
|
2416
|
+
},
|
|
2199
2417
|
},
|
|
2200
2418
|
],
|
|
2201
2419
|
})
|
|
@@ -2220,10 +2438,17 @@ describe('per-handoff session isolation ([[ZOD071]])', () => {
|
|
|
2220
2438
|
event_id: '$p1',
|
|
2221
2439
|
sender: '@parent:example.com',
|
|
2222
2440
|
content: {
|
|
2223
|
-
'm.mentions': {
|
|
2441
|
+
'm.mentions': {
|
|
2442
|
+
user_ids: ['@bebop:example.com', '@rocksteady:example.com'],
|
|
2443
|
+
},
|
|
2224
2444
|
},
|
|
2225
2445
|
},
|
|
2226
|
-
{
|
|
2446
|
+
{
|
|
2447
|
+
type: 'm.room.message',
|
|
2448
|
+
event_id: '$b1',
|
|
2449
|
+
sender: '@bebop:example.com',
|
|
2450
|
+
content: { body: 'done' },
|
|
2451
|
+
},
|
|
2227
2452
|
{
|
|
2228
2453
|
type: 'm.room.message',
|
|
2229
2454
|
event_id: '$p2',
|
|
@@ -2234,7 +2459,71 @@ describe('per-handoff session isolation ([[ZOD071]])', () => {
|
|
|
2234
2459
|
})),
|
|
2235
2460
|
}
|
|
2236
2461
|
const state = await rebuildThreadState(client as never, '!r:example.com', '$root', trio)
|
|
2237
|
-
expect(state.handoffs).toEqual({
|
|
2462
|
+
expect(state.handoffs).toEqual({
|
|
2463
|
+
bebop: ['$p1', '$p2'],
|
|
2464
|
+
rocksteady: ['$p1'],
|
|
2465
|
+
})
|
|
2238
2466
|
expect(state.callers).toEqual({ bebop: 'parent', rocksteady: 'parent' })
|
|
2239
2467
|
})
|
|
2240
2468
|
})
|
|
2469
|
+
|
|
2470
|
+
describe('taskActions.describeRole', () => {
|
|
2471
|
+
function makeTaskTransport() {
|
|
2472
|
+
const { reg, finishPrompt } = fakeRegistry()
|
|
2473
|
+
const approvals = fakeApprovals()
|
|
2474
|
+
const client = fakeClient()
|
|
2475
|
+
const bindings = [
|
|
2476
|
+
...baseAgents,
|
|
2477
|
+
{
|
|
2478
|
+
name: 'worker',
|
|
2479
|
+
userId: '@worker:example.com',
|
|
2480
|
+
rooms: [{ alias: '!r:example.com' }],
|
|
2481
|
+
trigger: 'mention' as const,
|
|
2482
|
+
},
|
|
2483
|
+
]
|
|
2484
|
+
const transport = createMatrixTransport({
|
|
2485
|
+
agents: reg as never,
|
|
2486
|
+
approvals: approvals as never,
|
|
2487
|
+
client: client as never,
|
|
2488
|
+
bindings,
|
|
2489
|
+
hsToken: 'hs-secret',
|
|
2490
|
+
botUserId: '@zooid:example.com',
|
|
2491
|
+
drainQuietMs: 0,
|
|
2492
|
+
})
|
|
2493
|
+
return { transport, finishPrompt }
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
it('describeRole reports assignee and depth for a task thread', async () => {
|
|
2497
|
+
const { transport } = makeTaskTransport()
|
|
2498
|
+
const started = await transport.taskActions.startTasks(
|
|
2499
|
+
{
|
|
2500
|
+
agentName: 'architect',
|
|
2501
|
+
channelId: '!r:example.com',
|
|
2502
|
+
threadRoot: '$parent',
|
|
2503
|
+
sessionKey: '$parent',
|
|
2504
|
+
},
|
|
2505
|
+
{ tasks: [{ agent: 'worker', prompt: 'do the thing' }] },
|
|
2506
|
+
)
|
|
2507
|
+
const result = started.results[0]!
|
|
2508
|
+
expect(result.status).toBe('started')
|
|
2509
|
+
const threadId = (result as { thread_id: string }).thread_id
|
|
2510
|
+
const role = await transport.taskActions.describeRole({
|
|
2511
|
+
agentName: 'worker',
|
|
2512
|
+
channelId: '!r:example.com',
|
|
2513
|
+
threadRoot: threadId,
|
|
2514
|
+
sessionKey: threadId,
|
|
2515
|
+
})
|
|
2516
|
+
expect(role).toEqual({ is_task_assignee: true, can_start_task_threads: false })
|
|
2517
|
+
})
|
|
2518
|
+
|
|
2519
|
+
it('describeRole reports a plain mention as neither assignee nor capped', async () => {
|
|
2520
|
+
const { transport } = makeTaskTransport()
|
|
2521
|
+
const role = await transport.taskActions.describeRole({
|
|
2522
|
+
agentName: 'architect',
|
|
2523
|
+
channelId: '!r:example.com',
|
|
2524
|
+
threadRoot: '$other',
|
|
2525
|
+
sessionKey: '$other',
|
|
2526
|
+
})
|
|
2527
|
+
expect(role).toEqual({ is_task_assignee: false, can_start_task_threads: true })
|
|
2528
|
+
})
|
|
2529
|
+
})
|