mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-09 03:39:51 -10:00
fix(slice-15): adopted-rig hasAssignedWork resolves both raw + canonical destination forms (qa BLOCKING-A2)
QA baseline-deep-dogfood BLOCKING-A2 (qitem-20260518063900-85745917):
adopted/live-session rigs do NOT surface assigned queue work. Root
cause: hasAssignedWork looks up pending qitems by
entry.canonicalSessionName ONLY.
For managed seats, canonicalSessionName is set by
deriveCanonicalSessionName at materialize time and equals the
canonical `{pod}-{member}@{rig}` form — the same form operators
address via `rig queue create --destination`. The single-key lookup
works.
For adopted seats, canonicalSessionName is the RAW tmux session name
(whatever the adopter chose, e.g., `my-existing-claude`). Operators
address adopted seats via the canonical `{pod}-{member}@{rig}` form
(derived from logicalId + rigName), so the single-key lookup misses
and adopted seats appear to have nothing assigned even when qitems
are pending.
Fix (tight, NO broad identity refactor per orch-lead's caution):
- New private countPendingForEntry resolves pending count by BOTH
forms via a deduplicating Set:
1. entry.canonicalSessionName (covers managed + adopted-by-raw)
2. derived `{pod}-{member}@{rig}` from logicalId + rigName (covers
adopted-by-canonical)
- The dot in logicalId (pod-aware `pod.member`) is split at the FIRST
dot to produce pod + member, matching deriveCanonicalSessionName's
format.
- Set-based dedup means managed seats (where canonicalSessionName ===
derived canonical) never double-count.
Discriminators added (test-driven; 3 of the 6 NEW tests failed PRE-FIX
to prove the regression class, all 13 tests PASS post-fix):
- adopted entry + queue dest=canonical form → hasAssignedWork true
- adopted entry + queue dest=raw tmux name still matches (back-compat)
- managed entry → unchanged behavior
- no double-count when both forms equal
- separate dests at raw + canonical sum correctly (3 items = 3)
- logical IDs with dots normalized via deriveCanonicalSessionName
Test totals: 13/13 attachTerminalActivityAndWork; 45/45 adjacent
slice-15 (graph-projection-pl019 + ps-projection + seat-activity-service).
Daemon tsc EXIT=0.
BRANCH: slice-15-fix-adopted-queue-identity off main cfec5508. LOCAL,
NO push.
This commit is contained in:
1 parent
cfec550801
commit
1aa0c5237e
2 files changed
+150
-3
No files matched your search
@@ -8,6 +8,7 @@ import type { TmuxAdapter } from "../adapters/tmux.js";
|
||||
import { probeSessionActivity } from "./session-transport.js";
|
||||
import { findLatestUsableSnapshot } from "./rig-repository.js";
|
||||
import { resolveNodeWorkspace } from "./workspace/workspace-resolver.js";
|
||||
import { deriveCanonicalSessionName } from "./session-name.js";
|
||||
|
||||
// -- Row types for SQL results --
|
||||
|
||||
@@ -656,9 +657,7 @@ export function attachTerminalActivityAndWork(
|
||||
const obs = seatActivity.getSeatActivity(entry.canonicalSessionName);
|
||||
terminalActive = obs ? obs.isActiveWithinWindow : null;
|
||||
}
|
||||
const pendingCount = entry.canonicalSessionName
|
||||
? (pendingByDest.get(entry.canonicalSessionName) ?? 0)
|
||||
: 0;
|
||||
const pendingCount = countPendingForEntry(entry, pendingByDest);
|
||||
return {
|
||||
...entry,
|
||||
terminalActive,
|
||||
@@ -668,6 +667,61 @@ export function attachTerminalActivityAndWork(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* QA baseline-deep-dogfood BLOCKING-A2 (qitem-20260518063900-85745917):
|
||||
* adopted/live-session rigs do NOT surface assigned queue work because
|
||||
* the prior lookup matched destination_session ONLY against
|
||||
* canonicalSessionName.
|
||||
*
|
||||
* For MANAGED seats, canonicalSessionName equals the canonical form
|
||||
* `{pod}-{member}@{rig}` (set by deriveCanonicalSessionName at
|
||||
* materialize time) and queue operators address them by that form, so
|
||||
* the single-key lookup works.
|
||||
*
|
||||
* For ADOPTED seats, canonicalSessionName is the RAW tmux session
|
||||
* name (whatever the adopter chose, e.g., `my-existing-claude`).
|
||||
* Operators address adopted seats by the canonical form (logical
|
||||
* `{pod}-{member}@{rig}`) through `rig queue create --destination`,
|
||||
* so the single-key lookup misses.
|
||||
*
|
||||
* Resolve via BOTH forms:
|
||||
* 1. entry.canonicalSessionName (covers managed + adopted-by-raw)
|
||||
* 2. derived `{pod}-{member}@{rig}` from logicalId + rigName
|
||||
* (covers adopted-by-canonical)
|
||||
*
|
||||
* The logicalId is the pod-aware `pod.member` form (dot-separated).
|
||||
* The canonical session form replaces the dot with a dash to match the
|
||||
* convention from deriveCanonicalSessionName (so `redo.driver-2` in
|
||||
* rig `openrig-velocity` becomes `redo-driver-2@openrig-velocity`).
|
||||
*
|
||||
* Sums distinct destination_session keys to avoid double-counting
|
||||
* when both forms are identical (managed seats whose
|
||||
* canonicalSessionName already equals the derived canonical form).
|
||||
*/
|
||||
function countPendingForEntry(
|
||||
entry: NodeInventoryEntry,
|
||||
pendingByDest: Map<string, number>,
|
||||
): number {
|
||||
const keys = new Set<string>();
|
||||
if (entry.canonicalSessionName) keys.add(entry.canonicalSessionName);
|
||||
const derived = deriveCanonicalFromEntry(entry);
|
||||
if (derived) keys.add(derived);
|
||||
let count = 0;
|
||||
for (const key of keys) {
|
||||
count += pendingByDest.get(key) ?? 0;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function deriveCanonicalFromEntry(entry: NodeInventoryEntry): string | null {
|
||||
if (!entry.rigName || !entry.logicalId) return null;
|
||||
const dotIdx = entry.logicalId.indexOf(".");
|
||||
if (dotIdx <= 0 || dotIdx === entry.logicalId.length - 1) return null;
|
||||
const pod = entry.logicalId.slice(0, dotIdx);
|
||||
const member = entry.logicalId.slice(dotIdx + 1);
|
||||
return deriveCanonicalSessionName(pod, member, entry.rigName);
|
||||
}
|
||||
|
||||
function readPendingWorkBySession(db: Database.Database): Map<string, number> {
|
||||
const rows = db.prepare(`
|
||||
SELECT destination_session, COUNT(*) as c
|
||||
|
||||
@@ -161,6 +161,99 @@ describe("attachTerminalActivityAndWork — slice 15 per-node enrichment", () =>
|
||||
expect(entry!.pendingWorkCount).toBe(3);
|
||||
});
|
||||
|
||||
// QA baseline-deep-dogfood BLOCKING-A2 (qitem-20260518063900-85745917):
|
||||
// adopted/live-session rigs do not surface assigned queue work because
|
||||
// hasAssignedWork only matches destination_session against
|
||||
// canonicalSessionName. For ADOPTED seats canonicalSessionName is the
|
||||
// raw tmux session name (e.g., `my-existing-claude`); operators
|
||||
// address adopted seats via the canonical `{pod}-{member}@{rig}` form
|
||||
// through `rig queue create --destination <pod>-<member>@<rig>`.
|
||||
// The match therefore fails for adopted, succeeds for managed (where
|
||||
// canonicalSessionName is already the canonical form).
|
||||
//
|
||||
// Fix: resolve pending work by BOTH forms — entry.canonicalSessionName
|
||||
// (covers managed + adopted users who queue by raw tmux name) AND the
|
||||
// derived `{pod}-{member}@{rig}` form (covers adopted users who queue
|
||||
// by canonical id). Discriminators below pin both directions.
|
||||
|
||||
it("BLOCKING-A2: adopted-style entry (canonicalSessionName=raw tmux name) — queue dest=canonical form matches → hasAssignedWork=true", () => {
|
||||
const rig = seedRig(db, "my-rig");
|
||||
const n = seedNode(db, rig, "default.dev");
|
||||
// Adopted node: session name is the raw tmux session, NOT the
|
||||
// canonical {pod}-{member}@{rig} form.
|
||||
seedSession(db, n, "raw-tmux-name", "running");
|
||||
// Operator queues by the canonical form (`rig queue create
|
||||
// --destination default-dev@my-rig`).
|
||||
seedPendingQitem(db, "default-dev@my-rig");
|
||||
|
||||
const [entry] = attachTerminalActivityAndWork(getNodeInventory(db, rig), { db });
|
||||
expect(entry!.canonicalSessionName).toBe("raw-tmux-name");
|
||||
expect(entry!.hasAssignedWork).toBe(true);
|
||||
expect(entry!.pendingWorkCount).toBe(1);
|
||||
});
|
||||
|
||||
it("BLOCKING-A2: adopted-style entry — queue dest=raw tmux name still matches (back-compat path)", () => {
|
||||
const rig = seedRig(db, "my-rig");
|
||||
const n = seedNode(db, rig, "default.dev");
|
||||
seedSession(db, n, "raw-tmux-name", "running");
|
||||
seedPendingQitem(db, "raw-tmux-name");
|
||||
|
||||
const [entry] = attachTerminalActivityAndWork(getNodeInventory(db, rig), { db });
|
||||
expect(entry!.hasAssignedWork).toBe(true);
|
||||
expect(entry!.pendingWorkCount).toBe(1);
|
||||
});
|
||||
|
||||
it("BLOCKING-A2: managed-style entry (canonicalSessionName IS canonical form) — unchanged behavior", () => {
|
||||
const rig = seedRig(db, "my-rig");
|
||||
const n = seedNode(db, rig, "default.dev");
|
||||
seedSession(db, n, "default-dev@my-rig", "running");
|
||||
seedPendingQitem(db, "default-dev@my-rig");
|
||||
|
||||
const [entry] = attachTerminalActivityAndWork(getNodeInventory(db, rig), { db });
|
||||
expect(entry!.hasAssignedWork).toBe(true);
|
||||
expect(entry!.pendingWorkCount).toBe(1);
|
||||
});
|
||||
|
||||
it("BLOCKING-A2: no double-count when canonicalSessionName already equals derived canonical form", () => {
|
||||
const rig = seedRig(db, "my-rig");
|
||||
const n = seedNode(db, rig, "default.dev");
|
||||
seedSession(db, n, "default-dev@my-rig", "running");
|
||||
// ONE pending qitem at the canonical destination — the entry's
|
||||
// canonicalSessionName equals the derived canonical form, so
|
||||
// looking up by both must not double-count.
|
||||
seedPendingQitem(db, "default-dev@my-rig");
|
||||
|
||||
const [entry] = attachTerminalActivityAndWork(getNodeInventory(db, rig), { db });
|
||||
expect(entry!.pendingWorkCount).toBe(1);
|
||||
});
|
||||
|
||||
it("BLOCKING-A2: separate queue dests at raw + canonical forms sum correctly (each item counted once)", () => {
|
||||
const rig = seedRig(db, "my-rig");
|
||||
const n = seedNode(db, rig, "default.dev");
|
||||
seedSession(db, n, "raw-tmux-name", "running");
|
||||
seedPendingQitem(db, "raw-tmux-name");
|
||||
seedPendingQitem(db, "default-dev@my-rig");
|
||||
seedPendingQitem(db, "default-dev@my-rig");
|
||||
|
||||
const [entry] = attachTerminalActivityAndWork(getNodeInventory(db, rig), { db });
|
||||
expect(entry!.hasAssignedWork).toBe(true);
|
||||
// Total = 1 (raw) + 2 (canonical) = 3
|
||||
expect(entry!.pendingWorkCount).toBe(3);
|
||||
});
|
||||
|
||||
it("BLOCKING-A2: logical IDs with dots are normalized to dashes for canonical form (matches deriveCanonicalSessionName)", () => {
|
||||
const rig = seedRig(db, "openrig-velocity");
|
||||
const n = seedNode(db, rig, "redo.driver-2");
|
||||
seedSession(db, n, "raw-name-X", "running");
|
||||
// The convention is `{pod}-{member}@{rig}` so logicalId
|
||||
// "redo.driver-2" becomes "redo-driver-2".
|
||||
seedPendingQitem(db, "redo-driver-2@openrig-velocity");
|
||||
|
||||
const [entry] = attachTerminalActivityAndWork(getNodeInventory(db, rig), { db });
|
||||
expect(entry!.hasAssignedWork).toBe(true);
|
||||
expect(entry!.pendingWorkCount).toBe(1);
|
||||
});
|
||||
|
||||
it("non-pending qitems do NOT count toward pendingWorkCount", () => {
|
||||
const rig = seedRig(db, "states");
|
||||
const n = seedNode(db, rig, "dev");
|
||||
|
||||
Reference in new issue
Block a user