@teamscala/service-boot 0.23.312 → 0.23.314
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
CHANGED
|
@@ -80,6 +80,39 @@ export declare const assignTaskInput: v.ObjectSchema<{
|
|
|
80
80
|
}, undefined>;
|
|
81
81
|
/** The resolved (post-schema) shape the handlers operate on. */
|
|
82
82
|
export type AssignTaskInput = v.InferOutput<typeof assignTaskInput>;
|
|
83
|
+
/**
|
|
84
|
+
* bindLane ORPC input. Declaring the schema is the documented remedy for the
|
|
85
|
+
* schema-less-body gap (`upsertFromTaskListInput` cites the same measured bug):
|
|
86
|
+
* oRPC does NOT parse a JSON body into `input` for a procedure with no
|
|
87
|
+
* `.input()`, so a SessionStart adapter that POSTs the work item id as a DIRECT
|
|
88
|
+
* body field (`{lane:{...}, parent_id}`) arrives as an EMPTY input — `parent_id`
|
|
89
|
+
* is silently dropped, `adoptAssignmentTask` gets null, no adoption happens, and
|
|
90
|
+
* `readAssignedBrief` returns nulls for a session that has nothing bound to it.
|
|
91
|
+
* That is a DATA gap (nothing bound to the session), never a serving one — which
|
|
92
|
+
* is exactly the misdiagnosis this closes: a prior lane read those nulls as
|
|
93
|
+
* "fn.taskSync is 404" and reached for serving changes, when the row was simply
|
|
94
|
+
* never bound because the body never parsed.
|
|
95
|
+
*
|
|
96
|
+
* With the schema oRPC parses the body, so `parent_id` reaches the adopt path.
|
|
97
|
+
* Every field is OPTIONAL and the handler keeps a header fallback for each, so a
|
|
98
|
+
* caller that transports everything on headers (zerostack: x-lane-pane/session/
|
|
99
|
+
* work-item) parses to an empty object and behaves EXACTLY as before — the schema
|
|
100
|
+
* ADDS the body path without removing the header one. As a side effect it also
|
|
101
|
+
* fixes zerostack cli_kind mis-attribution: zerostack's body `{lane:{cli_kind:
|
|
102
|
+
* "zerostack"}}` was never parsed, so its cli_kind fell through every header to
|
|
103
|
+
* the "claude-code" default; parsed now, it reads "zerostack" as intended.
|
|
104
|
+
*/
|
|
105
|
+
export declare const bindLaneInput: v.ObjectSchema<{
|
|
106
|
+
readonly lane: v.OptionalSchema<v.ObjectSchema<{
|
|
107
|
+
readonly pane: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
108
|
+
readonly session: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
109
|
+
readonly cli_kind: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
110
|
+
readonly role: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
111
|
+
readonly organisation_id: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
112
|
+
}, undefined>, undefined>;
|
|
113
|
+
readonly title: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
114
|
+
readonly parent_id: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
115
|
+
}, undefined>;
|
|
83
116
|
/**
|
|
84
117
|
* Resolve the org a dispatched task is created under. Pure: the platform org is
|
|
85
118
|
* INJECTED (resolved by the caller at the handler boundary, where a registry
|
|
@@ -13,6 +13,7 @@ import { describe, it, expect } from "bun:test";
|
|
|
13
13
|
import * as v from "valibot";
|
|
14
14
|
import {
|
|
15
15
|
assignTaskInput,
|
|
16
|
+
bindLaneInput,
|
|
16
17
|
createBriefTaskInput,
|
|
17
18
|
createTaskWithBriefComment,
|
|
18
19
|
adoptAssignmentTask,
|
|
@@ -287,6 +288,44 @@ describe("resolveAssignOrg", () => {
|
|
|
287
288
|
});
|
|
288
289
|
});
|
|
289
290
|
|
|
291
|
+
describe("bindLaneInput (body parent_id reaches the adopt path — schema-less gap closed)", () => {
|
|
292
|
+
// The data-not-serving gap this schema exists to close: a SessionStart
|
|
293
|
+
// adapter POSTs the work item id as a DIRECT body field, but oRPC parses the
|
|
294
|
+
// body into `input` ONLY when a `.input()` schema is declared. Without one,
|
|
295
|
+
// parent_id arrived as an empty input, adopt got null, and readAssignedBrief
|
|
296
|
+
// returned nulls for a session with nothing bound — read by one lane as
|
|
297
|
+
// "fn.taskSync is 404". The schema makes the body parse; the header stays as
|
|
298
|
+
// the redundant transport a header-only caller already sends.
|
|
299
|
+
it("parses a direct body carrying parent_id (the assignment task id)", () => {
|
|
300
|
+
const out = v.safeParse(bindLaneInput, {
|
|
301
|
+
lane: { pane: "tmux:%5", session: "ses-1", cli_kind: "pi", role: "agent" },
|
|
302
|
+
parent_id: "task-99",
|
|
303
|
+
});
|
|
304
|
+
expect(out.success).toBe(true);
|
|
305
|
+
expect(out.success && out.output.parent_id).toBe("task-99");
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("parses an EMPTY body to all-undefined (header-only caller behaves as before)", () => {
|
|
309
|
+
// A caller transporting everything on headers sends {} (or omits the
|
|
310
|
+
// body); the schema must not reject it, and every field is undefined so the
|
|
311
|
+
// handler's header fallbacks carry the values.
|
|
312
|
+
const out = v.safeParse(bindLaneInput, {});
|
|
313
|
+
expect(out.success).toBe(true);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("treats every field as optional (no caller is forced to populate the body)", () => {
|
|
317
|
+
expect(v.safeParse(bindLaneInput, { lane: { cli_kind: "zerostack" } }).success).toBe(true);
|
|
318
|
+
expect(v.safeParse(bindLaneInput, { parent_id: "t-1" }).success).toBe(true);
|
|
319
|
+
expect(v.safeParse(bindLaneInput, { title: "Lane" }).success).toBe(true);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("does NOT require lane.session in the body (session may arrive on a header)", () => {
|
|
323
|
+
// bindLane resolves session from raw.lane?.session ?? x-lane-session, so the
|
|
324
|
+
// schema must not make the body the sole transport for it.
|
|
325
|
+
expect(v.safeParse(bindLaneInput, { parent_id: "t-1" }).success).toBe(true);
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
290
329
|
describe("buildAssignmentSpawnPayload (no keystroke brief — row-based self-brief)", () => {
|
|
291
330
|
const task = { id: "task-99" };
|
|
292
331
|
|
|
@@ -92,6 +92,42 @@ export const assignTaskInput = v.object({
|
|
|
92
92
|
/** The resolved (post-schema) shape the handlers operate on. */
|
|
93
93
|
export type AssignTaskInput = v.InferOutput<typeof assignTaskInput>;
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* bindLane ORPC input. Declaring the schema is the documented remedy for the
|
|
97
|
+
* schema-less-body gap (`upsertFromTaskListInput` cites the same measured bug):
|
|
98
|
+
* oRPC does NOT parse a JSON body into `input` for a procedure with no
|
|
99
|
+
* `.input()`, so a SessionStart adapter that POSTs the work item id as a DIRECT
|
|
100
|
+
* body field (`{lane:{...}, parent_id}`) arrives as an EMPTY input — `parent_id`
|
|
101
|
+
* is silently dropped, `adoptAssignmentTask` gets null, no adoption happens, and
|
|
102
|
+
* `readAssignedBrief` returns nulls for a session that has nothing bound to it.
|
|
103
|
+
* That is a DATA gap (nothing bound to the session), never a serving one — which
|
|
104
|
+
* is exactly the misdiagnosis this closes: a prior lane read those nulls as
|
|
105
|
+
* "fn.taskSync is 404" and reached for serving changes, when the row was simply
|
|
106
|
+
* never bound because the body never parsed.
|
|
107
|
+
*
|
|
108
|
+
* With the schema oRPC parses the body, so `parent_id` reaches the adopt path.
|
|
109
|
+
* Every field is OPTIONAL and the handler keeps a header fallback for each, so a
|
|
110
|
+
* caller that transports everything on headers (zerostack: x-lane-pane/session/
|
|
111
|
+
* work-item) parses to an empty object and behaves EXACTLY as before — the schema
|
|
112
|
+
* ADDS the body path without removing the header one. As a side effect it also
|
|
113
|
+
* fixes zerostack cli_kind mis-attribution: zerostack's body `{lane:{cli_kind:
|
|
114
|
+
* "zerostack"}}` was never parsed, so its cli_kind fell through every header to
|
|
115
|
+
* the "claude-code" default; parsed now, it reads "zerostack" as intended.
|
|
116
|
+
*/
|
|
117
|
+
export const bindLaneInput = v.object({
|
|
118
|
+
lane: v.optional(
|
|
119
|
+
v.object({
|
|
120
|
+
pane: v.optional(v.string()),
|
|
121
|
+
session: v.optional(v.string()),
|
|
122
|
+
cli_kind: v.optional(v.string()),
|
|
123
|
+
role: v.optional(v.string()),
|
|
124
|
+
organisation_id: v.optional(v.string()),
|
|
125
|
+
}),
|
|
126
|
+
),
|
|
127
|
+
title: v.optional(v.string()),
|
|
128
|
+
parent_id: v.optional(v.string()),
|
|
129
|
+
});
|
|
130
|
+
|
|
95
131
|
/**
|
|
96
132
|
* Resolve the org a dispatched task is created under. Pure: the platform org is
|
|
97
133
|
* INJECTED (resolved by the caller at the handler boundary, where a registry
|
|
@@ -187,7 +187,17 @@ export declare const taskSyncRouter: {
|
|
|
187
187
|
prisma: any;
|
|
188
188
|
user: import("./auth-router.ts").User;
|
|
189
189
|
orgId: string | null | undefined;
|
|
190
|
-
}>,
|
|
190
|
+
}>, v.ObjectSchema<{
|
|
191
|
+
readonly lane: v.OptionalSchema<v.ObjectSchema<{
|
|
192
|
+
readonly pane: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
193
|
+
readonly session: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
194
|
+
readonly cli_kind: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
195
|
+
readonly role: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
196
|
+
readonly organisation_id: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
197
|
+
}, undefined>, undefined>;
|
|
198
|
+
readonly title: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
199
|
+
readonly parent_id: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
200
|
+
}, undefined>, import("@orpc/contract").Schema<{
|
|
191
201
|
task_id: string;
|
|
192
202
|
created: boolean;
|
|
193
203
|
}, {
|
|
@@ -43,7 +43,7 @@ import { authed, type Context } from "./auth-router.ts";
|
|
|
43
43
|
import { notifyLaneOfTaskChange } from "./task-sync-dispatch.ts";
|
|
44
44
|
import { resolveLaneTask, resolveLaneWorkName } from "./task-sync-lane.ts";
|
|
45
45
|
import { readAssignedBriefHandler } from "./task-sync-brief.ts";
|
|
46
|
-
import { adoptAssignmentTask, createBriefTaskHandler } from "./task-sync-assign.ts";
|
|
46
|
+
import { adoptAssignmentTask, bindLaneInput, createBriefTaskHandler } from "./task-sync-assign.ts";
|
|
47
47
|
import * as v from "valibot";
|
|
48
48
|
import {
|
|
49
49
|
STATUS_TO_WORK_ITEM,
|
|
@@ -422,7 +422,7 @@ export const taskSyncRouter = {
|
|
|
422
422
|
* fleet_agent_runs row (matched by pane) to the task via tracked_task_id —
|
|
423
423
|
* the join the operator's lane-labels monitor + the unified fleet view read.
|
|
424
424
|
*/
|
|
425
|
-
bindLane: authed.handler(async ({ input, context }) => {
|
|
425
|
+
bindLane: authed.input(bindLaneInput).handler(async ({ input, context }) => {
|
|
426
426
|
const ctx = context as Context;
|
|
427
427
|
const prisma = ctx.prisma as Prisma;
|
|
428
428
|
const log = getAppLogger();
|
|
@@ -443,8 +443,18 @@ export const taskSyncRouter = {
|
|
|
443
443
|
const pane = raw.lane?.pane ?? headerPane;
|
|
444
444
|
const headerSession = ctx.headers?.get("x-lane-session") ?? null;
|
|
445
445
|
const session = raw.lane?.session ?? headerSession;
|
|
446
|
-
// parent_id
|
|
446
|
+
// parent_id arrives EITHER as a body field (parsed now that bindLane has an
|
|
447
|
+
// `.input()` schema — bindLaneInput) OR on the x-lane-work-item header. Both
|
|
448
|
+
// are the assignment task id this lane ADOPTS (`adoptAssignmentTask`); the
|
|
449
|
+
// header stays as the redundant transport a header-only caller (zerostack)
|
|
450
|
+
// already sends. Before the schema, the body path silently never parsed, so a
|
|
451
|
+
// body-only caller (pi) dropped parent_id, never adopted, and self-briefed to
|
|
452
|
+
// nulls — the data-not-serving gap `bindLaneInput` exists to close.
|
|
447
453
|
const headerWorkItem = ctx.headers?.get("x-lane-work-item") ?? null;
|
|
454
|
+
// cli_kind via header is the fallback for callers that transport it there; a
|
|
455
|
+
// caller that puts cli_kind in the body lane object (parsed by bindLaneInput)
|
|
456
|
+
// is read from raw.lane.cli_kind first.
|
|
457
|
+
const headerWorkKind = ctx.headers?.get("x-lane-cli-kind") ?? null;
|
|
448
458
|
if (!session) {
|
|
449
459
|
const { ORPCError } = await import("@orpc/server");
|
|
450
460
|
throw new ORPCError("BAD_REQUEST", {
|
|
@@ -454,7 +464,7 @@ export const taskSyncRouter = {
|
|
|
454
464
|
const lane: LaneIdentity = {
|
|
455
465
|
pane,
|
|
456
466
|
session,
|
|
457
|
-
cli_kind: raw.lane?.cli_kind ?? "claude-code",
|
|
467
|
+
cli_kind: (raw.lane?.cli_kind ?? headerWorkKind ?? "claude-code") as LaneIdentity["cli_kind"],
|
|
458
468
|
role: raw.lane?.role ?? "agent",
|
|
459
469
|
organisation_id: raw.lane?.organisation_id,
|
|
460
470
|
};
|