merge: slice 21 FR-4 — queue ergonomics (release-0.3.2)

This commit is contained in:
mvschwarz committed 2026-06-01 05:56:35 -07:00
commit e154a5cef1
6 files changed
+455 -10

No files matched your search

+123 -6
View File
@@ -1,3 +1,4 @@
import fs from "node:fs";
import { Command } from "commander";
import { DaemonClient } from "../client.js";
import { getDaemonStatus, getDaemonUrl } from "../daemon-lifecycle.js";
@@ -41,6 +42,95 @@ function printResult(json: boolean, body: unknown, status: number): void {
if (status >= 400) process.exitCode = status >= 500 ? 2 : 1;
}
// OPR.0.3.2.21.FR-4(a) — body input resolution. Three accepted shapes:
// --body "<text>" inline (legacy; backtick-prone for raw
// multiline content)
// --body-file <path> read body content from a file path
// (kills the backtick-corruption class)
// --body-file - or --body - read body from stdin (pipeline-friendly)
//
// Exactly one of --body / --body-file must be provided; the resolver throws
// a 3-part fact/consequence/action error otherwise.
//
// stdinReader is dependency-injected so tests can swap it without touching
// process.stdin. Default reads UTF-8 from process.stdin until EOF.
export interface ResolveBodyOpts {
body?: string;
bodyFile?: string;
}
export async function defaultStdinReader(): Promise<string> {
return new Promise<string>((resolve, reject) => {
let data = "";
process.stdin.setEncoding("utf8");
process.stdin.on("data", (chunk: string) => { data += chunk; });
process.stdin.on("end", () => resolve(data));
process.stdin.on("error", reject);
if (process.stdin.isTTY) {
// No pipe is connected to stdin; resolve immediately to empty
// rather than blocking forever waiting for data on a TTY. The
// empty body then flows through to the daemon's content
// validation (queue-repository owns the body contract); the CLI
// does not error locally on empty stdin.
resolve("");
}
});
}
export async function resolveQueueBody(
opts: ResolveBodyOpts,
stdinReader: () => Promise<string> = defaultStdinReader,
): Promise<string> {
const hasInline = opts.body !== undefined && opts.body !== "";
const hasFile = opts.bodyFile !== undefined && opts.bodyFile !== "";
if (hasInline && hasFile) {
const err = new Error("--body and --body-file are mutually exclusive.") as Error & { fact?: string; consequence?: string; action?: string };
err.fact = "Both --body and --body-file were passed; the body source is ambiguous.";
err.consequence = "rig queue create did not run; daemon was not contacted.";
err.action = "Pass exactly one of --body or --body-file.";
throw err;
}
if (!hasInline && !hasFile) {
const err = new Error("Missing required body input.") as Error & { fact?: string; consequence?: string; action?: string };
err.fact = "Neither --body nor --body-file was provided.";
err.consequence = "rig queue create did not run; daemon was not contacted.";
err.action = "Pass the qitem body via --body \"<text>\" or --body-file <path> (use - for stdin).";
throw err;
}
if (hasInline) {
if (opts.body === "-") return stdinReader();
return opts.body!;
}
// hasFile path
if (opts.bodyFile === "-") return stdinReader();
const absPath = opts.bodyFile!;
if (!fs.existsSync(absPath)) {
const err = new Error(`--body-file path does not exist: ${absPath}`) as Error & { fact?: string; consequence?: string; action?: string };
err.fact = `--body-file path does not exist: ${absPath}`;
err.consequence = "rig queue create did not run; daemon was not contacted.";
err.action = "Check the path; pass an absolute path; or use --body-file - to read from stdin.";
throw err;
}
const stat = fs.statSync(absPath);
if (!stat.isFile()) {
const err = new Error(`--body-file path is not a regular file: ${absPath}`) as Error & { fact?: string; consequence?: string; action?: string };
err.fact = `--body-file path is not a regular file: ${absPath}`;
err.consequence = "rig queue create did not run; daemon was not contacted.";
err.action = "Pass a path to a readable file (not a directory, symlink-to-directory, or block device). Use --body-file - to read from stdin.";
throw err;
}
return fs.readFileSync(absPath, "utf8");
}
function emitBodyResolveError(err: Error & { fact?: string; consequence?: string; action?: string }, json: boolean): void {
if (json) {
console.log(JSON.stringify({ ok: false, error: { fact: err.fact ?? err.message, consequence: err.consequence ?? "", action: err.action ?? "" } }, null, 2));
} else {
process.stderr.write(`Error: ${err.fact ?? err.message}\n${err.consequence ?? ""}\n${err.action ?? ""}\n`);
}
process.exitCode = 1;
}
function resolveCurrentSession(explicit: string | undefined, optionName: string): string | undefined {
const session = explicit ?? readOpenRigEnv("OPENRIG_SESSION_NAME", "RIGGED_SESSION_NAME");
if (session) return session;
@@ -62,10 +152,13 @@ export function queueCommand(depsOverride?: QueueDeps): Command {
.description("Create a new qitem")
.requiredOption("--source <session>", "Source session")
.requiredOption("--destination <session>", "Destination session (the seat that owns the work)")
.requiredOption("--body <text>", "Qitem body")
.option("--body <text>", "Qitem body inline (use - to read from stdin; mutually exclusive with --body-file)")
.option("--body-file <path>", "Read qitem body from a file path (use - for stdin; mutually exclusive with --body). Kills the backtick-shell-corruption class for multiline bodies.")
.option("--mission <id>", "First-class mission scope; translated to a mission:<id> tag (composes with --tags)")
.option("--slice <id>", "First-class slice scope; translated to a slice:<id> tag (composes with --tags)")
.option("--priority <priority>", "Priority: routine | urgent | critical", "routine")
.option("--tier <tier>", "Tier (e.g. fast, routine, deep, critical) — drives SLA")
.option("--tags <tags>", "Comma-separated tags")
.option("--tags <tags>", "Comma-separated tags (composes with --mission and --slice)")
.option("--expires-at <iso>", "ISO timestamp at which the qitem expires")
.option("--id <qitemId>", "Idempotent qitem_id (skip if not provided)")
.option("--target-repo <name>", "PL-007: typed repo scope (must match a repo in the source rig's RigSpec.workspace.repos[])")
@@ -74,7 +167,10 @@ export function queueCommand(depsOverride?: QueueDeps): Command {
.action(async (opts: {
source: string;
destination: string;
body: string;
body?: string;
bodyFile?: string;
mission?: string;
slice?: string;
priority: string;
tier?: string;
tags?: string;
@@ -84,14 +180,35 @@ export function queueCommand(depsOverride?: QueueDeps): Command {
nudge?: boolean;
json?: boolean;
}) => {
// OPR.0.3.2.21.FR-4(a) — resolve body BEFORE contacting the daemon
// so a missing/ambiguous body fails fast and locally.
let resolvedBody: string;
try {
resolvedBody = await resolveQueueBody({ body: opts.body, bodyFile: opts.bodyFile });
} catch (err) {
emitBodyResolveError(err as Error & { fact?: string; consequence?: string; action?: string }, opts.json ?? false);
return;
}
const deps = getDeps();
const tags = opts.tags ? opts.tags.split(",").map((s) => s.trim()).filter(Boolean) : undefined;
// OPR.0.3.2.21.FR-4(b) — first-class --mission / --slice flags
// translate to canonical mission:<id> / slice:<id> tags. Composes
// with --tags (any flag-derived tags prepend; explicit --tags
// append). De-duplicates so passing both --mission X and
// --tags mission:X yields one mission:X tag.
const fromTagsArg = opts.tags ? opts.tags.split(",").map((s) => s.trim()).filter(Boolean) : [];
const fromFlags: string[] = [];
if (opts.mission) fromFlags.push(`mission:${opts.mission}`);
if (opts.slice) fromFlags.push(`slice:${opts.slice}`);
const merged = [...fromFlags, ...fromTagsArg];
const seen = new Set<string>();
const dedupedTags = merged.filter((t) => { if (seen.has(t)) return false; seen.add(t); return true; });
const tags = dedupedTags.length > 0 ? dedupedTags : undefined;
await withClient(deps, async (client) => {
const res = await client.post<Record<string, unknown>>("/api/queue/create", {
qitemId: opts.id,
sourceSession: opts.source,
destinationSession: opts.destination,
body: opts.body,
body: resolvedBody,
priority: opts.priority,
tier: opts.tier,
tags,
@@ -141,7 +258,7 @@ export function queueCommand(depsOverride?: QueueDeps): Command {
cmd
.command("update <qitemId>")
.description("Mutate qitem state. state=done REQUIRES --closure-reason (one of: handed_off_to, blocked_on, denied, canceled, no-follow-on, escalation)")
.description("Mutate qitem state. state=done REQUIRES --closure-reason (one of: handed_off_to, blocked_on, denied, canceled, no-follow-on, escalation). Closure ≠ acceptance: handed_off_to records delivery to the next stage; acceptance is the next stage's verdict on its own qitem, not this closure.")
.option("--actor <session>", "Actor session performing the transition (defaults to OPENRIG_SESSION_NAME)")
.requiredOption("--state <state>", "New state: pending | in-progress | done | blocked | failed | denied | canceled | handed-off")
.option("--closure-reason <reason>", "Required for state=done")
+272
View File
@@ -1,5 +1,9 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import type { QueueDeps } from "../src/commands/queue.js";
import { resolveQueueBody } from "../src/commands/queue.js";
import { createProgram } from "../src/index.js";
/**
@@ -134,6 +138,274 @@ describe("rig queue CLI", () => {
expect((create!.body as { nudge: boolean }).nudge).toBe(false);
});
// OPR.0.3.2.21.FR-4(a) — body input resolution kills the
// backtick-corruption class. Three accepted shapes: --body inline,
// --body-file <path>, --body / --body-file - for stdin. Exactly one
// of --body / --body-file is required; mutual exclusion validates.
describe("FR-4(a) — --body-file + stdin support", () => {
it("resolveQueueBody returns inline body when --body is passed", async () => {
const out = await resolveQueueBody({ body: "inline value" });
expect(out).toBe("inline value");
});
it("resolveQueueBody reads from a file path when --body-file is passed", async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "queue-body-"));
const bodyPath = path.join(tmp, "body.txt");
const content = "Multi-line body with `raw backticks` and\nliteral newlines\n— this is the corruption class --body-file kills.";
fs.writeFileSync(bodyPath, content, "utf8");
try {
const out = await resolveQueueBody({ bodyFile: bodyPath });
expect(out).toBe(content);
// Discriminator: the backtick-corruption shell class is bypassed
// entirely because no shell substitution happens on file content.
expect(out).toMatch(/`raw backticks`/);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
it("resolveQueueBody throws 3-part error when both --body and --body-file are passed", async () => {
await expect(resolveQueueBody({ body: "inline", bodyFile: "/tmp/x" })).rejects.toMatchObject({
fact: expect.stringMatching(/mutually exclusive|ambiguous/i),
consequence: expect.stringMatching(/did not run/),
action: expect.stringMatching(/exactly one/),
});
});
it("resolveQueueBody throws 3-part error when neither --body nor --body-file is passed", async () => {
await expect(resolveQueueBody({})).rejects.toMatchObject({
fact: expect.stringMatching(/Neither --body nor --body-file/),
consequence: expect.stringMatching(/did not run/),
action: expect.stringMatching(/--body|--body-file/),
});
});
it("resolveQueueBody throws 3-part error when --body-file path does not exist", async () => {
await expect(resolveQueueBody({ bodyFile: "/tmp/this-path-does-not-exist-fr4a-test.md" })).rejects.toMatchObject({
fact: expect.stringMatching(/does not exist/),
consequence: expect.stringMatching(/did not run/),
action: expect.stringMatching(/Check the path/),
});
});
// OPR.0.3.2.21.FR-4 cleanup (guard non-blocking note on FR-4a CLEAR): a
// directory passed to --body-file used to fall through to fs.readFileSync
// and surface a bare Error("EISDIR: illegal operation on a directory") with
// blank consequence/action. The cleanup commit emits the 3-part shape
// explicitly so the error reads consistently with the other body-resolve
// failure modes.
it("resolveQueueBody throws 3-part error when --body-file path is a directory (not a regular file)", async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "queue-body-isdir-"));
try {
await expect(resolveQueueBody({ bodyFile: tmp })).rejects.toMatchObject({
fact: expect.stringMatching(/not a regular file/),
consequence: expect.stringMatching(/did not run/),
action: expect.stringMatching(/Pass a path to a readable file/),
});
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
it("resolveQueueBody calls the injected stdin reader when --body is -", async () => {
const stdinReader = vi.fn(async () => "from stdin\n");
const out = await resolveQueueBody({ body: "-" }, stdinReader);
expect(out).toBe("from stdin\n");
expect(stdinReader).toHaveBeenCalledTimes(1);
});
it("resolveQueueBody calls the injected stdin reader when --body-file is -", async () => {
const stdinReader = vi.fn(async () => "from stdin file dash\n");
const out = await resolveQueueBody({ bodyFile: "-" }, stdinReader);
expect(out).toBe("from stdin file dash\n");
expect(stdinReader).toHaveBeenCalledTimes(1);
});
it("create --body-file <file-with-backticks> POSTs the file content as body (operator-copy-paste-safe)", async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "queue-create-body-file-"));
const bodyPath = path.join(tmp, "body.txt");
const content = "Per-commit handoff for OPR.X.Y.Z\n\n```bash\nrig queue handoff qitem-1 --to next@rig\n```\n\nDone.";
fs.writeFileSync(bodyPath, content, "utf8");
try {
const { deps, calls } = makeDeps({
routes: { "POST /api/queue/create": { status: 201, data: { qitemId: "qitem-fr4a-1", state: "pending" } } },
});
const program = createProgram({ queueDeps: deps });
program.exitOverride();
await program.parseAsync([
"node", "rig", "queue", "create",
"--source", "alice@rig",
"--destination", "bob@rig",
"--body-file", bodyPath,
"--json",
]);
const create = calls.find((c) => c.path === "/api/queue/create");
expect(create, "expected POST /api/queue/create to fire").toBeDefined();
const body = create!.body as Record<string, unknown>;
expect(body.body).toBe(content);
// Discriminator: the backtick fence survived intact, proving the
// shell-substitution class never touched the content.
expect((body.body as string)).toMatch(/```bash[\s\S]*?```/);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
it("create with both --body and --body-file errors with exit 1 + 3-part error + does NOT contact the daemon", async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "queue-create-conflict-"));
const bodyPath = path.join(tmp, "body.txt");
fs.writeFileSync(bodyPath, "x", "utf8");
try {
const { deps, calls } = makeDeps();
const program = createProgram({ queueDeps: deps });
program.exitOverride();
const prevExit = process.exitCode;
process.exitCode = undefined;
try {
await program.parseAsync([
"node", "rig", "queue", "create",
"--source", "alice@rig",
"--destination", "bob@rig",
"--body", "inline",
"--body-file", bodyPath,
"--json",
]);
expect(process.exitCode).toBe(1);
expect(calls.find((c) => c.path === "/api/queue/create")).toBeUndefined();
} finally {
process.exitCode = prevExit;
}
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
it("create with neither --body nor --body-file errors with exit 1 + does NOT contact the daemon", async () => {
const { deps, calls } = makeDeps();
const program = createProgram({ queueDeps: deps });
program.exitOverride();
const prevExit = process.exitCode;
process.exitCode = undefined;
try {
await program.parseAsync([
"node", "rig", "queue", "create",
"--source", "alice@rig",
"--destination", "bob@rig",
"--json",
]);
expect(process.exitCode).toBe(1);
expect(calls.find((c) => c.path === "/api/queue/create")).toBeUndefined();
} finally {
process.exitCode = prevExit;
}
});
});
// OPR.0.3.2.21.FR-4(b) — --mission / --slice first-class flags
// translate to mission:<id> / slice:<id> tags and compose with --tags.
// This is tag-formalization only — no schema change; the qitem still
// stores tags as a flat list.
describe("FR-4(b) — --mission / --slice first-class flag-formalization", () => {
it("--mission translates to a mission:<id> tag", async () => {
const { deps, calls } = makeDeps({
routes: { "POST /api/queue/create": { status: 201, data: { qitemId: "q-fr4b-1" } } },
});
const program = createProgram({ queueDeps: deps });
program.exitOverride();
await program.parseAsync([
"node", "rig", "queue", "create",
"--source", "alice@rig",
"--destination", "bob@rig",
"--body", "x",
"--mission", "release-0.3.2",
"--json",
]);
const create = calls.find((c) => c.path === "/api/queue/create");
expect((create!.body as { tags: string[] }).tags).toEqual(["mission:release-0.3.2"]);
});
it("--slice translates to a slice:<id> tag", async () => {
const { deps, calls } = makeDeps({
routes: { "POST /api/queue/create": { status: 201, data: { qitemId: "q-fr4b-2" } } },
});
const program = createProgram({ queueDeps: deps });
program.exitOverride();
await program.parseAsync([
"node", "rig", "queue", "create",
"--source", "alice@rig",
"--destination", "bob@rig",
"--body", "x",
"--slice", "21-fr-4-queue-ergonomics",
"--json",
]);
const create = calls.find((c) => c.path === "/api/queue/create");
expect((create!.body as { tags: string[] }).tags).toEqual(["slice:21-fr-4-queue-ergonomics"]);
});
it("--mission + --slice + --tags merges all three sets (mission/slice first, --tags appended) and de-duplicates", async () => {
const { deps, calls } = makeDeps({
routes: { "POST /api/queue/create": { status: 201, data: { qitemId: "q-fr4b-3" } } },
});
const program = createProgram({ queueDeps: deps });
program.exitOverride();
await program.parseAsync([
"node", "rig", "queue", "create",
"--source", "alice@rig",
"--destination", "bob@rig",
"--body", "x",
"--mission", "release-0.3.2",
"--slice", "21-fr-4-queue-ergonomics",
"--tags", "gate:guard,handoff:per-commit",
"--json",
]);
const create = calls.find((c) => c.path === "/api/queue/create");
expect((create!.body as { tags: string[] }).tags).toEqual([
"mission:release-0.3.2",
"slice:21-fr-4-queue-ergonomics",
"gate:guard",
"handoff:per-commit",
]);
});
it("--mission release-0.3.2 + --tags mission:release-0.3.2 de-duplicates the redundant tag (one mission:X kept)", async () => {
const { deps, calls } = makeDeps({
routes: { "POST /api/queue/create": { status: 201, data: { qitemId: "q-fr4b-4" } } },
});
const program = createProgram({ queueDeps: deps });
program.exitOverride();
await program.parseAsync([
"node", "rig", "queue", "create",
"--source", "alice@rig",
"--destination", "bob@rig",
"--body", "x",
"--mission", "release-0.3.2",
"--tags", "mission:release-0.3.2,gate:guard",
"--json",
]);
const create = calls.find((c) => c.path === "/api/queue/create");
const tags = (create!.body as { tags: string[] }).tags;
expect(tags.filter((t) => t === "mission:release-0.3.2")).toHaveLength(1);
expect(tags).toContain("gate:guard");
});
it("no --mission/--slice/--tags → tags is undefined on the wire (legacy behavior preserved)", async () => {
const { deps, calls } = makeDeps({
routes: { "POST /api/queue/create": { status: 201, data: { qitemId: "q-fr4b-5" } } },
});
const program = createProgram({ queueDeps: deps });
program.exitOverride();
await program.parseAsync([
"node", "rig", "queue", "create",
"--source", "alice@rig",
"--destination", "bob@rig",
"--body", "x",
"--json",
]);
const create = calls.find((c) => c.path === "/api/queue/create");
expect((create!.body as { tags?: string[] }).tags).toBeUndefined();
});
});
it("update --state done WITHOUT --closure-reason renders structured hot-potato error and exits non-zero", async () => {
const { deps } = makeDeps({
routes: {
@@ -35,7 +35,11 @@ import type { Migration } from "../migrate.js";
* - reason (TEXT) — required for hold + drop
* - annotation (TEXT) — required for annotate
* - notify_attempted (INTEGER 0|1) — for handoff
* - notify_result (TEXT) — verified | sent-unverified | failed:<reason>
* - notify_result (TEXT) — verified | delivered-ack-pending | failed:<reason>
* (post-OPR.0.3.2.21.FR-4(c) wording: "sent-unverified" was misleading
* because it read as a partial-failure even when delivery was fine;
* "delivered-ack-pending" reads as the normal-and-expected case for
* codex seats whose synchronous ack window expired while busy)
* - audit_notes_json (TEXT) — operator-supplied evidence map
*
* Indexes:
@@ -292,7 +292,7 @@ export class QueueRepository {
* can invoke after its outer transaction commits, completing the
* createWithinTransaction()'s deferred post-commit side effects.
*
* V0.3.1 slice 23 founder-walk-queue-handoff-envelope: the nudge body
* V0.3.1 slice 23 queue-handoff-envelope: the nudge body
* is now wrapped with the same From/To/---/body/---/↩ Reply envelope
* that `rig send` uses. `sourceSession` is the seat that triggered
* the create/handoff so the recipient pane shows where the nudge
@@ -311,8 +311,15 @@ export class QueueRepository {
const text = wrapPaneEnvelope(sourceSession, destinationSession, bareBody);
try {
const res = await this.transport.send(destinationSession, text, { verify: true });
// OPR.0.3.2.21.FR-4(c) — wording rename: the prior literal
// "sent-unverified" read as a failure even in the common case
// (delivery confirmed but the synchronous ack window expired,
// which is normal for codex seats mid-task). The new literal
// "delivered-ack-pending" reads as healthy. The old "verified"
// case is unchanged for backward-compat with any tooling that
// already consumed the positive literal.
const result = res.ok
? (res.verified ? "verified" : "sent-unverified")
? (res.verified ? "verified" : "delivered-ack-pending")
: `failed:${res.error ?? res.reason ?? "unknown"}`;
this.recordNudgeAttempt(qitemId, result);
} catch (err) {
+23 -1
View File
@@ -187,7 +187,29 @@ export function queueRoutes(): Hono {
}
});
// POST /:qitemId/update — general state mutator (incl. done)
// POST /:qitemId/update — general state mutator (incl. done).
//
// OPR.0.3.2.21.FR-4(d-docs) — closure ≠ acceptance.
//
// `state=done` with `closure_reason=handed_off_to` records that the
// source seat has DELIVERED the work to the next stage. It does NOT
// record that the next stage has ACCEPTED the work — that's the next
// stage's verdict on its own qitem (typically a separate close with
// its own closure_reason).
//
// Closure vocabulary:
// - handed_off_to delivered to next stage; acceptance pending
// that stage's verdict on its own qitem
// - blocked_on waiting on a named blocker (closureTarget)
// - denied the source seat refuses the work
// - canceled work no longer needed (no follow-on)
// - no-follow-on completed in place; no further routing
// - escalation routed to a higher-authority seat
//
// The "accepted" state IS NOT a queue state in v0.3.x — the qitem
// model captures delivery + the receiving stage owns acceptance as
// a separate transaction. (FR-4d-state schema change adding a
// distinct "accepted" state is deferred to release-0.3.3.)
app.post("/:qitemId/update", async (c) => {
const qitemId = c.req.param("qitemId");
const body = await c.req.json<{
@@ -343,6 +343,29 @@ describe("QueueRepository", () => {
expect(fresh.state).toBe("pending");
});
// OPR.0.3.2.21.FR-4(c) — wording rename for the delivered-but-ack-expired
// case. Prior literal "sent-unverified" read as a partial-failure even
// when the underlying delivery was fine; "delivered-ack-pending" reads
// as the healthy-and-expected case (codex seats mid-task commonly miss
// the synchronous ack window).
it("ok-but-unverified nudge records lastNudgeResult as 'delivered-ack-pending' (was 'sent-unverified')", async () => {
const stubTransport = {
send: async () => ({ ok: true, verified: false }),
};
const nudgingRepo = new QueueRepository(db, bus, { transport: stubTransport });
const item = await nudgingRepo.create({
sourceSession: "alice@rig",
destinationSession: "bob@rig",
body: "delivered but ack window expired",
});
const fresh = nudgingRepo.getById(item.qitemId)!;
expect(fresh.lastNudgeResult).toBe("delivered-ack-pending");
// Discriminator: the old wording must NOT appear anywhere on
// the freshly-read row (proves the literal was renamed end-to-end,
// not just shadowed).
expect(fresh.lastNudgeResult).not.toBe("sent-unverified");
});
it("handoff nudges new destination by default", async () => {
const sends: Array<{ session: string }> = [];
const stubTransport = {