@desplega.ai/agent-swarm 1.80.2 → 1.81.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 +3 -0
- package/openapi.json +486 -29
- package/package.json +3 -3
- package/plugin/commands/user-management.md +85 -46
- package/plugin/pi-skills/user-management/SKILL.md +85 -46
- package/src/agentmail/handlers.ts +25 -3
- package/src/agentmail/types.ts +1 -0
- package/src/be/db.ts +33 -109
- package/src/be/migrations/067_users_first_class.sql +185 -0
- package/src/be/migrations/068_profile_changed_event_type.sql +56 -0
- package/src/be/unmapped-identities.ts +98 -0
- package/src/be/users.ts +531 -0
- package/src/github/handlers.ts +67 -7
- package/src/gitlab/handlers.ts +73 -5
- package/src/http/operator-actor.ts +59 -0
- package/src/http/users.ts +611 -21
- package/src/http/webhooks.ts +9 -0
- package/src/http/workflows.ts +2 -15
- package/src/linear/oauth.ts +61 -1
- package/src/linear/sync.ts +134 -21
- package/src/slack/actions.ts +8 -2
- package/src/slack/assistant.ts +12 -9
- package/src/slack/enrich.ts +162 -0
- package/src/slack/handlers.ts +11 -19
- package/src/tests/agentmail-handlers.test.ts +166 -0
- package/src/tests/github-handlers.test.ts +290 -0
- package/src/tests/gitlab-handlers.test.ts +293 -1
- package/src/tests/http-api-integration.test.ts +8 -4
- package/src/tests/http-users.test.ts +605 -0
- package/src/tests/linear-sync-identity.test.ts +427 -0
- package/src/tests/mcp-tools-user.test.ts +292 -0
- package/src/tests/slack-identity-resolution.test.ts +349 -0
- package/src/tests/user-identity.test.ts +351 -81
- package/src/tests/workflow-triggers-v2.test.ts +261 -20
- package/src/tools/manage-user.ts +119 -24
- package/src/tools/resolve-user.ts +43 -29
- package/src/types.ts +26 -4
- package/src/utils/secret-scrubber.ts +5 -0
- package/src/workflows/input.ts +7 -2
- package/src/workflows/triggers.ts +89 -9
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import {
|
|
4
|
+
closeDb,
|
|
5
|
+
createUser,
|
|
6
|
+
deleteKv,
|
|
7
|
+
getDb,
|
|
8
|
+
getKv,
|
|
9
|
+
getTaskById,
|
|
10
|
+
initDb,
|
|
11
|
+
upsertKv,
|
|
12
|
+
} from "../be/db";
|
|
13
|
+
import { getTrackerSyncByExternalId } from "../be/db-queries/tracker";
|
|
14
|
+
import { findUserByExternalId, linkIdentity } from "../be/users";
|
|
15
|
+
import { handleAgentSessionEvent, handleAgentSessionPrompted } from "../linear/sync";
|
|
16
|
+
import { _clearRecentDeliveries } from "../linear/webhook";
|
|
17
|
+
import { getTemplateDefinition } from "../prompts/registry";
|
|
18
|
+
|
|
19
|
+
const TEST_DB_PATH = "./test-linear-sync-identity.sqlite";
|
|
20
|
+
const UNMAPPED_NAMESPACE = "integration:unmapped:linear";
|
|
21
|
+
const APP_USER_ID_NAMESPACE = "integration:linear:bot-app-user-id";
|
|
22
|
+
|
|
23
|
+
// ── Helpers ──
|
|
24
|
+
|
|
25
|
+
let issueCounter = 0;
|
|
26
|
+
function makeIssue(): {
|
|
27
|
+
id: string;
|
|
28
|
+
identifier: string;
|
|
29
|
+
title: string;
|
|
30
|
+
url: string;
|
|
31
|
+
description: string;
|
|
32
|
+
} {
|
|
33
|
+
issueCounter += 1;
|
|
34
|
+
const n = String(issueCounter).padStart(3, "0");
|
|
35
|
+
return {
|
|
36
|
+
id: `issue-identity-${n}`,
|
|
37
|
+
identifier: `IDP-${n}`,
|
|
38
|
+
title: `Identity test issue ${n}`,
|
|
39
|
+
url: `https://linear.app/team/issue/IDP-${n}`,
|
|
40
|
+
description: "Test description",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function identityEventTypes(userId: string): string[] {
|
|
45
|
+
return getDb()
|
|
46
|
+
.prepare<{ eventType: string }, string>(
|
|
47
|
+
"SELECT eventType FROM user_identity_events WHERE userId = ? ORDER BY createdAt ASC, rowid ASC",
|
|
48
|
+
)
|
|
49
|
+
.all(userId)
|
|
50
|
+
.map((r) => r.eventType);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function externalIdsCount(): number {
|
|
54
|
+
const row = getDb()
|
|
55
|
+
.prepare<{ n: number }, []>("SELECT COUNT(*) AS n FROM user_external_ids")
|
|
56
|
+
.get();
|
|
57
|
+
return row?.n ?? 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function usersCount(): number {
|
|
61
|
+
const row = getDb().prepare<{ n: number }, []>("SELECT COUNT(*) AS n FROM users").get();
|
|
62
|
+
return row?.n ?? 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Setup ──
|
|
66
|
+
|
|
67
|
+
beforeAll(async () => {
|
|
68
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
69
|
+
await unlink(`${TEST_DB_PATH}${suffix}`).catch(() => {});
|
|
70
|
+
}
|
|
71
|
+
initDb(TEST_DB_PATH);
|
|
72
|
+
// Linear sync needs a lead agent to be present (it uses findLeadAgent()).
|
|
73
|
+
const { createAgent } = await import("../be/db");
|
|
74
|
+
createAgent({ name: "TestLead", isLead: true, status: "idle" });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
afterAll(async () => {
|
|
78
|
+
closeDb();
|
|
79
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
80
|
+
await unlink(`${TEST_DB_PATH}${suffix}`).catch(() => {});
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
beforeEach(async () => {
|
|
85
|
+
_clearRecentDeliveries();
|
|
86
|
+
if (!getTemplateDefinition("linear.issue.assigned")) {
|
|
87
|
+
await import(`../linear/templates?t=${Date.now()}`);
|
|
88
|
+
}
|
|
89
|
+
// Reset identity-relevant rows between tests so each case starts clean.
|
|
90
|
+
// Order matters — agent_tasks has FK on users.id via requestedByUserId.
|
|
91
|
+
const db = getDb();
|
|
92
|
+
db.prepare("DELETE FROM tracker_sync").run();
|
|
93
|
+
db.prepare("DELETE FROM agent_tasks").run();
|
|
94
|
+
db.prepare("DELETE FROM user_external_ids").run();
|
|
95
|
+
db.prepare("DELETE FROM user_identity_events").run();
|
|
96
|
+
db.prepare("DELETE FROM users").run();
|
|
97
|
+
db.prepare("DELETE FROM kv_entries WHERE namespace = ?").run(UNMAPPED_NAMESPACE);
|
|
98
|
+
db.prepare("DELETE FROM kv_entries WHERE namespace = ?").run(APP_USER_ID_NAMESPACE);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ─── AgentSessionEvent.created ────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
describe("handleAgentSessionEvent — identity resolution (Q21.A fix)", () => {
|
|
104
|
+
test("fast path: existing user_external_ids row resolves requestedByUserId", async () => {
|
|
105
|
+
const issue = makeIssue();
|
|
106
|
+
const linearUserId = "lin-user-fastpath-001";
|
|
107
|
+
const u = createUser({ name: "Existing Human", email: "existing@example.com" });
|
|
108
|
+
linkIdentity(u.id, "linear", linearUserId, { kind: "system", id: "test-fixture" });
|
|
109
|
+
|
|
110
|
+
const beforeUsers = usersCount();
|
|
111
|
+
const beforeExt = externalIdsCount();
|
|
112
|
+
|
|
113
|
+
const event = {
|
|
114
|
+
type: "AgentSessionEvent",
|
|
115
|
+
action: "created",
|
|
116
|
+
organizationId: "org-1",
|
|
117
|
+
agentSession: {
|
|
118
|
+
id: "sess-1",
|
|
119
|
+
url: "https://linear.app/sess/sess-1",
|
|
120
|
+
issue,
|
|
121
|
+
creator: { id: linearUserId, email: "existing@example.com", name: "Existing Human" },
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
await handleAgentSessionEvent(event);
|
|
126
|
+
|
|
127
|
+
const sync = getTrackerSyncByExternalId("linear", "task", issue.id);
|
|
128
|
+
expect(sync).not.toBeNull();
|
|
129
|
+
const task = getTaskById(sync!.swarmId);
|
|
130
|
+
expect(task?.requestedByUserId).toBe(u.id);
|
|
131
|
+
|
|
132
|
+
// No new user / no new external-id row was inserted.
|
|
133
|
+
expect(usersCount()).toBe(beforeUsers);
|
|
134
|
+
expect(externalIdsCount()).toBe(beforeExt);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("cascade: unknown linear ID + email present creates user + links identity", async () => {
|
|
138
|
+
const issue = makeIssue();
|
|
139
|
+
const linearUserId = "lin-user-cascade-001";
|
|
140
|
+
|
|
141
|
+
expect(findUserByExternalId("linear", linearUserId)).toBeNull();
|
|
142
|
+
|
|
143
|
+
const event = {
|
|
144
|
+
type: "AgentSessionEvent",
|
|
145
|
+
action: "created",
|
|
146
|
+
organizationId: "org-1",
|
|
147
|
+
agentSession: {
|
|
148
|
+
id: "sess-2",
|
|
149
|
+
url: "https://linear.app/sess/sess-2",
|
|
150
|
+
issue,
|
|
151
|
+
creator: { id: linearUserId, email: "cascade@example.com", name: "Cascade Human" },
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
await handleAgentSessionEvent(event);
|
|
156
|
+
|
|
157
|
+
const sync = getTrackerSyncByExternalId("linear", "task", issue.id);
|
|
158
|
+
expect(sync).not.toBeNull();
|
|
159
|
+
const task = getTaskById(sync!.swarmId);
|
|
160
|
+
expect(task?.requestedByUserId).toBeTruthy();
|
|
161
|
+
|
|
162
|
+
const linked = findUserByExternalId("linear", linearUserId);
|
|
163
|
+
expect(linked).not.toBeNull();
|
|
164
|
+
expect(linked!.email).toBe("cascade@example.com");
|
|
165
|
+
expect(task?.requestedByUserId).toBe(linked!.id);
|
|
166
|
+
|
|
167
|
+
// Both auto_merge (from findOrCreateUserByEmail's create branch emits
|
|
168
|
+
// identity_added) and identity_added (from linkIdentity) should be present.
|
|
169
|
+
const types = identityEventTypes(linked!.id);
|
|
170
|
+
expect(types).toContain("identity_added");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("unknown linear ID + no email → unmapped kv rows written, requestedByUserId undefined", async () => {
|
|
174
|
+
const issue = makeIssue();
|
|
175
|
+
const linearUserId = "lin-user-unmapped-001";
|
|
176
|
+
|
|
177
|
+
const event = {
|
|
178
|
+
type: "AgentSessionEvent",
|
|
179
|
+
action: "created",
|
|
180
|
+
organizationId: "org-1",
|
|
181
|
+
agentSession: {
|
|
182
|
+
id: "sess-3",
|
|
183
|
+
url: "https://linear.app/sess/sess-3",
|
|
184
|
+
issue,
|
|
185
|
+
// No email — pure unmapped.
|
|
186
|
+
creator: { id: linearUserId, name: "Unmapped Human" },
|
|
187
|
+
comment: { body: "I need help with deploys" },
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
await handleAgentSessionEvent(event);
|
|
192
|
+
|
|
193
|
+
const sync = getTrackerSyncByExternalId("linear", "task", issue.id);
|
|
194
|
+
expect(sync).not.toBeNull();
|
|
195
|
+
const task = getTaskById(sync!.swarmId);
|
|
196
|
+
expect(task?.requestedByUserId).toBeUndefined();
|
|
197
|
+
|
|
198
|
+
const meta = getKv(UNMAPPED_NAMESPACE, `${linearUserId}:meta`);
|
|
199
|
+
expect(meta).not.toBeNull();
|
|
200
|
+
expect(meta!.valueType).toBe("json");
|
|
201
|
+
const metaValue = meta!.value as { sampleEventType: string; sampleContext: string | null };
|
|
202
|
+
expect(metaValue.sampleEventType).toBe("AgentSessionEvent.created");
|
|
203
|
+
expect(metaValue.sampleContext).toBe("I need help with deploys");
|
|
204
|
+
|
|
205
|
+
const count = getKv(UNMAPPED_NAMESPACE, `${linearUserId}:count`);
|
|
206
|
+
expect(count).not.toBeNull();
|
|
207
|
+
expect(count!.value).toBe(1);
|
|
208
|
+
|
|
209
|
+
// No users / external-id rows were created.
|
|
210
|
+
expect(findUserByExternalId("linear", linearUserId)).toBeNull();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("appUserId guard: creator.id === storedAppUserId → no user, no unmapped", async () => {
|
|
214
|
+
const issue = makeIssue();
|
|
215
|
+
const appUserId = "lin-app-user-bot-001";
|
|
216
|
+
upsertKv({
|
|
217
|
+
namespace: APP_USER_ID_NAMESPACE,
|
|
218
|
+
key: "org-1",
|
|
219
|
+
value: appUserId,
|
|
220
|
+
valueType: "string",
|
|
221
|
+
expiresAt: null,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const before = { users: usersCount(), ext: externalIdsCount() };
|
|
225
|
+
|
|
226
|
+
const event = {
|
|
227
|
+
type: "AgentSessionEvent",
|
|
228
|
+
action: "created",
|
|
229
|
+
organizationId: "org-1",
|
|
230
|
+
agentSession: {
|
|
231
|
+
id: "sess-4",
|
|
232
|
+
url: "https://linear.app/sess/sess-4",
|
|
233
|
+
issue,
|
|
234
|
+
creator: { id: appUserId, email: "bot@swarm.example", name: "Agent Swarm" },
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
await handleAgentSessionEvent(event);
|
|
239
|
+
|
|
240
|
+
const sync = getTrackerSyncByExternalId("linear", "task", issue.id);
|
|
241
|
+
expect(sync).not.toBeNull();
|
|
242
|
+
const task = getTaskById(sync!.swarmId);
|
|
243
|
+
expect(task?.requestedByUserId).toBeUndefined();
|
|
244
|
+
|
|
245
|
+
// Crucially: no users row, no unmapped entry. The swarm doesn't hear itself.
|
|
246
|
+
expect(usersCount()).toBe(before.users);
|
|
247
|
+
expect(externalIdsCount()).toBe(before.ext);
|
|
248
|
+
expect(getKv(UNMAPPED_NAMESPACE, `${appUserId}:meta`)).toBeNull();
|
|
249
|
+
expect(getKv(UNMAPPED_NAMESPACE, `${appUserId}:count`)).toBeNull();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("regression: OLD event.actor shape no longer enrolls a user", async () => {
|
|
253
|
+
const issue = makeIssue();
|
|
254
|
+
const before = { users: usersCount(), ext: externalIdsCount() };
|
|
255
|
+
|
|
256
|
+
// Construct a payload in the broken old shape — top-level `actor` with no
|
|
257
|
+
// `agentSession.creator`. The new extraction reads the nested path only;
|
|
258
|
+
// this payload should produce no identity work at all.
|
|
259
|
+
const event = {
|
|
260
|
+
type: "AgentSessionEvent",
|
|
261
|
+
action: "created",
|
|
262
|
+
organizationId: "org-1",
|
|
263
|
+
actor: { id: "lin-user-regression-001", email: "ghost@example.com", name: "Ghost" },
|
|
264
|
+
agentSession: {
|
|
265
|
+
id: "sess-5",
|
|
266
|
+
url: "https://linear.app/sess/sess-5",
|
|
267
|
+
issue,
|
|
268
|
+
// Note: no `creator` field.
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
await handleAgentSessionEvent(event);
|
|
273
|
+
|
|
274
|
+
const sync = getTrackerSyncByExternalId("linear", "task", issue.id);
|
|
275
|
+
expect(sync).not.toBeNull();
|
|
276
|
+
const task = getTaskById(sync!.swarmId);
|
|
277
|
+
expect(task?.requestedByUserId).toBeUndefined();
|
|
278
|
+
expect(usersCount()).toBe(before.users);
|
|
279
|
+
expect(externalIdsCount()).toBe(before.ext);
|
|
280
|
+
expect(getKv(UNMAPPED_NAMESPACE, `lin-user-regression-001:meta`)).toBeNull();
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// ─── AgentSessionEvent.prompted ───────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
describe("handleAgentSessionPrompted — identity resolution (Q21.A fix)", () => {
|
|
287
|
+
// Helper: seed a completed task + tracker_sync so the prompted handler
|
|
288
|
+
// falls through to the follow-up branch where identity extraction runs.
|
|
289
|
+
async function seedCompletedTask(issueId: string, identifier: string): Promise<void> {
|
|
290
|
+
const { createTaskExtended } = await import("../be/db");
|
|
291
|
+
const t = createTaskExtended("Seeded prior", { source: "linear", taskType: "linear-issue" });
|
|
292
|
+
getDb().query("UPDATE agent_tasks SET status = 'completed' WHERE id = ?").run(t.id);
|
|
293
|
+
const { createTrackerSync } = await import("../be/db-queries/tracker");
|
|
294
|
+
createTrackerSync({
|
|
295
|
+
provider: "linear",
|
|
296
|
+
entityType: "task",
|
|
297
|
+
providerEntityType: "Issue",
|
|
298
|
+
swarmId: t.id,
|
|
299
|
+
externalId: issueId,
|
|
300
|
+
externalIdentifier: identifier,
|
|
301
|
+
externalUrl: `https://linear.app/team/issue/${identifier}`,
|
|
302
|
+
lastSyncOrigin: "external",
|
|
303
|
+
syncDirection: "inbound",
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
test("fast path: existing external-id row populates requestedByUserId on followup", async () => {
|
|
308
|
+
const issue = makeIssue();
|
|
309
|
+
await seedCompletedTask(issue.id, issue.identifier);
|
|
310
|
+
|
|
311
|
+
const linearUserId = "lin-user-prompted-fastpath-001";
|
|
312
|
+
const u = createUser({ name: "Prompted Human", email: "pf@example.com" });
|
|
313
|
+
linkIdentity(u.id, "linear", linearUserId, { kind: "system", id: "test-fixture" });
|
|
314
|
+
|
|
315
|
+
const event = {
|
|
316
|
+
type: "AgentSessionEvent",
|
|
317
|
+
action: "prompted",
|
|
318
|
+
organizationId: "org-1",
|
|
319
|
+
agentSession: { id: "sess-p1", url: "x", issue },
|
|
320
|
+
agentActivity: {
|
|
321
|
+
signal: "",
|
|
322
|
+
content: { body: "please continue" },
|
|
323
|
+
user: { id: linearUserId, email: "pf@example.com", name: "Prompted Human" },
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
await handleAgentSessionPrompted(event);
|
|
328
|
+
|
|
329
|
+
const sync = getTrackerSyncByExternalId("linear", "task", issue.id);
|
|
330
|
+
expect(sync).not.toBeNull();
|
|
331
|
+
const task = getTaskById(sync!.swarmId);
|
|
332
|
+
expect(task?.requestedByUserId).toBe(u.id);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("cascade: prompted with unknown id + email creates and links", async () => {
|
|
336
|
+
const issue = makeIssue();
|
|
337
|
+
await seedCompletedTask(issue.id, issue.identifier);
|
|
338
|
+
|
|
339
|
+
const linearUserId = "lin-user-prompted-cascade-001";
|
|
340
|
+
|
|
341
|
+
const event = {
|
|
342
|
+
type: "AgentSessionEvent",
|
|
343
|
+
action: "prompted",
|
|
344
|
+
organizationId: "org-1",
|
|
345
|
+
agentSession: { id: "sess-p2", url: "x", issue },
|
|
346
|
+
agentActivity: {
|
|
347
|
+
signal: "",
|
|
348
|
+
content: { body: "more context here" },
|
|
349
|
+
user: { id: linearUserId, email: "pc@example.com", name: "Prompted Cascade" },
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
await handleAgentSessionPrompted(event);
|
|
354
|
+
|
|
355
|
+
const linked = findUserByExternalId("linear", linearUserId);
|
|
356
|
+
expect(linked).not.toBeNull();
|
|
357
|
+
|
|
358
|
+
const sync = getTrackerSyncByExternalId("linear", "task", issue.id);
|
|
359
|
+
expect(sync).not.toBeNull();
|
|
360
|
+
const task = getTaskById(sync!.swarmId);
|
|
361
|
+
expect(task?.requestedByUserId).toBe(linked!.id);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("unmapped: prompted with unknown id + no email records kv", async () => {
|
|
365
|
+
const issue = makeIssue();
|
|
366
|
+
await seedCompletedTask(issue.id, issue.identifier);
|
|
367
|
+
|
|
368
|
+
const linearUserId = "lin-user-prompted-unmapped-001";
|
|
369
|
+
|
|
370
|
+
const event = {
|
|
371
|
+
type: "AgentSessionEvent",
|
|
372
|
+
action: "prompted",
|
|
373
|
+
organizationId: "org-1",
|
|
374
|
+
agentSession: { id: "sess-p3", url: "x", issue },
|
|
375
|
+
agentActivity: {
|
|
376
|
+
signal: "",
|
|
377
|
+
content: { body: "anonymous follow-up" },
|
|
378
|
+
user: { id: linearUserId, name: "No Email" },
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
await handleAgentSessionPrompted(event);
|
|
383
|
+
|
|
384
|
+
const meta = getKv(UNMAPPED_NAMESPACE, `${linearUserId}:meta`);
|
|
385
|
+
expect(meta).not.toBeNull();
|
|
386
|
+
const metaValue = meta!.value as { sampleEventType: string; sampleContext: string | null };
|
|
387
|
+
expect(metaValue.sampleEventType).toBe("AgentSessionEvent.prompted");
|
|
388
|
+
expect(metaValue.sampleContext).toBe("anonymous follow-up");
|
|
389
|
+
|
|
390
|
+
// Cleanup ledger so this test is hermetic across reruns.
|
|
391
|
+
deleteKv(UNMAPPED_NAMESPACE, `${linearUserId}:count`);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test("appUserId guard on prompted: user.id === storedAppUserId → no enrollment", async () => {
|
|
395
|
+
const issue = makeIssue();
|
|
396
|
+
await seedCompletedTask(issue.id, issue.identifier);
|
|
397
|
+
|
|
398
|
+
const appUserId = "lin-app-user-bot-prompted-001";
|
|
399
|
+
upsertKv({
|
|
400
|
+
namespace: APP_USER_ID_NAMESPACE,
|
|
401
|
+
key: "org-1",
|
|
402
|
+
value: appUserId,
|
|
403
|
+
valueType: "string",
|
|
404
|
+
expiresAt: null,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
const before = { users: usersCount(), ext: externalIdsCount() };
|
|
408
|
+
|
|
409
|
+
const event = {
|
|
410
|
+
type: "AgentSessionEvent",
|
|
411
|
+
action: "prompted",
|
|
412
|
+
organizationId: "org-1",
|
|
413
|
+
agentSession: { id: "sess-p4", url: "x", issue },
|
|
414
|
+
agentActivity: {
|
|
415
|
+
signal: "",
|
|
416
|
+
content: { body: "swarm self-echo" },
|
|
417
|
+
user: { id: appUserId, email: "bot@swarm.example", name: "Agent Swarm" },
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
await handleAgentSessionPrompted(event);
|
|
422
|
+
|
|
423
|
+
expect(usersCount()).toBe(before.users);
|
|
424
|
+
expect(externalIdsCount()).toBe(before.ext);
|
|
425
|
+
expect(getKv(UNMAPPED_NAMESPACE, `${appUserId}:meta`)).toBeNull();
|
|
426
|
+
});
|
|
427
|
+
});
|