* fix(threads): stop and join the prompt cleanup thread, the image worker and the observer (#302, #370)
- PromptTracker: the cleanup thread waits on a stop event instead of
time.sleep; new shutdown(timeout=2) joins it and returns whether it ended.
- ImageGenerationHandler.shutdown(timeout=5) now joins the worker and
returns a bool; new close() is final: schedule() refuses and a sealed
run's history re-check is dropped, so no worker restarts afterwards.
- ImageMonitor.stop_monitoring(timeout=5) closes the handler first, then
stops and joins the watchdog observer within one deadline.
- Every test that builds a real tracker, handler or monitor ends with its
final stop and an assertion, before its folder and databases go.
- #370: the wrapper/session history test drains before dispose(); the
held-write bound test uses 30 s ceilings instead of tight wall clocks.
* test(history): drain the writer before asserting a bounded close it does not test (#370)
* fix(threads): a later stop waits again for a worker or observer an earlier stop left running (#302)
Review findings on #372:
- ImageGenerationHandler keeps the workers a stop signalled that had not
ended; every later shutdown/close joins them again, so a second call no
longer returns True while the first worker still runs.
- ImageMonitor.stop_monitoring keeps an observer or handler it left
running and stops and joins it again on the next call.
- A closed handler takes no awaiting history path, and a re-check that
raced close() is not kept due.
- Tests: second stops of a busy worker and a live observer, the closed
handler's re-check paths, the handle test asserts the worker ended and
stops before it asserts, and a no-new-thread check instead of set
equality.
* test(browser): the System reset restores every settings value, safety_backup included
The frontend coverage shard of #372 failed in workspace-system-tagging:
the review read "No safety backup (the policy is off)". The backups spec
saves safety_backup=false, and /__test__/system/reset restored config.json
but not PromptManagerConfig.SAFETY_BACKUP (nor INFINITE_SCROLL), so a
later spec on the same worker server saw the policy off. The reset now
captures and restores every non-tagging value POST /settings can change.
On a slow Windows runner the restore that the rotation test injects between
the writer's transactions outlived wait_for's 3 s worker deadline. The
subtest failed, and because every subtest shared the process-wide context
budget and left its writer running, the still-held restore cascaded: the
next subtest saw the in-flight 621-byte payload as charged, and the late
restore rotated the epoch under the last subtest's writer.
Each subtest now owns its writer and budget and closes them before the next
starts, and the handoff waits on an event set once the rotation returns,
with the existing BLOCK_SECONDS ceiling, before the short pending wait.
A new test holds a restore between transactions with events and proves the
payload is charged only while in flight, released afterwards, and that the
writer re-probes and accepts new evidence.
The real-clock subtest of the bounded-close test asserted close(0.02)
returned in under 0.2 s; a slow macOS runner took 0.26 s. The failed
subtest then skipped its release and join, so its worker stayed blocked
holding a charge on the process-wide SHARED_CONTEXT_BUDGET, and the next
subtest saw 621 != 0. Not a production race: close(2) only returns True
once the worker has exited and released its leases.
Prove boundedness with events instead: close() must return False while
the in-flight write is still blocked (it has not left its fake write).
Keep the exact upper bound only under the manual clock, where it is
deterministic. Release and join each subtest's worker in a finally so a
failure cannot leak a charge into the next subtest. The commit-gate test
gets the same treatment: submit() and close(0) must return before the
blocked invalidation and commit finish, instead of a 0.1 s bound.