@teamscala/service-boot 0.23.350 → 0.23.352
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/package.json +479 -567
- package/src/agreement-gate.ts +1 -1
- package/src/auth-gate.ts +0 -11
- package/src/fn-routers/index.ts +1 -12
- package/src/fn-routers/public-agreement-router.ts +37 -49
- package/src/mount-orpc-router.test.ts +46 -0
- package/src/mount-orpc-router.ts +16 -3
- package/src/mutable-app-ref.ts +0 -20
- package/src/page-server-orpc/puck-pages-router.ts +0 -102
- package/src/register-page-server-client.ts +0 -4
- package/src/register-ws-routes.ts +10 -3
- package/src/two-phase-boot.ts +2 -1
- package/src/ui-service.ts +11 -18
- package/src/fn-routers/ai-health-router.ts +0 -64
- package/src/fn-routers/panel-data-router.ts +0 -93
- package/src/fn-routers/plans-router.ts +0 -141
- package/src/fn-routers/task-sync-assign.test.ts +0 -483
- package/src/fn-routers/task-sync-assign.ts +0 -485
- package/src/fn-routers/task-sync-bind.test.ts +0 -110
- package/src/fn-routers/task-sync-brief.test.ts +0 -120
- package/src/fn-routers/task-sync-brief.ts +0 -151
- package/src/fn-routers/task-sync-classify.test.ts +0 -109
- package/src/fn-routers/task-sync-close.test.ts +0 -44
- package/src/fn-routers/task-sync-close.ts +0 -82
- package/src/fn-routers/task-sync-created-status.test.ts +0 -58
- package/src/fn-routers/task-sync-dispatch.test.ts +0 -62
- package/src/fn-routers/task-sync-hook-lifecycle.test.ts +0 -204
- package/src/fn-routers/task-sync-lane.test.ts +0 -36
- package/src/fn-routers/task-sync-projection.test.ts +0 -172
- package/src/fn-routers/task-sync-projection.ts +0 -273
- package/src/fn-routers/task-sync-rename.test.ts +0 -53
- package/src/fn-routers/task-sync-rename.ts +0 -111
- package/src/fn-routers/tasks-router.ts +0 -77
- package/src/page-server-orpc/portal-diagnostics-router.ts +0 -170
- package/src/page-server-orpc/templates-router.ts +0 -116
- package/src/serve/serve-ws.test.ts +0 -135
- package/src/serve/serve.ts +0 -148
- package/src/serve/stamp-transport-identity.test.ts +0 -24
- package/src/serve/stamp-transport-identity.ts +0 -109
|
@@ -1,483 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @system service-boot
|
|
3
|
-
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Unit tests for the dispatch-by-assignment WRITE core (task-sync-assign.ts):
|
|
7
|
-
* the "incomplete task is unconstructible" signature invariant, the atomic
|
|
8
|
-
* create-task + first-comment (= brief) write, and the no-initialPrompt spawn
|
|
9
|
-
* payload (regression-locks the row-based self-brief model — fails the moment
|
|
10
|
-
* someone re-adds keystroke delivery). Covers `what-must-have-a-test`.
|
|
11
|
-
*/
|
|
12
|
-
import { describe, it, expect } from "bun:test";
|
|
13
|
-
import * as v from "valibot";
|
|
14
|
-
import {
|
|
15
|
-
assignTaskInput,
|
|
16
|
-
bindLaneInput,
|
|
17
|
-
createBriefTaskInput,
|
|
18
|
-
createTaskWithBriefComment,
|
|
19
|
-
adoptAssignmentTask,
|
|
20
|
-
bindTaskToKnownSession,
|
|
21
|
-
buildAssignmentSpawnPayload,
|
|
22
|
-
resolveAssignOrg,
|
|
23
|
-
resolveBindAction,
|
|
24
|
-
} from "./task-sync-assign.ts";
|
|
25
|
-
import type { WorkItem } from "./task-sync-router.ts";
|
|
26
|
-
|
|
27
|
-
/** A minimal fake prisma exposing only $transaction + the two creates the fn uses. */
|
|
28
|
-
function fakePrisma(overrides: { platformOrg?: string } = {}) {
|
|
29
|
-
const calls: {
|
|
30
|
-
workItem: Record<string, unknown> | null;
|
|
31
|
-
comment: Record<string, unknown> | null;
|
|
32
|
-
}[] = [];
|
|
33
|
-
let createdTask: Record<string, unknown> = {
|
|
34
|
-
id: "task-new-1",
|
|
35
|
-
kind: "task",
|
|
36
|
-
title: "unused",
|
|
37
|
-
status: "active",
|
|
38
|
-
kind_data: {},
|
|
39
|
-
};
|
|
40
|
-
const prisma = {
|
|
41
|
-
$transaction: async (fn: (tx: unknown) => Promise<unknown>) => {
|
|
42
|
-
const tx = {
|
|
43
|
-
work_items: {
|
|
44
|
-
create: async (a: { data: Record<string, unknown> }) => {
|
|
45
|
-
createdTask = { ...createdTask, ...a.data, id: "task-new-1" };
|
|
46
|
-
return createdTask;
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
comments: {
|
|
50
|
-
create: async (a: { data: Record<string, unknown> }) => {
|
|
51
|
-
calls.push({ workItem: null, comment: a.data });
|
|
52
|
-
return { ...a.data, id: "comment-new-1" };
|
|
53
|
-
},
|
|
54
|
-
},
|
|
55
|
-
};
|
|
56
|
-
const result = await fn(tx);
|
|
57
|
-
// Record the task create alongside the first comment for assertion.
|
|
58
|
-
calls[calls.length - 1] = { ...calls[calls.length - 1], workItem: { ...createdTask } };
|
|
59
|
-
return result;
|
|
60
|
-
},
|
|
61
|
-
};
|
|
62
|
-
return { prisma, calls, platformOrg: overrides.platformOrg ?? "org-platform" };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
describe("assignTaskInput (incomplete task is unconstructible)", () => {
|
|
66
|
-
it("accepts name + description together", () => {
|
|
67
|
-
const out = v.safeParse(assignTaskInput, { name: "FleetTruth", description: "Do X" });
|
|
68
|
-
expect(out.success).toBe(true);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("rejects a missing description — no brief, no task", () => {
|
|
72
|
-
// The description IS the brief (first comment); a task without it is exactly
|
|
73
|
-
// the incomplete state the operator said must be unconstructible.
|
|
74
|
-
const out = v.safeParse(assignTaskInput, { name: "FleetTruth" });
|
|
75
|
-
expect(out.success).toBe(false);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("rejects a missing name", () => {
|
|
79
|
-
const out = v.safeParse(assignTaskInput, { description: "Do X" });
|
|
80
|
-
expect(out.success).toBe(false);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("rejects an empty-string description (not just absent)", () => {
|
|
84
|
-
const out = v.safeParse(assignTaskInput, { name: "FleetTruth", description: "" });
|
|
85
|
-
expect(out.success).toBe(false);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("treats parent_id/slug/windowName as optional", () => {
|
|
89
|
-
const out = v.safeParse(assignTaskInput, {
|
|
90
|
-
name: "Exploratory",
|
|
91
|
-
description: "Look around",
|
|
92
|
-
});
|
|
93
|
-
expect(out.success).toBe(true);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("REJECTS the generic 'Lane' title (assignment mint — same gate as createBriefTask)", () => {
|
|
97
|
-
expect(v.safeParse(assignTaskInput, { name: "Lane", description: "d" }).success).toBe(false);
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
describe("createBriefTaskInput (the spawner's mint entry — incomplete task is unconstructible)", () => {
|
|
102
|
-
// The mint a raw initialPrompt hits: spawn_agent_tab calls createBriefTask,
|
|
103
|
-
// which validates against this schema BEFORE the handler runs. An incomplete
|
|
104
|
-
// mint (no brief) is rejected at the signature, never caught after the fact —
|
|
105
|
-
// the same gate assignTaskInput sets, on the entry point spawn_agent_tab owns.
|
|
106
|
-
it("accepts name + description together", () => {
|
|
107
|
-
const out = v.safeParse(createBriefTaskInput, {
|
|
108
|
-
name: "Spawn mints row",
|
|
109
|
-
description: "the brief text",
|
|
110
|
-
});
|
|
111
|
-
expect(out.success).toBe(true);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("rejects a missing description — no brief, no minted task", () => {
|
|
115
|
-
expect(v.safeParse(createBriefTaskInput, { name: "X" }).success).toBe(false);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("rejects a missing name", () => {
|
|
119
|
-
expect(v.safeParse(createBriefTaskInput, { description: "d" }).success).toBe(false);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it("rejects an empty-string description (not just absent)", () => {
|
|
123
|
-
expect(v.safeParse(createBriefTaskInput, { name: "X", description: "" }).success).toBe(false);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it("treats parent_id/organisation_id as optional (exploratory lane)", () => {
|
|
127
|
-
const out = v.safeParse(createBriefTaskInput, { name: "Explore", description: "d" });
|
|
128
|
-
expect(out.success).toBe(true);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// "Lane" is the generic sentinel a spawn that forgot to name its lane falls
|
|
132
|
-
// back to. A row titled "Lane" is spawn debris an orchestrator cannot
|
|
133
|
-
// attribute — rejected at the mint chokepoint so the malformed spawn is
|
|
134
|
-
// refused, never written (lanes-named-by-the-spawner).
|
|
135
|
-
it("REJECTS the generic 'Lane' title — spawn debris is unconstructible", () => {
|
|
136
|
-
expect(
|
|
137
|
-
v.safeParse(createBriefTaskInput, { name: "Lane", description: "d" }).success,
|
|
138
|
-
).toBe(false);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it("rejects 'Lane' case- and whitespace-insensitively", () => {
|
|
142
|
-
expect(
|
|
143
|
-
v.safeParse(createBriefTaskInput, { name: " lane ", description: "d" }).success,
|
|
144
|
-
).toBe(false);
|
|
145
|
-
expect(
|
|
146
|
-
v.safeParse(createBriefTaskInput, { name: "LANE", description: "d" }).success,
|
|
147
|
-
).toBe(false);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it("accepts a real work name (e.g. the lane's slot_name)", () => {
|
|
151
|
-
expect(
|
|
152
|
-
v.safeParse(createBriefTaskInput, { name: "Bus handlers", description: "d" }).success,
|
|
153
|
-
).toBe(true);
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
describe("createTaskWithBriefComment (atomic task + first comment = brief)", () => {
|
|
158
|
-
it("creates the task active with kind_data carrying NO session, and writes the description as the first comment in the SAME transaction", async () => {
|
|
159
|
-
const { prisma, calls } = fakePrisma();
|
|
160
|
-
const task = await createTaskWithBriefComment(
|
|
161
|
-
prisma as never,
|
|
162
|
-
{ name: "Dispatch test", description: "the brief text", organisation_id: "org-1" },
|
|
163
|
-
"user-1",
|
|
164
|
-
);
|
|
165
|
-
|
|
166
|
-
// The task is the active parent the lane will later bind its session to.
|
|
167
|
-
expect(task.kind).toBe("task");
|
|
168
|
-
expect(task.status).toBe("active");
|
|
169
|
-
expect(task.title).toBe("Dispatch test");
|
|
170
|
-
// CRITICAL: no session key yet — the CLI mints it at spawn and SessionStart
|
|
171
|
-
// writes it. Carrying one now would let resolveLaneTask race a half-bound row.
|
|
172
|
-
// assignee_type:"agent" marks it lane-owned from creation (ownership marker).
|
|
173
|
-
expect(task.kind_data).toEqual({ assignee_type: "agent" });
|
|
174
|
-
expect((task.kind_data as Record<string, unknown>).lane).toBeUndefined();
|
|
175
|
-
|
|
176
|
-
// Exactly one comment, content = the description (one source, not two).
|
|
177
|
-
expect(calls).toHaveLength(1);
|
|
178
|
-
expect(calls[0].comment).toMatchObject({
|
|
179
|
-
work_item_id: task.id,
|
|
180
|
-
content: "the brief text",
|
|
181
|
-
user_id: "user-1",
|
|
182
|
-
});
|
|
183
|
-
// The comment was written inside the same transaction as the task create
|
|
184
|
-
// (atomic — a task row without its brief comment can never exist).
|
|
185
|
-
expect(calls[0].workItem).toMatchObject({ id: task.id, kind: "task", status: "active" });
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it("parents the task under parent_id when supplied, else null", async () => {
|
|
189
|
-
const withParent = fakePrisma();
|
|
190
|
-
await createTaskWithBriefComment(
|
|
191
|
-
withParent.prisma as never,
|
|
192
|
-
{ name: "T", description: "d", parent_id: "proj-9", organisation_id: "org-1" },
|
|
193
|
-
"u",
|
|
194
|
-
);
|
|
195
|
-
expect(withParent.calls[0].workItem?.parent_id).toBe("proj-9");
|
|
196
|
-
|
|
197
|
-
const noParent = fakePrisma();
|
|
198
|
-
await createTaskWithBriefComment(
|
|
199
|
-
noParent.prisma as never,
|
|
200
|
-
{ name: "T", description: "d", organisation_id: "org-1" },
|
|
201
|
-
"u",
|
|
202
|
-
);
|
|
203
|
-
expect(noParent.calls[0].workItem?.parent_id).toBe(null);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it("stamps created_by/updated_by + comment user_id from the caller", async () => {
|
|
207
|
-
const { prisma, calls } = fakePrisma();
|
|
208
|
-
await createTaskWithBriefComment(
|
|
209
|
-
prisma as never,
|
|
210
|
-
{ name: "T", description: "d", organisation_id: "org-1" },
|
|
211
|
-
"user-42",
|
|
212
|
-
);
|
|
213
|
-
expect(calls[0].workItem).toMatchObject({ created_by: "user-42", updated_by: "user-42" });
|
|
214
|
-
expect(calls[0].comment).toMatchObject({ user_id: "user-42" });
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
describe("adoptAssignmentTask (SessionStart adopts the pre-created task + ownership)", () => {
|
|
219
|
-
function fakeAdoptPrisma(existing: Record<string, unknown> | null) {
|
|
220
|
-
const updates: Array<{ id: string } & Record<string, unknown>> = [];
|
|
221
|
-
const updateManyCalls: unknown[] = [];
|
|
222
|
-
const prisma = {
|
|
223
|
-
work_items: {
|
|
224
|
-
findFirst: async () => existing,
|
|
225
|
-
update: async (a: { where: { id: string }; data: Record<string, unknown> }) => {
|
|
226
|
-
const row = { ...(existing ?? {}), ...a.data, id: a.where.id };
|
|
227
|
-
updates.push(row);
|
|
228
|
-
return row;
|
|
229
|
-
},
|
|
230
|
-
updateMany: async (a: unknown) => {
|
|
231
|
-
updateManyCalls.push(a);
|
|
232
|
-
return { count: 0 };
|
|
233
|
-
},
|
|
234
|
-
},
|
|
235
|
-
};
|
|
236
|
-
return { prisma, updates, updateManyCalls };
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
it("writes the lane session onto the pre-created task AND sets assignee_id (ownership)", async () => {
|
|
240
|
-
const task = { id: "task-1", kind: "task", kind_data: { assignee_type: "agent" }, assignee_id: null };
|
|
241
|
-
const { prisma, updates } = fakeAdoptPrisma(task);
|
|
242
|
-
const out = await adoptAssignmentTask(prisma as never, "task-1", { session: "ses-9", pane: "tmux:%5" });
|
|
243
|
-
expect(out).not.toBeNull();
|
|
244
|
-
expect(updates).toHaveLength(1);
|
|
245
|
-
// OWNERSHIP: assignee_id = the lane session (so the gate can key caller==assignee).
|
|
246
|
-
expect(updates[0].assignee_id).toBe("ses-9");
|
|
247
|
-
// BINDING: kind_data.lane.session set (resolveLaneTask can now find it).
|
|
248
|
-
const kd = updates[0].kind_data as { lane: { session: string; pane: string }; assignee_type: string };
|
|
249
|
-
expect(kd.lane.session).toBe("ses-9");
|
|
250
|
-
expect(kd.lane.pane).toBe("tmux:%5");
|
|
251
|
-
expect(kd.assignee_type).toBe("agent");
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it("preserves existing kind_data when merging the lane marker onto it", async () => {
|
|
255
|
-
const task = { id: "t", kind: "task", kind_data: { assignee_type: "agent", other: "keep" } };
|
|
256
|
-
const { prisma, updates } = fakeAdoptPrisma(task);
|
|
257
|
-
await adoptAssignmentTask(prisma as never, "t", { session: "s", pane: null });
|
|
258
|
-
const kd = updates[0].kind_data as { other: string; lane: { session: string } };
|
|
259
|
-
expect(kd.other).toBe("keep"); // not clobbered
|
|
260
|
-
expect(kd.lane.session).toBe("s");
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
it("RE-BINDS a task a stale session owns to the fresh lane (pane slot recovery)", async () => {
|
|
264
|
-
// Migration 109 makes (kind_data->'lane'->>'pane') unique WHERE active, so a
|
|
265
|
-
// re-spawn on a pane whose prior task is still active under a dead session
|
|
266
|
-
// cannot create a new task (P2002). adopt re-binds the assignment task to
|
|
267
|
-
// the fresh session + releases the stale pane slot first. A stale session
|
|
268
|
-
// is by definition dead, so the task belongs to whichever lane was
|
|
269
|
-
// dispatched to it — re-binding is correct recovery, not theft.
|
|
270
|
-
const task = { id: "t", kind: "task", kind_data: { lane: { session: "someone-else", pane: "tmux:%9" } } };
|
|
271
|
-
const { prisma, updates, updateManyCalls } = fakeAdoptPrisma(task);
|
|
272
|
-
const out = await adoptAssignmentTask(prisma as never, "t", { session: "fresh", pane: "tmux:%9" });
|
|
273
|
-
expect(out).not.toBeNull();
|
|
274
|
-
expect(updates).toHaveLength(1);
|
|
275
|
-
expect((updates[0].kind_data as { lane: { session: string } }).lane.session).toBe("fresh");
|
|
276
|
-
expect(updates[0].assignee_id).toBe("fresh");
|
|
277
|
-
// The stale pane slot was released before the re-bind (STEAL-before-CLAIM).
|
|
278
|
-
expect(updateManyCalls).toHaveLength(1);
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
it("re-adopts a task already bound to THIS session (idempotent re-bind)", async () => {
|
|
282
|
-
const task = { id: "t", kind: "task", kind_data: { lane: { session: "ses-9" } } };
|
|
283
|
-
const { prisma, updates } = fakeAdoptPrisma(task);
|
|
284
|
-
const out = await adoptAssignmentTask(prisma as never, "t", { session: "ses-9", pane: "tmux:%5" });
|
|
285
|
-
expect(out).not.toBeNull();
|
|
286
|
-
expect(updates).toHaveLength(1);
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it("returns null when the id is absent (nothing to adopt — caller creates)", async () => {
|
|
290
|
-
const { prisma, updates } = fakeAdoptPrisma(null);
|
|
291
|
-
expect(await adoptAssignmentTask(prisma as never, null, { session: "s", pane: null })).toBeNull();
|
|
292
|
-
expect(await adoptAssignmentTask(prisma as never, undefined, { session: "s", pane: null })).toBeNull();
|
|
293
|
-
expect(updates).toHaveLength(0);
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
it("returns null when the id is a PROJECT, not a task (disambiguates by kind)", async () => {
|
|
297
|
-
// A project parent must NOT be adopted — bindLane creates a task UNDER it.
|
|
298
|
-
const { prisma, updates } = fakeAdoptPrisma({ id: "proj-1", kind: "project", kind_data: {} });
|
|
299
|
-
expect(await adoptAssignmentTask(prisma as never, "proj-1", { session: "s", pane: null })).toBeNull();
|
|
300
|
-
expect(updates).toHaveLength(0);
|
|
301
|
-
});
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
describe("bindTaskToKnownSession (resume path binds to a known session up front)", () => {
|
|
305
|
-
it("binds the task to the known resumeId session (ownership = resumeId)", async () => {
|
|
306
|
-
const task = { id: "t", kind: "task", kind_data: { assignee_type: "agent" }, assignee_id: null };
|
|
307
|
-
const prisma = {
|
|
308
|
-
work_items: {
|
|
309
|
-
findFirst: async () => task,
|
|
310
|
-
update: async (a: { where: { id: string }; data: Record<string, unknown> }) =>
|
|
311
|
-
({ ...task, ...a.data, id: a.where.id }) as Record<string, unknown>,
|
|
312
|
-
},
|
|
313
|
-
};
|
|
314
|
-
const out = (await bindTaskToKnownSession(prisma as never, "t", {
|
|
315
|
-
session: "resumeId-1",
|
|
316
|
-
pane: "tmux:%9",
|
|
317
|
-
})) as Record<string, unknown> | null;
|
|
318
|
-
expect(out).not.toBeNull();
|
|
319
|
-
expect(out?.assignee_id).toBe("resumeId-1");
|
|
320
|
-
});
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
describe("resolveAssignOrg", () => {
|
|
324
|
-
it("returns the explicit org when supplied", () => {
|
|
325
|
-
expect(resolveAssignOrg("org-explicit")).toBe("org-explicit");
|
|
326
|
-
});
|
|
327
|
-
it("falls back to the INJECTED platform org when explicit is omitted", () => {
|
|
328
|
-
// Pure: platform org arrives from the caller (resolved at the handler
|
|
329
|
-
// boundary), never pulled from the registry inside this module.
|
|
330
|
-
expect(resolveAssignOrg(undefined, "org-platform")).toBe("org-platform");
|
|
331
|
-
});
|
|
332
|
-
it("prefers explicit over the injected platform org", () => {
|
|
333
|
-
expect(resolveAssignOrg("org-explicit", "org-platform")).toBe("org-explicit");
|
|
334
|
-
});
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
describe("bindLaneInput (body parent_id reaches the adopt path — schema-less gap closed)", () => {
|
|
338
|
-
// The data-not-serving gap this schema exists to close: a SessionStart
|
|
339
|
-
// adapter POSTs the work item id as a DIRECT body field, but oRPC parses the
|
|
340
|
-
// body into `input` ONLY when a `.input()` schema is declared. Without one,
|
|
341
|
-
// parent_id arrived as an empty input, adopt got null, and readAssignedBrief
|
|
342
|
-
// returned nulls for a session with nothing bound — read by one lane as
|
|
343
|
-
// "fn.taskSync is 404". The schema makes the body parse; the header stays as
|
|
344
|
-
// the redundant transport a header-only caller already sends.
|
|
345
|
-
it("parses a direct body carrying parent_id (the assignment task id)", () => {
|
|
346
|
-
const out = v.safeParse(bindLaneInput, {
|
|
347
|
-
lane: { pane: "tmux:%5", session: "ses-1", cli_kind: "pi", role: "agent" },
|
|
348
|
-
parent_id: "task-99",
|
|
349
|
-
});
|
|
350
|
-
expect(out.success).toBe(true);
|
|
351
|
-
expect(out.success && out.output.parent_id).toBe("task-99");
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
it("parses an EMPTY body to all-undefined (header-only caller behaves as before)", () => {
|
|
355
|
-
// A caller transporting everything on headers sends {} (or omits the
|
|
356
|
-
// body); the schema must not reject it, and every field is undefined so the
|
|
357
|
-
// handler's header fallbacks carry the values.
|
|
358
|
-
expect(v.safeParse(bindLaneInput, {}).success).toBe(true);
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
it("ACCEPTS undefined input — a raw-object POST yields undefined (root-optional)", () => {
|
|
362
|
-
// oRPC extracts input from the {json:{...}} envelope; a raw-object POST
|
|
363
|
-
// (what zerostack/pi send today) yields undefined. The root MUST be optional
|
|
364
|
-
// or those callers 400 instead of falling back to headers. A strict root
|
|
365
|
-
// regressed exactly this (0.23.314), so this assertion locks the tolerance.
|
|
366
|
-
expect(v.safeParse(bindLaneInput, undefined).success).toBe(true);
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
it("treats every field as optional (no caller is forced to populate the body)", () => {
|
|
370
|
-
expect(v.safeParse(bindLaneInput, { lane: { cli_kind: "zerostack" } }).success).toBe(true);
|
|
371
|
-
expect(v.safeParse(bindLaneInput, { parent_id: "t-1" }).success).toBe(true);
|
|
372
|
-
expect(v.safeParse(bindLaneInput, { title: "Lane" }).success).toBe(true);
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
it("does NOT require lane.session in the body (session may arrive on a header)", () => {
|
|
376
|
-
// bindLane resolves session from raw.lane?.session ?? x-lane-session, so the
|
|
377
|
-
// schema must not make the body the sole transport for it.
|
|
378
|
-
expect(v.safeParse(bindLaneInput, { parent_id: "t-1" }).success).toBe(true);
|
|
379
|
-
});
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
describe("buildAssignmentSpawnPayload (no keystroke brief — row-based self-brief)", () => {
|
|
383
|
-
const task = { id: "task-99" };
|
|
384
|
-
|
|
385
|
-
it("emits command spawn_agent_tab carrying the taskId as workItemId", () => {
|
|
386
|
-
const out = buildAssignmentSpawnPayload(task, { slug: "cc-glm52", windowName: "FleetTruth" });
|
|
387
|
-
expect(out.command).toBe("spawn_agent_tab");
|
|
388
|
-
expect(out.payload.workItemId).toBe("task-99");
|
|
389
|
-
expect(out.payload.slug).toBe("cc-glm52");
|
|
390
|
-
expect(out.payload.windowName).toBe("FleetTruth");
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
it("NEVER carries initialPrompt — the lane pulls its brief from the DB by session id", () => {
|
|
394
|
-
// Regression-lock for the failure class this deletes: a 4400-char
|
|
395
|
-
// initialPrompt fails; a 2600-char one arrives but is never submitted.
|
|
396
|
-
// The moment someone re-adds keystroke delivery this assertion fails.
|
|
397
|
-
const out = buildAssignmentSpawnPayload(task, { slug: "cc-glm52" });
|
|
398
|
-
expect("initialPrompt" in out.payload).toBe(false);
|
|
399
|
-
expect(out.payload).not.toHaveProperty("initialPrompt");
|
|
400
|
-
expect("cliSessionId" in out.payload).toBe(false);
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
it("omits optional spawn fields when not supplied (no undefined keys)", () => {
|
|
404
|
-
const out = buildAssignmentSpawnPayload(task, {});
|
|
405
|
-
expect(out.payload).toEqual({ workItemId: "task-99" });
|
|
406
|
-
expect("slug" in out.payload).toBe(false);
|
|
407
|
-
expect("windowName" in out.payload).toBe(false);
|
|
408
|
-
expect("idempotencyKey" in out.payload).toBe(false);
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
it("forwards idempotencyKey when supplied (retries reuse it, new spawns don't)", () => {
|
|
412
|
-
const out = buildAssignmentSpawnPayload(task, { idempotencyKey: "k-1" });
|
|
413
|
-
expect(out.payload.idempotencyKey).toBe("k-1");
|
|
414
|
-
});
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
describe("resolveBindAction (a spawn that cannot bind fails loudly, never orphans)", () => {
|
|
418
|
-
// The `absent-input-reads-as-success` failure this closes: a spawn carried a
|
|
419
|
-
// workItemId the lane should adopt, but no task with that id exists. The old
|
|
420
|
-
// path silently created a NEW task, abandoning the spawn's referenced brief
|
|
421
|
-
// and binding the lane to an empty task it was never meant to own — an orphan
|
|
422
|
-
// an orchestrator cannot tell from real unowned work. bindLane now REFUSES on
|
|
423
|
-
// this outcome; this is the pure, unit-testable decision behind that refusal.
|
|
424
|
-
function taskRow(id: string): WorkItem {
|
|
425
|
-
return {
|
|
426
|
-
id,
|
|
427
|
-
kind: "task",
|
|
428
|
-
parent_id: null,
|
|
429
|
-
organisation_id: "org",
|
|
430
|
-
title: "T",
|
|
431
|
-
status: "active",
|
|
432
|
-
position: null,
|
|
433
|
-
completed: null,
|
|
434
|
-
kind_data: {},
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
it("uses the adopted task when adopt succeeds", () => {
|
|
439
|
-
const t = taskRow("t1");
|
|
440
|
-
const out = resolveBindAction({ adopted: t, workItemId: "t1", referencedKind: "task" });
|
|
441
|
-
expect(out.action).toBe("use");
|
|
442
|
-
if (out.action === "use") expect(out.task.id).toBe("t1");
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
it("creates for an exploratory lane (no workItemId supplied)", () => {
|
|
446
|
-
const out = resolveBindAction({ adopted: null, workItemId: null, referencedKind: null });
|
|
447
|
-
expect(out.action).toBe("create");
|
|
448
|
-
if (out.action === "create") expect(out.scope).toBe("exploratory");
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
it("creates under a PROJECT parent (workItemId resolves to a project, not a task)", () => {
|
|
452
|
-
const out = resolveBindAction({
|
|
453
|
-
adopted: null,
|
|
454
|
-
workItemId: "proj-1",
|
|
455
|
-
referencedKind: "project",
|
|
456
|
-
});
|
|
457
|
-
expect(out.action).toBe("create");
|
|
458
|
-
if (out.action === "create") expect(out.scope).toBe("project-parent");
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
it("REFUSES when the spawn named a task that does not exist — no orphan created", () => {
|
|
462
|
-
// THE owed test: a spawn carrying a workItemId that resolves to NO task
|
|
463
|
-
// must fail (refuse) instead of silently creating a new orphan task.
|
|
464
|
-
const out = resolveBindAction({
|
|
465
|
-
adopted: null,
|
|
466
|
-
workItemId: "ghost-9",
|
|
467
|
-
referencedKind: null,
|
|
468
|
-
});
|
|
469
|
-
expect(out.action).toBe("refuse");
|
|
470
|
-
if (out.action === "refuse") {
|
|
471
|
-
expect(out.reason).toBe("missing-referenced-task");
|
|
472
|
-
expect(out.workItemId).toBe("ghost-9");
|
|
473
|
-
}
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
it("prefers a successful adopt even when a (stale) referenced kind is present", () => {
|
|
477
|
-
// adopt already writes the session + assignee_id; once it succeeds the
|
|
478
|
-
// referenced-kind probe is irrelevant.
|
|
479
|
-
const t = taskRow("t1");
|
|
480
|
-
const out = resolveBindAction({ adopted: t, workItemId: "t1", referencedKind: null });
|
|
481
|
-
expect(out.action).toBe("use");
|
|
482
|
-
});
|
|
483
|
-
});
|