pi-extended-teams 2.1.16
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 +105 -0
- package/assets/pi-extended-teams-agent-navigation.png +0 -0
- package/assets/pi-extended-teams-in-action.png +0 -0
- package/extensions/agents/read-agent-report.ts +181 -0
- package/extensions/agents/read-agent-session-lifecycle.test.ts +605 -0
- package/extensions/agents/read-agent-session-lifecycle.ts +676 -0
- package/extensions/agents/read-agent.test.ts +3077 -0
- package/extensions/agents/read-agent.ts +1119 -0
- package/extensions/agents/write-agent.test.ts +513 -0
- package/extensions/agents/write-agent.ts +392 -0
- package/extensions/events/register-events.test.ts +465 -0
- package/extensions/events/register-events.ts +409 -0
- package/extensions/index.test.ts +1659 -0
- package/extensions/index.ts +1228 -0
- package/extensions/internal/agent-session-files.test.ts +164 -0
- package/extensions/internal/agent-session-files.ts +320 -0
- package/extensions/internal/debug.ts +44 -0
- package/extensions/internal/model-selection.ts +82 -0
- package/extensions/internal/pi-command.test.ts +191 -0
- package/extensions/internal/pi-command.ts +224 -0
- package/extensions/internal/pi-runtime-api.test.ts +43 -0
- package/extensions/internal/pi-runtime-api.ts +78 -0
- package/extensions/internal/schema.ts +19 -0
- package/extensions/internal/session-context-reference.test.ts +290 -0
- package/extensions/internal/session-context-reference.ts +430 -0
- package/extensions/internal/session-files.test.ts +225 -0
- package/extensions/internal/session-files.ts +273 -0
- package/extensions/internal/session-usage.ts +59 -0
- package/extensions/resources/spawn-resource-plan.test.ts +233 -0
- package/extensions/resources/spawn-resource-plan.ts +247 -0
- package/extensions/runtime/active-agent-sleep.test.ts +157 -0
- package/extensions/runtime/active-agent-sleep.ts +117 -0
- package/extensions/runtime/nested-read-agents.ts +22 -0
- package/extensions/runtime/pending-child-controller.test.ts +169 -0
- package/extensions/runtime/pending-child-controller.ts +280 -0
- package/extensions/runtime/types.ts +68 -0
- package/extensions/team/contracts.test.ts +111 -0
- package/extensions/team/lifecycle.test.ts +1095 -0
- package/extensions/team/lifecycle.ts +502 -0
- package/extensions/team/recipient-closure.test.ts +97 -0
- package/extensions/team/recipient-closure.ts +120 -0
- package/extensions/team/roster.test.ts +129 -0
- package/extensions/team/roster.ts +165 -0
- package/extensions/team/team-contracts.json +43 -0
- package/extensions/team/writer-screens.test.ts +50 -0
- package/extensions/team/writer-screens.ts +156 -0
- package/extensions/tools/agent-communication-tools.test.ts +306 -0
- package/extensions/tools/agent-communication-tools.ts +183 -0
- package/extensions/tools/coordination-tools.test.ts +670 -0
- package/extensions/tools/coordination-tools.ts +304 -0
- package/extensions/tools/delegation-guard.test.ts +95 -0
- package/extensions/tools/delegation-guard.ts +65 -0
- package/extensions/tools/file-claim-tools.test.ts +104 -0
- package/extensions/tools/file-claim-tools.ts +68 -0
- package/extensions/tools/model-tools.ts +53 -0
- package/extensions/tools/predefined-tools.test.ts +647 -0
- package/extensions/tools/predefined-tools.ts +331 -0
- package/extensions/tools/read-helper.test.ts +35 -0
- package/extensions/tools/task-runtime-tools.test.ts +334 -0
- package/extensions/tools/task-runtime-tools.ts +227 -0
- package/extensions/tools/team-tools.read-agent.test.ts +1924 -0
- package/extensions/tools/team-tools.ts +1270 -0
- package/extensions/ui/agent-follow-view.test.ts +520 -0
- package/extensions/ui/agent-follow-view.ts +779 -0
- package/extensions/ui/agent-navigation.test.ts +52 -0
- package/extensions/ui/agent-navigation.ts +55 -0
- package/extensions/ui/ansi.ts +16 -0
- package/extensions/ui/extensions-command.test.ts +336 -0
- package/extensions/ui/extensions-command.ts +282 -0
- package/extensions/ui/favorite-models-command.test.ts +280 -0
- package/extensions/ui/favorite-models-command.ts +423 -0
- package/extensions/ui/frame.ts +77 -0
- package/extensions/ui/input.ts +17 -0
- package/extensions/ui/read-agent-status.test.ts +117 -0
- package/extensions/ui/read-agent-status.ts +125 -0
- package/extensions/ui/renderers.ts +243 -0
- package/extensions/ui/status-widget.test.ts +143 -0
- package/extensions/ui/status-widget.ts +440 -0
- package/extensions/ui-frame.test.ts +69 -0
- package/package.json +75 -0
- package/skills/teams.md +256 -0
- package/src/adapters/terminal-registry.ts +78 -0
- package/src/adapters/tmux-adapter.test.ts +276 -0
- package/src/adapters/tmux-adapter.ts +200 -0
- package/src/orchestration/index.ts +437 -0
- package/src/orchestration/orchestrator.test.ts +396 -0
- package/src/orchestration/types.ts +125 -0
- package/src/utils/atomic-json.ts +21 -0
- package/src/utils/claims.test.ts +137 -0
- package/src/utils/claims.ts +169 -0
- package/src/utils/hooks.test.ts +75 -0
- package/src/utils/hooks.ts +35 -0
- package/src/utils/lifecycle-tombstone.test.ts +105 -0
- package/src/utils/lifecycle-tombstone.ts +268 -0
- package/src/utils/lock.race.child.ts +44 -0
- package/src/utils/lock.race.test.ts +198 -0
- package/src/utils/lock.test.ts +90 -0
- package/src/utils/lock.ts +186 -0
- package/src/utils/messaging.test.ts +337 -0
- package/src/utils/messaging.ts +441 -0
- package/src/utils/model-resolution.test.ts +231 -0
- package/src/utils/model-resolution.ts +322 -0
- package/src/utils/models.test.ts +8 -0
- package/src/utils/models.ts +115 -0
- package/src/utils/paths.ts +81 -0
- package/src/utils/predefined-teams/types.ts +48 -0
- package/src/utils/predefined-teams.save-template.test.ts +74 -0
- package/src/utils/predefined-teams.test.ts +441 -0
- package/src/utils/predefined-teams.ts +471 -0
- package/src/utils/read-helper-queue.ts +99 -0
- package/src/utils/report-events.test.ts +154 -0
- package/src/utils/report-events.ts +222 -0
- package/src/utils/runtime.test.ts +314 -0
- package/src/utils/runtime.ts +261 -0
- package/src/utils/security.test.ts +43 -0
- package/src/utils/settings.test.ts +480 -0
- package/src/utils/settings.ts +645 -0
- package/src/utils/shared-memory.test.ts +80 -0
- package/src/utils/shared-memory.ts +81 -0
- package/src/utils/tasks.race.test.ts +44 -0
- package/src/utils/tasks.test.ts +229 -0
- package/src/utils/tasks.ts +396 -0
- package/src/utils/teams.ts +231 -0
- package/src/utils/terminal-adapter.ts +103 -0
- package/src/utils/thinking-levels.test.ts +56 -0
- package/src/utils/thinking-levels.ts +47 -0
- package/src/utils/workflow-metadata.test.ts +11 -0
- package/src/utils/workflow-metadata.ts +88 -0
- package/src/utils/write-queue.test.ts +251 -0
- package/src/utils/write-queue.ts +202 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import {
|
|
6
|
+
appendMessage,
|
|
7
|
+
broadcastMessage,
|
|
8
|
+
broadcastMessageOnce,
|
|
9
|
+
findInboxMessageByOperation,
|
|
10
|
+
peekInbox,
|
|
11
|
+
readInbox,
|
|
12
|
+
readInboxTail,
|
|
13
|
+
sendPlainMessage,
|
|
14
|
+
sendPlainMessageIfRunning,
|
|
15
|
+
sendPlainMessageOnce,
|
|
16
|
+
} from "./messaging";
|
|
17
|
+
import type { InboxMessage } from "./models";
|
|
18
|
+
import * as paths from "./paths";
|
|
19
|
+
import { closePersistedRecipient } from "../../extensions/team/recipient-closure";
|
|
20
|
+
|
|
21
|
+
// Keep this suite isolated from task tests and parallel Vitest workers.
|
|
22
|
+
let testDir: string;
|
|
23
|
+
|
|
24
|
+
function writeInbox(agentName: string, messages: InboxMessage[]) {
|
|
25
|
+
const inboxFilePath = path.join(testDir, "inboxes", `${agentName}.json`);
|
|
26
|
+
fs.mkdirSync(path.dirname(inboxFilePath), { recursive: true });
|
|
27
|
+
fs.writeFileSync(inboxFilePath, JSON.stringify(messages, null, 2));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("Messaging Utilities", () => {
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-extended-teams-messaging-"));
|
|
33
|
+
|
|
34
|
+
// Override paths to use testDir
|
|
35
|
+
vi.spyOn(paths, "inboxPath").mockImplementation((teamName, agentName) => {
|
|
36
|
+
return path.join(testDir, "inboxes", `${agentName}.json`);
|
|
37
|
+
});
|
|
38
|
+
vi.spyOn(paths, "teamDir").mockReturnValue(testDir);
|
|
39
|
+
vi.spyOn(paths, "configPath").mockImplementation((teamName) => {
|
|
40
|
+
return path.join(testDir, "config.json");
|
|
41
|
+
});
|
|
42
|
+
vi.spyOn(paths, "lifecycleTombstonePath").mockImplementation((_teamName, agentName) => {
|
|
43
|
+
return path.join(testDir, "lifecycle", "quarantine", `${agentName}.json`);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
vi.restoreAllMocks();
|
|
49
|
+
if (fs.existsSync(testDir)) fs.rmSync(testDir, { recursive: true });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("never truncates a live inbox in place", async () => {
|
|
53
|
+
await appendMessage("test-team", "receiver", { from: "sender", text: "first", timestamp: "now", read: false });
|
|
54
|
+
const inboxFile = path.join(testDir, "inboxes", "receiver.json");
|
|
55
|
+
const writes: string[] = [];
|
|
56
|
+
const realWriteFileSync = fs.writeFileSync;
|
|
57
|
+
vi.spyOn(fs, "writeFileSync").mockImplementation(((target: any, data: any, options: any) => {
|
|
58
|
+
writes.push(String(target));
|
|
59
|
+
return realWriteFileSync(target, data, options);
|
|
60
|
+
}) as typeof fs.writeFileSync);
|
|
61
|
+
|
|
62
|
+
await appendMessage("test-team", "receiver", { from: "sender", text: "second", timestamp: "now", read: false });
|
|
63
|
+
await readInbox("test-team", "receiver", true, true);
|
|
64
|
+
|
|
65
|
+
expect(writes).not.toContain(inboxFile);
|
|
66
|
+
expect((await readInbox("test-team", "receiver", false, false)).map(message => message.text)).toEqual(["first", "second"]);
|
|
67
|
+
expect(fs.readdirSync(path.dirname(inboxFile)).filter(entry => entry.endsWith(".tmp"))).toEqual([]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("broadcasts once to reachable members and reports the unreachable ones", async () => {
|
|
71
|
+
fs.writeFileSync(path.join(testDir, "config.json"), JSON.stringify({
|
|
72
|
+
name: "test-team",
|
|
73
|
+
members: [
|
|
74
|
+
{ name: "team-lead" },
|
|
75
|
+
{ name: "ghost", isActive: false },
|
|
76
|
+
{ name: "writer", isActive: true },
|
|
77
|
+
],
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
const results = await broadcastMessageOnce("test-team", "team-lead", "ping", "broadcast", { operationId: "op-1" });
|
|
81
|
+
|
|
82
|
+
expect(results.map(result => result.recipient).sort()).toEqual(["ghost", "writer"]);
|
|
83
|
+
expect(results.find(result => result.recipient === "writer")?.delivered).toBe(true);
|
|
84
|
+
expect(results.find(result => result.recipient === "ghost")).toMatchObject({
|
|
85
|
+
delivered: false,
|
|
86
|
+
error: expect.stringContaining("not running"),
|
|
87
|
+
});
|
|
88
|
+
expect((await readInbox("test-team", "writer", false, false)).length).toBe(1);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should append a message successfully", async () => {
|
|
92
|
+
const msg = { from: "sender", text: "hello", timestamp: "now", read: false };
|
|
93
|
+
await appendMessage("test-team", "receiver", msg);
|
|
94
|
+
|
|
95
|
+
const inbox = await readInbox("test-team", "receiver", false, false);
|
|
96
|
+
expect(inbox.length).toBe(1);
|
|
97
|
+
expect(inbox[0].text).toBe("hello");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("should handle concurrent appends (Stress Test)", async () => {
|
|
101
|
+
const numMessages = 100;
|
|
102
|
+
const promises = [];
|
|
103
|
+
for (let i = 0; i < numMessages; i++) {
|
|
104
|
+
promises.push(sendPlainMessage("test-team", `sender-${i}`, "receiver", `msg-${i}`, `summary-${i}`));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
await Promise.all(promises);
|
|
108
|
+
|
|
109
|
+
const inbox = await readInbox("test-team", "receiver", false, false);
|
|
110
|
+
expect(inbox.length).toBe(numMessages);
|
|
111
|
+
|
|
112
|
+
// Verify all messages are present
|
|
113
|
+
const texts = inbox.map(m => m.text).sort();
|
|
114
|
+
for (let i = 0; i < numMessages; i++) {
|
|
115
|
+
expect(texts).toContain(`msg-${i}`);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("should mark messages as read", async () => {
|
|
120
|
+
await sendPlainMessage("test-team", "sender", "receiver", "msg1", "summary1");
|
|
121
|
+
await sendPlainMessage("test-team", "sender", "receiver", "msg2", "summary2");
|
|
122
|
+
|
|
123
|
+
// Read only unread messages
|
|
124
|
+
const unread = await readInbox("test-team", "receiver", true, true);
|
|
125
|
+
expect(unread.length).toBe(2);
|
|
126
|
+
|
|
127
|
+
// Now all should be read
|
|
128
|
+
const all = await readInbox("test-team", "receiver", false, false);
|
|
129
|
+
expect(all.length).toBe(2);
|
|
130
|
+
expect(all.every(m => m.read)).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("should mark large unread inboxes as read", async () => {
|
|
134
|
+
const numMessages = 5000;
|
|
135
|
+
const messages = Array.from({ length: numMessages }, (_, index) => ({
|
|
136
|
+
from: "sender",
|
|
137
|
+
text: `msg-${index}`,
|
|
138
|
+
timestamp: `time-${index}`,
|
|
139
|
+
read: index % 3 === 0,
|
|
140
|
+
}));
|
|
141
|
+
const expectedUnread = messages.filter((message) => !message.read).length;
|
|
142
|
+
writeInbox("receiver", messages);
|
|
143
|
+
|
|
144
|
+
const unread = await readInbox("test-team", "receiver", true, true);
|
|
145
|
+
expect(unread.length).toBe(expectedUnread);
|
|
146
|
+
expect(unread.every((message) => message.read)).toBe(true);
|
|
147
|
+
|
|
148
|
+
const all = await readInbox("test-team", "receiver", false, false);
|
|
149
|
+
expect(all.length).toBe(numMessages);
|
|
150
|
+
expect(all.every((message) => message.read)).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("should read a bounded unread tail and mark only selected messages", async () => {
|
|
154
|
+
writeInbox("receiver", [
|
|
155
|
+
{ from: "sender", text: "old-unread", timestamp: "time-1", read: false },
|
|
156
|
+
{ from: "sender", text: "already-read", timestamp: "time-2", read: true },
|
|
157
|
+
{ from: "sender", text: "middle-unread", timestamp: "time-3", read: false },
|
|
158
|
+
{ from: "sender", text: "new-unread", timestamp: "time-4", read: false },
|
|
159
|
+
]);
|
|
160
|
+
|
|
161
|
+
const tail = await readInboxTail("test-team", "receiver", 2, { unreadOnly: true, markAsRead: true });
|
|
162
|
+
expect(tail.map((message) => message.text)).toEqual(["middle-unread", "new-unread"]);
|
|
163
|
+
expect(tail.every((message) => message.read)).toBe(true);
|
|
164
|
+
|
|
165
|
+
const all = await readInbox("test-team", "receiver", false, false);
|
|
166
|
+
expect(all.map((message) => [message.text, message.read])).toEqual([
|
|
167
|
+
["old-unread", false],
|
|
168
|
+
["already-read", true],
|
|
169
|
+
["middle-unread", true],
|
|
170
|
+
["new-unread", true],
|
|
171
|
+
]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("should isolate returned inbox messages from persisted inbox state", async () => {
|
|
175
|
+
writeInbox("receiver", [
|
|
176
|
+
{
|
|
177
|
+
from: "sender",
|
|
178
|
+
text: "original",
|
|
179
|
+
timestamp: "time-1",
|
|
180
|
+
read: false,
|
|
181
|
+
metadata: { operationId: "op-1" },
|
|
182
|
+
},
|
|
183
|
+
]);
|
|
184
|
+
|
|
185
|
+
const inbox = await readInbox("test-team", "receiver", false, false);
|
|
186
|
+
inbox[0].text = "mutated";
|
|
187
|
+
inbox[0].metadata!.operationId = "mutated";
|
|
188
|
+
|
|
189
|
+
const persisted = await readInbox("test-team", "receiver", false, false);
|
|
190
|
+
expect(persisted[0].text).toBe("original");
|
|
191
|
+
expect(persisted[0].metadata?.operationId).toBe("op-1");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("should peek without marking messages as read", async () => {
|
|
195
|
+
await sendPlainMessage("test-team", "sender", "receiver", "msg1", "summary1");
|
|
196
|
+
|
|
197
|
+
const peeked = await peekInbox("test-team", "receiver", true);
|
|
198
|
+
expect(peeked.length).toBe(1);
|
|
199
|
+
expect(peeked[0].read).toBe(false);
|
|
200
|
+
|
|
201
|
+
const unread = await readInbox("test-team", "receiver", true, false);
|
|
202
|
+
expect(unread.length).toBe(1);
|
|
203
|
+
expect(unread[0].read).toBe(false);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("should send operation messages once", async () => {
|
|
207
|
+
const first = await sendPlainMessageOnce("test-team", "sender", "receiver", "msg1", "summary1", { operationId: "op-1" });
|
|
208
|
+
const second = await sendPlainMessageOnce("test-team", "sender", "receiver", "msg2", "summary2", { operationId: "op-1" });
|
|
209
|
+
|
|
210
|
+
expect(first.delivered).toBe(true);
|
|
211
|
+
expect(second.delivered).toBe(false);
|
|
212
|
+
expect(second.message.text).toBe("msg1");
|
|
213
|
+
|
|
214
|
+
const inbox = await readInbox("test-team", "receiver", false, false);
|
|
215
|
+
expect(inbox.length).toBe(1);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("should find operation messages in top-level and metadata fields", async () => {
|
|
219
|
+
writeInbox("receiver", [
|
|
220
|
+
{
|
|
221
|
+
from: "sender",
|
|
222
|
+
text: "metadata operation",
|
|
223
|
+
timestamp: "time-1",
|
|
224
|
+
read: false,
|
|
225
|
+
metadata: { operationId: "op-1", workflowRunId: "wf-1" },
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
from: "sender",
|
|
229
|
+
text: "top-level operation",
|
|
230
|
+
timestamp: "time-2",
|
|
231
|
+
read: false,
|
|
232
|
+
operationId: "op-2",
|
|
233
|
+
},
|
|
234
|
+
]);
|
|
235
|
+
|
|
236
|
+
const metadataMessage = await findInboxMessageByOperation("test-team", "receiver", "op-1", "wf-1");
|
|
237
|
+
expect(metadataMessage?.text).toBe("metadata operation");
|
|
238
|
+
|
|
239
|
+
const topLevelMessage = await findInboxMessageByOperation("test-team", "receiver", "op-2");
|
|
240
|
+
expect(topLevelMessage?.text).toBe("top-level operation");
|
|
241
|
+
|
|
242
|
+
const mismatchedWorkflow = await findInboxMessageByOperation("test-team", "receiver", "op-1", "other-workflow");
|
|
243
|
+
expect(mismatchedWorkflow).toBeUndefined();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("orders a send that acquires the lifecycle fence before close", async () => {
|
|
247
|
+
const configFilePath = path.join(testDir, "config.json");
|
|
248
|
+
fs.writeFileSync(configFilePath, JSON.stringify({
|
|
249
|
+
name: "test-team",
|
|
250
|
+
members: [{ name: "receiver", lifecycleRunId: "receiver-run", isActive: true }],
|
|
251
|
+
}));
|
|
252
|
+
|
|
253
|
+
const delivery = sendPlainMessageIfRunning("test-team", "sender", "receiver", "before stop", "summary", undefined, {
|
|
254
|
+
expectedRecipientRunId: "receiver-run",
|
|
255
|
+
});
|
|
256
|
+
const stop = closePersistedRecipient("test-team", "receiver", "receiver-run");
|
|
257
|
+
await Promise.all([delivery, stop]);
|
|
258
|
+
|
|
259
|
+
expect((await readInbox("test-team", "receiver", false, false)).map(message => message.text)).toEqual(["before stop"]);
|
|
260
|
+
await expect(
|
|
261
|
+
sendPlainMessageIfRunning("test-team", "sender", "receiver", "after stop", "summary")
|
|
262
|
+
).rejects.toThrow("lifecycle-quarantined");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("rejects a send that follows close without creating or changing the inbox", async () => {
|
|
266
|
+
fs.writeFileSync(path.join(testDir, "config.json"), JSON.stringify({
|
|
267
|
+
name: "test-team",
|
|
268
|
+
members: [{ name: "receiver", lifecycleRunId: "receiver-run", isActive: true }],
|
|
269
|
+
}));
|
|
270
|
+
await closePersistedRecipient("test-team", "receiver", "receiver-run");
|
|
271
|
+
|
|
272
|
+
await expect(
|
|
273
|
+
sendPlainMessageIfRunning("test-team", "sender", "receiver", "too late", "summary")
|
|
274
|
+
).rejects.toThrow("lifecycle-quarantined");
|
|
275
|
+
expect(await readInbox("test-team", "receiver", false, false)).toEqual([]);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("rejects occupied and corrupt lifecycle tombstones before inbox admission", async () => {
|
|
279
|
+
fs.writeFileSync(path.join(testDir, "config.json"), JSON.stringify({
|
|
280
|
+
name: "test-team",
|
|
281
|
+
members: [{ name: "receiver", lifecycleRunId: "receiver-run", isActive: true }],
|
|
282
|
+
}));
|
|
283
|
+
const tombstonePath = paths.lifecycleTombstonePath("test-team", "receiver");
|
|
284
|
+
fs.mkdirSync(path.dirname(tombstonePath), { recursive: true });
|
|
285
|
+
fs.writeFileSync(tombstonePath, JSON.stringify({
|
|
286
|
+
version: 1,
|
|
287
|
+
team: "test-team",
|
|
288
|
+
agent: "receiver",
|
|
289
|
+
runId: "receiver-run",
|
|
290
|
+
role: "read",
|
|
291
|
+
reason: "quit",
|
|
292
|
+
phase: "closing",
|
|
293
|
+
ownerPid: process.pid,
|
|
294
|
+
extensionInstanceId: "test-instance",
|
|
295
|
+
timestamps: { createdAt: Date.now(), updatedAt: Date.now() },
|
|
296
|
+
}));
|
|
297
|
+
await expect(sendPlainMessageIfRunning("test-team", "sender", "receiver", "blocked", "summary"))
|
|
298
|
+
.rejects.toThrow("lifecycle-quarantined for run receiver-run");
|
|
299
|
+
|
|
300
|
+
fs.writeFileSync(tombstonePath, "not-json");
|
|
301
|
+
await expect(sendPlainMessageIfRunning("test-team", "sender", "receiver", "blocked", "summary"))
|
|
302
|
+
.rejects.toThrow("corrupt tombstone");
|
|
303
|
+
expect(await readInbox("test-team", "receiver", false, false)).toEqual([]);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("should broadcast message to all members except the sender", async () => {
|
|
307
|
+
// Setup team config
|
|
308
|
+
const config = {
|
|
309
|
+
name: "test-team",
|
|
310
|
+
members: [
|
|
311
|
+
{ name: "sender" },
|
|
312
|
+
{ name: "member1" },
|
|
313
|
+
{ name: "member2" }
|
|
314
|
+
]
|
|
315
|
+
};
|
|
316
|
+
const configFilePath = path.join(testDir, "config.json");
|
|
317
|
+
fs.writeFileSync(configFilePath, JSON.stringify(config));
|
|
318
|
+
|
|
319
|
+
await broadcastMessage("test-team", "sender", "broadcast text", "summary");
|
|
320
|
+
|
|
321
|
+
// Check member1's inbox
|
|
322
|
+
const inbox1 = await readInbox("test-team", "member1", false, false);
|
|
323
|
+
expect(inbox1.length).toBe(1);
|
|
324
|
+
expect(inbox1[0].text).toBe("broadcast text");
|
|
325
|
+
expect(inbox1[0].from).toBe("sender");
|
|
326
|
+
|
|
327
|
+
// Check member2's inbox
|
|
328
|
+
const inbox2 = await readInbox("test-team", "member2", false, false);
|
|
329
|
+
expect(inbox2.length).toBe(1);
|
|
330
|
+
expect(inbox2[0].text).toBe("broadcast text");
|
|
331
|
+
expect(inbox2[0].from).toBe("sender");
|
|
332
|
+
|
|
333
|
+
// Check sender's inbox (should be empty)
|
|
334
|
+
const inboxSender = await readInbox("test-team", "sender", false, false);
|
|
335
|
+
expect(inboxSender.length).toBe(0);
|
|
336
|
+
});
|
|
337
|
+
});
|