@teamscala/service-boot 0.23.313 → 0.23.315
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,42 @@ 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
|
+
* ROOT-OPTIONAL (`v.optional(v.object(...))`) so it tolerates a caller that sends
|
|
97
|
+
* a DIRECT body or none at all: oRPC extracts `input` from the `{json:{...}}` RPC
|
|
98
|
+
* envelope, so a raw-object or empty POST yields `undefined`, which the optional
|
|
99
|
+
* root ACCEPTS — the handler then falls back to its headers, exactly as it did
|
|
100
|
+
* schema-less (a header-only caller like zerostack is unchanged). An adapter that
|
|
101
|
+
* wants the body path (pi's `parent_id`) POSTs the `{json:{...}}` envelope and
|
|
102
|
+
* `parent_id` is parsed + reaches `adoptAssignmentTask` — the same envelope
|
|
103
|
+
* convention pi ALREADY uses for upsertFromTaskList. A raw-object POST is NOT
|
|
104
|
+
* parsed (oRPC hands the handler `undefined`), which is why pi's bindLaneTask
|
|
105
|
+
* must move to the envelope to deliver `parent_id`. Every field is optional with
|
|
106
|
+
* a header fallback, so the schema ADDS the body path without removing the header.
|
|
107
|
+
*/
|
|
108
|
+
export declare const bindLaneInput: v.OptionalSchema<v.ObjectSchema<{
|
|
109
|
+
readonly lane: v.OptionalSchema<v.ObjectSchema<{
|
|
110
|
+
readonly pane: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
111
|
+
readonly session: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
112
|
+
readonly cli_kind: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
113
|
+
readonly role: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
114
|
+
readonly organisation_id: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
115
|
+
}, undefined>, undefined>;
|
|
116
|
+
readonly title: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
117
|
+
readonly parent_id: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
118
|
+
}, undefined>, undefined>;
|
|
83
119
|
/**
|
|
84
120
|
* Resolve the org a dispatched task is created under. Pure: the platform org is
|
|
85
121
|
* 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,51 @@ 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
|
+
expect(v.safeParse(bindLaneInput, {}).success).toBe(true);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("ACCEPTS undefined input — a raw-object POST yields undefined (root-optional)", () => {
|
|
316
|
+
// oRPC extracts input from the {json:{...}} envelope; a raw-object POST
|
|
317
|
+
// (what zerostack/pi send today) yields undefined. The root MUST be optional
|
|
318
|
+
// or those callers 400 instead of falling back to headers. A strict root
|
|
319
|
+
// regressed exactly this (0.23.314), so this assertion locks the tolerance.
|
|
320
|
+
expect(v.safeParse(bindLaneInput, undefined).success).toBe(true);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("treats every field as optional (no caller is forced to populate the body)", () => {
|
|
324
|
+
expect(v.safeParse(bindLaneInput, { lane: { cli_kind: "zerostack" } }).success).toBe(true);
|
|
325
|
+
expect(v.safeParse(bindLaneInput, { parent_id: "t-1" }).success).toBe(true);
|
|
326
|
+
expect(v.safeParse(bindLaneInput, { title: "Lane" }).success).toBe(true);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("does NOT require lane.session in the body (session may arrive on a header)", () => {
|
|
330
|
+
// bindLane resolves session from raw.lane?.session ?? x-lane-session, so the
|
|
331
|
+
// schema must not make the body the sole transport for it.
|
|
332
|
+
expect(v.safeParse(bindLaneInput, { parent_id: "t-1" }).success).toBe(true);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
290
336
|
describe("buildAssignmentSpawnPayload (no keystroke brief — row-based self-brief)", () => {
|
|
291
337
|
const task = { id: "task-99" };
|
|
292
338
|
|
|
@@ -92,6 +92,47 @@ 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
|
+
* ROOT-OPTIONAL (`v.optional(v.object(...))`) so it tolerates a caller that sends
|
|
109
|
+
* a DIRECT body or none at all: oRPC extracts `input` from the `{json:{...}}` RPC
|
|
110
|
+
* envelope, so a raw-object or empty POST yields `undefined`, which the optional
|
|
111
|
+
* root ACCEPTS — the handler then falls back to its headers, exactly as it did
|
|
112
|
+
* schema-less (a header-only caller like zerostack is unchanged). An adapter that
|
|
113
|
+
* wants the body path (pi's `parent_id`) POSTs the `{json:{...}}` envelope and
|
|
114
|
+
* `parent_id` is parsed + reaches `adoptAssignmentTask` — the same envelope
|
|
115
|
+
* convention pi ALREADY uses for upsertFromTaskList. A raw-object POST is NOT
|
|
116
|
+
* parsed (oRPC hands the handler `undefined`), which is why pi's bindLaneTask
|
|
117
|
+
* must move to the envelope to deliver `parent_id`. Every field is optional with
|
|
118
|
+
* a header fallback, so the schema ADDS the body path without removing the header.
|
|
119
|
+
*/
|
|
120
|
+
export const bindLaneInput = v.optional(
|
|
121
|
+
v.object({
|
|
122
|
+
lane: v.optional(
|
|
123
|
+
v.object({
|
|
124
|
+
pane: v.optional(v.string()),
|
|
125
|
+
session: v.optional(v.string()),
|
|
126
|
+
cli_kind: v.optional(v.string()),
|
|
127
|
+
role: v.optional(v.string()),
|
|
128
|
+
organisation_id: v.optional(v.string()),
|
|
129
|
+
}),
|
|
130
|
+
),
|
|
131
|
+
title: v.optional(v.string()),
|
|
132
|
+
parent_id: v.optional(v.string()),
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
|
|
95
136
|
/**
|
|
96
137
|
* Resolve the org a dispatched task is created under. Pure: the platform org is
|
|
97
138
|
* 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.OptionalSchema<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>, 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,10 +443,17 @@ 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;
|
|
448
|
-
// cli_kind via header
|
|
449
|
-
//
|
|
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.
|
|
450
457
|
const headerWorkKind = ctx.headers?.get("x-lane-cli-kind") ?? null;
|
|
451
458
|
if (!session) {
|
|
452
459
|
const { ORPCError } = await import("@orpc/server");
|