Skip to content

fix(qso-answerer): D-CALLER-018/016/019 — abort hard-stop, faster re-engage, dblclick cleanup#72

Open
frank001 wants to merge 1 commit into
mainfrom
fix/d-caller-018-abort-hard-stop
Open

fix(qso-answerer): D-CALLER-018/016/019 — abort hard-stop, faster re-engage, dblclick cleanup#72
frank001 wants to merge 1 commit into
mainfrom
fix/d-caller-018-abort-hard-stop

Conversation

@frank001

Copy link
Copy Markdown
Owner

Summary

Implements dev-tasks/2026-07-12-answerer-abort-hard-stop-and-reengage-timing.md in full (D-CALLER-018 critical, D-CALLER-016 high, D-CALLER-019 medium/cleanup).

  • D-CALLER-018 (critical): AbortAsync previously returned immediately whenever _state was already Idle, which meant a pending CQ target (AnswerCqAsync) or jump-in target (EngageAtAsync) armed while Idle could not be cancelled by the operator — it fired regardless, up to ~30s later, no matter how many times Abort was clicked (14 no-op clicks observed in logs/openswfz-20260712T211150Z.log). AbortAsync now unconditionally clears the pending-target/jump-in fields before its Idle guard, matching the same fields SafeAbortToIdleAsync already clears. Per the Captain's stated requirement: "Abort just means stop all and everything... Do not engage TX again unless the operator decides otherwise."
  • D-CALLER-016 (high): raises MaxLateStartSeconds from 1.5s to 2.0s. The 1.5s threshold left ~0.8-1.0s of realistic click budget after decode processing (0.4-0.7s observed), so nearly every manual click missed its window and was deferred a full 30s (phase alternates every 15s, so the next matching-phase window is always two cycles away). 2.0s still leaves a 0.36s hard safety margin against FT8 overrun.
  • D-CALLER-019 (cleanup): removes web/js/main.js's redundant legacy CQ-row dblclick handler (predates D-CALLER-012's engage-decode handler, which already reproduces it exactly plus does abort-first). It always lost the race and produced a guaranteed spurious 409 on every double-click. Carried its stray-selection cleanup (window.getSelection()?.removeAllRanges()) forward into the remaining handler so that behavior isn't lost.

Test plan

  • dotnet build OpenWSFZ.slnx -c Release — 0 errors, 0 warnings
  • Full suite — 1176/1176 passed (baseline was ≥948; two new unit tests added: AC-1 pending-target, AC-2 jump-in)
  • New unit tests confirmed to fail against the pre-fix code (git stash of just QsoAnswererService.cs) and pass against the fix — not just green, meaningfully green
  • Live verification (dev-task §5 mandate — this defect was only caught by reading a real log, not unit tests): qa/d-caller-018-abort-hard-stop-live-verify/live_verify_abort_hardstop.py drives a real isolated daemon over its real HTTP/WebSocket API (no FakeTimeProvider, no mocked IPttController), reproducing the exact production sequence — arm a pending CQ target late, let the guard defer it, hammer Abort x14 while Idle, wait past the next matching-phase window, confirm no re-engagement. PASS with the fix; deliberately reverted the fix and reran — FAIL, reproducing the exact log line ("pending CQ target 'Q9BUG' ... answering at A phase" firing ~29s after the hammer) — proving the script is a real regression guard, not a false PASS. See qa/d-caller-018-abort-hard-stop-live-verify/README.md for the full report index.
  • AC-5 (D-CALLER-019, exactly one outbound POST on CQ-row double-click) verified live in a real headless Chromium browser against the real running daemon: real index.html, synthetic decode WS frame → real unmodified handleDecodes() creates a real <tr>, real browser double-click → exactly one POST /api/v1/tx/engage-decode, zero POST /api/v1/tx/answer-cq, zero console errors. (One-off Playwright driver not committed — repo has no Node/Playwright dependency elsewhere; result recorded in the README above.)

🤖 Generated with Claude Code

…engage, dead-code cleanup

Abort must be a hard stop per the Captain's stated requirement: once clicked, nothing may
re-engage TX until the operator explicitly requests it again. AbortAsync previously no-opped
whenever _state was already Idle, so a pending CQ target or jump-in armed by AnswerCqAsync/
EngageAtAsync (which fire independently of _state while Idle) could not be cancelled — it fired
regardless, up to ~30s later, no matter how many times Abort was clicked (14 no-op clicks
observed in logs/openswfz-20260712T211150Z.log). AbortAsync now unconditionally clears the
pending-target/jump-in fields before its Idle guard.

Also raises MaxLateStartSeconds from 1.5s to 2.0s (D-CALLER-016): the 1.5s threshold left almost
no realistic click budget after decode processing, so nearly every manual engagement missed its
window and was deferred a full 30s. 2.0s still leaves a 0.36s safety margin against FT8 overrun
while catching realistic latency.

D-CALLER-019 removes web/js/main.js's redundant legacy CQ-row dblclick handler (predates
D-CALLER-012's engage-decode handler, which already reproduces it plus does abort-first) — it
always lost the race and produced a guaranteed spurious 409 on every double-click. Carried its
stray-selection cleanup forward into the remaining handler.

Verified live against a real isolated daemon over its real HTTP/WebSocket API (no mocks) —
qa/d-caller-018-abort-hard-stop-live-verify/ — including a deliberate control run with the fix
reverted that reproduces the exact production failure, and a real-browser double-click check
proving D-CALLER-019 leaves exactly one outbound POST. Full suite 1176/1176.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant