fix(maestro): correct connection view race and approval decisions (#140)
Three fixes from acceptance review: - ConnectionPool.mount cached a connection's kind (terminal/structured) permanently from whatever `info` it saw first. A deep link or restored layout mounts before the catalog snapshot names a session's view, so `info` is undefined and a structured session got an xterm connection forever. The pool now tracks the view each cached connection was built for, subscribes to the catalog itself, and replaces a mistyped connection in place the moment the real view resolves — without downgrading a resolved connection just because a later caller couldn't resolve the session, and without treating a session merely absent from a snapshot (mid-removal) as "Terminal". - The approval card offered all three decisions unconditionally. Per the daemon's decision mapping (session.rs's permission_outcome), a decision with no matching PermissionOptionKind in the request's own options resolves Cancelled silently — so "Allow for session" could look like it worked while the daemon withdrew it. Buttons are now gated on the options actually offered. - Clicking a decision disabled the buttons and never re-enabled them if the POST failed. The decision path is async now: a rejection restores the buttons and surfaces the error only if the approval is still the pending one — a superseded/resolved approval's stale rejection is a no-op, not a wrongly revived control. Regression tests: 12 new (connections.test.ts, structured.test.ts, structuredRender.test.ts). 488 tests total.
This commit is contained in:
+16
-3
@@ -115,13 +115,26 @@ export function createStructuredConnection(
|
||||
};
|
||||
}
|
||||
|
||||
function decide(rowId: string, decision: StructuredApprovalDecision): void {
|
||||
async function decide(rowId: string, decision: StructuredApprovalDecision): Promise<void> {
|
||||
const nonce = state.pending_approval?.nonce;
|
||||
// Already resolved elsewhere between the click and here — the row's
|
||||
// buttons should already be gone, but a stale nonce is a no-op, not an
|
||||
// error (docs/STRUCTURED-VIEW.md: any nonce answers 204).
|
||||
// error (docs/STRUCTURED-VIEW.md: any nonce answers 204). Resolve
|
||||
// quietly: there is nothing for the row to restore.
|
||||
if (!nonce || rowId !== pendingApprovalRowId) return;
|
||||
resolveStructuredApproval(sessionId, nonce, decision).catch((e: unknown) => showError(String(e)));
|
||||
try {
|
||||
await resolveStructuredApproval(sessionId, nonce, decision);
|
||||
} catch (e: unknown) {
|
||||
// Only propagate the failure — so the row re-enables its buttons — if
|
||||
// this is still the pending approval. If it moved on while the
|
||||
// request was in flight (superseded, cancelled, answered elsewhere),
|
||||
// the state-driven re-render already replaced the row; reviving
|
||||
// buttons on it would be reviving a decision that no longer applies.
|
||||
if (rowId === pendingApprovalRowId) {
|
||||
showError(String(e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mountRow(row: TranscriptRow): void {
|
||||
|
||||
Reference in New Issue
Block a user