From 0ee8d3726b8184648b8f5e003e527e6409c37f76 Mon Sep 17 00:00:00 2001 From: Midhat Nashar <35053211+midhatn@users.noreply.github.com> Date: Mon, 9 Nov 2026 06:16:53 +0300 Subject: [PATCH 1/5] docs: reconcile integrated development and upstream delivery --- CHANGELOG.md | 14 +++++++++ HANDOFF.md | 61 ++++++++++++++++++++++++++++----------- PLAN.md | 50 ++++++++++++++++++++++++-------- docs/evidence.md | 28 ++++++++++++++++++ docs/reboot-budget.md | 13 ++++++--- docs/upstream-delivery.md | 31 ++++++++++++++++++-- docs/upstream-prs.md | 24 +++++++++++++++ 7 files changed, 186 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9f470f..7d2d634 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## Unreleased — development after 0.4.0a1 + +These changes belong to the development source. The published `0.4.0a1` tag and downloadable packages retain their original capabilities and validation scope. + +- Add **Open saved task** after a safe Stop, including tasks created later through the running controller. Accepted installed IQ3_S trials preserve the original task, confirmed reads, conversation, deadline and consumed work across application close/reopen in the same Windows boot. A completed task can be viewed without starting another worker or model, or writing its result again. See [saved-task reopening](docs/saved-task-reopening.md). +- Add a bounded **C++17 compile-and-test recipe** that an opted-in Hermes task can request through the existing resource handoff. Reuse CMake/CTest, retained process ownership and durable tool feedback. An accepted IQ3_S foreground trial handed resources to the compiler and returned its checked result to the same task. See [the compiler recipe and evidence](docs/compiler-tool-recipe.md). +- Add a bounded **GPU video recipe** using pinned FFmpeg/FFprobe and NVENC. An accepted IQ3_S foreground trial parked inference, encoded and checked one silent 90-frame test clip, restored the model and completed the task from saved tool feedback. This is a fixed test-clip capability; H3, audio synchronization and the full autonomous media workflow remain unqualified. See [the video recipe and evidence](docs/video-tool-recipe.md). +- Recover missing tool feedback through the existing explicit reconciliation path when the completed result is known. Extend this to a pending journal entry when the live tool host retains the exact terminal result. Installed Hermes SDK trials use one actual video operation with scripted provider replies and resource observations; they preserve the task and deadline without replaying the operation. Unknown outcomes remain unresolved. See [completed-result recovery](docs/video-tool-recipe.md#completed-result-feedback-repair) and the [pending-effect receipt](provenance/pending-tool-recovery/installed-worker-loss-2026-11-02.json). +- Add **automatic desktop worker replacement** for that narrowly defined pending-result boundary. In the accepted real IQ3_S trial, foreground activity parked inference, one video operation finished, and the first worker exited before recording completion. The live host reconciled the saved result and launched one replacement automatically; the same task completed within its original deadline after two total model requests. Independent closed review passed 49 checks. The separate shutdown observer failed and remains an open diagnostic; this is not active-tool interruption, whole-host loss or reboot recovery. See [automatic recovery](docs/video-tool-recipe.md#automatic-desktop-recovery) and its [physical receipt](provenance/automatic-tool-recovery/physical-foreground-recovery-2026-11-08.json). +- Add the **durable reboot-budget foundation**: a fixed expiry and saved remaining-time cap prevent a restart from granting a new execution allowance. Source, package, installation and scripted SDK checks passed. The first actual Windows restart preserved saved work but the original allowance expired before continuation; execution was correctly refused. Successful computer-reboot continuation remains open. See [budget behavior](docs/reboot-budget.md) and the [retained expired attempt](provenance/installed-reopen/reboot-budget/physical-restart-expired-2026-11-02.json). +- Add browser-video observation and comparison tooling, retain unsuccessful measurement attempts and correct continuation-test cleanup. The attempted comparison did not establish desktop/video performance because its observation conditions differed; no broad coexistence or speed claim follows from it. + +The automatic-recovery source passed the complete **1,595-test local core suite** and all **16 additional workflow commands**, with the earlier failed test-double attempt retained. Source checks, installed SDK fixtures and physical IQ3_S evidence remain separately identified in the [evidence ledger](docs/evidence.md) and [local validation record](provenance/automatic-tool-recovery/local-validation-2026-11-08.json). These results do not complete M3, M4, M5, daily-use qualification or the full Adaptive-Hermes-Media-Autonomous Test. + ## 0.4.0a1 — supported Windows reference alpha (2026-11-01) This prerelease packages the supported Windows/IQ3_S/Hermes reference, ordinary diff --git a/HANDOFF.md b/HANDOFF.md index 3c5a9f2..5e26a83 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -5,7 +5,7 @@ Read [AGENTS.md](AGENTS.md) first. The repository, its retained evidence and cur Git state are authoritative; no previous conversation or particular AI provider is required to understand the intended work. -**Snapshot date: 2 November 2026.** Recheck branch heads, PRs, task ownership and +**Snapshot date: 9 November 2026.** Recheck branch heads, PRs, task ownership and the actual machine before acting. This file does not reserve hardware or assert that a previously recorded process is still running. @@ -40,7 +40,18 @@ citations and unsuccessful evidence. The full milestone sequence is in ## Current continuation state -**Active isolated M4 increment:** automatic desktop recovery of a pending +**Integrated development source — 9 November 2026:** PRs #34–#39 merged in +dependency order into `main` at `f362ed0ed8c0f9f6552bd71c9c3ba9e8a847aa3a`. +Its tree `7d17387075e4ac11be05384c5c1b109eaa76f149` exactly matches reviewed +automatic-recovery head `08053a00d92c065b88bda3ce14f80a7cfe078f0f`. +This integrates the bounded compiler/video recipes, known-result reconciliation, +automatic worker replacement and durable reboot-budget foundation. Saved-task +application reopening was already on main. The [Unreleased notes](CHANGELOG.md#unreleased--development-after-040a1) +describe these development capabilities; published `0.4.0a1` artifacts retain +their original scope. [PLAN.md](PLAN.md) is the authoritative goal, milestone +order and next-work plan. M3–M5 and daily-use qualification remain incomplete. + +**Latest accepted M4 increment:** automatic desktop recovery of a pending compiler/video operation after worker loss. `DesktopComposition` now detects a non-zero worker exit with one pending effect whose exact terminal result is held by the live tool host, exports `CompilerHost.terminal_receipt`, runs the @@ -51,10 +62,24 @@ and all 16 remaining workflow commands after a tests-only correction (the failed first run is retained). The [installed SDK trial](provenance/automatic-tool-recovery/installed-worker-loss-2026-11-08.json) passed 21 closed-review checks, and the [physical IQ3_S foreground trial](provenance/automatic-tool-recovery/physical-foreground-recovery-2026-11-08.json) passed 49 closed-audit checks: Explorer F5 parked inference, NVENC ran, the -first worker exited with code 86, and the host recovered automatically. Draft -[PR #39](https://github.com/midhatn/adaptive-inference/pull/39) is stacked on -[PR #38](https://github.com/midhatn/adaptive-inference/pull/38). Next: active-tool -interruption, host loss, observer shutdown diagnosis and M5 restart/reboot. +first worker exited with code 86, and the host recovered automatically. The +separate observer failed during shutdown; the accepted task/cleanup audit does +not erase that failure. Recovery requires the live host's exact completed result +and does not qualify interruption of an active tool or loss of that host. + +**Next bounded work:** diagnose the retained observer shutdown failure before +relying on it for a fresh physical acceptance, then follow M4's active-tool +interruption gate. Reuse existing cancellation and ownership mechanisms and +research any missing behavior before implementation. Whole-host loss, successful +OS-reboot continuation and broad desktop/media qualification remain open under +the roadmap. Preserve unchanged IQ3_S quality and the closed accepted/failed +cases; a new experiment needs a fresh case, not a renewed historical budget. + +## Preceding evidence and integration history + +The following dated sources explain how the integrated result was reached. +Their historical tests and installation identities remain separate; they are +not instructions to replay accepted cases or rebuild the released alpha. **Preceding increment (PR #38):** recover a pending compiler/video operation when the tool host has retained its exact terminal result but the worker disappeared @@ -75,10 +100,11 @@ reconciliation to repair both the effect journal and missing native feedback, and completed the same task with a replacement worker within its original 900-second deadline. Provider replies and resource observations were scripted. Independent closed review passed 99 checks with 74 original case files unchanged. Automatic -desktop recovery and unchanged-IQ3_S recovery remain separate gates. +desktop recovery and unchanged-IQ3_S recovery were separate gates for this +increment; the later bounded result above now supplies that evidence. The preceding [completed-feedback increment](https://github.com/midhatn/adaptive-inference/pull/37) -is published in draft at `93eba3ce4eee9609283f360fb7ca51b6c136e8bb` and merged +was published in draft at `93eba3ce4eee9609283f360fb7ca51b6c136e8bb` and merged with the validated video fixture correction at `3a07a7776ed41fc811aa92ef2f8ee0b9639d13f9`, stacked on [the video recipe](https://github.com/midhatn/adaptive-inference/pull/36). Both original hosted workflows at `93eba3c`, `e62a291`, and merged ancestor `3a07a77` @@ -93,7 +119,8 @@ one actual video effect, ordinary reconciliation restored its missing feedback, and a replacement completed the same task within its original deadline. Provider replies and resource observations were scripted. Independent closed review passed 99 checks with 74 original case files unchanged. Unchanged-IQ3_S recovery and automatic desktop -recovery remain separate gates. The first trial stopped on a private checker +recovery were separate gates for that increment, subsequently covered at the +narrow boundary above. The first trial stopped on a private checker projection error and remains recorded as failed. The [safe-restart gate](provenance/video-tool/installed-worker-restart-2026-11-02.json) preserved one completed video operation across two workers in the same task and @@ -107,11 +134,11 @@ Windows Restart preserved saved work but expired before continuation; the [attempt record](provenance/installed-reopen/reboot-budget/physical-restart-expired-2026-11-02.json) retains that failure and the separate observer timeouts. The installed reader refused the expired task before another app/worker launch. Successful reboot -continuation remains open and PR #34 stays draft. Never replay or renew this +continuation remains open after integration of PR #34's source. Never replay or renew this consumed attempt. Its documentation update at `f94e11b` also passed its original 14 hosted jobs and 12 distribution checks, independently verified on attempt 1; -PR #34 records both runs and remains draft. Keep the actual installation fixed while developing -compiler support. The inert +PR #34 records both runs. That source integration does not change the fixed +installation or qualify successful physical reboot. The inert [M3 focus-switch rehearsal](provenance/video-coexistence/inert-focus-switch-2026-11-01.json) passed at the earlier installed source with no inference or playback measurement. Its task is closed. Video-surface attribution and a complete measured comparison @@ -243,7 +270,7 @@ checks and all 1,352 local tests. Exact-head package verification passed for 1,233 source files/62 runtime modules; all committed bytes matched its input. The original post-merge run is tracked separately from these completed gates. -**Current source increment:** later tasks created through the open controller +**Earlier source increment:** later tasks created through the open controller now receive the same recovery context and host lock, and appear in the bounded saved-task selector. Its 49 affected component checks and 75 source-preservation checks passed; the predecessor regression failed at the missing context as @@ -317,15 +344,15 @@ daily-use qualification remain open. | Item | Dated state and verification boundary | | --- | --- | -| Stable main starting point | Commit `8396676dcc55ffbbf09e0ff640db9238519a07ea`, including merged PR #31/#32 and their earlier prerequisites. Verify current `main`; this is a dated anchor, not a claim that it remains HEAD. | +| Integrated main starting point | Commit `f362ed0ed8c0f9f6552bd71c9c3ba9e8a847aa3a`, including PRs #34–#39, has the exact reviewed `08053a0` tree. Verify current `main`; this is the 9 November anchor, not a claim that it will remain HEAD. | | Released reference and active work | [v0.4.0a1](https://github.com/midhatn/adaptive-inference/releases/tag/v0.4.0a1) is published at tested source `57f673f8e934e275bc2bd51e85d434dc7025bc9a`; PR #28's merged tree matches it exactly. PR #29's measurement preparation merged after its exact-head checks. The first adaptive diagnostic and following control are closed; their scoped results and unmet comparison gates are recorded in the [evidence ledger](docs/evidence.md#first-adaptive-video-diagnostic). | -| Installed source used for the latest experiments | Later-task M5 used `b35b631aca9ec37da9955903b87463201a6b3630`; initial-task M5 used `6ac601546daf32d96e38b6689a5818e4cc8dafa8`; the earlier M3 video sequence used `7306a53085ddc31d53d2ba50e91975c80cfc0014`. All preserved IQ3_S and quality. Source, package, actual-SDK and physical evidence remain separate levels. | -| Current work | The isolated video increment passed local/package/install, installed SDK and real IQ3_S foreground/task gates. Its second physical task passed 47 primary checks, 34 independent functional checks and 17 separate onset-review checks; the first missed its foreground action. Publish this as a separate increment stacked on compiler PR #35, then qualify useful-tool interruption/recovery. PR #34 remains draft after its expired physical restart attempt. Both compiler and video tasks remain closed; observer shutdown and earlier failures remain preserved. | +| Installed source and evidence | The latest automatic-recovery [physical receipt](provenance/automatic-tool-recovery/physical-foreground-recovery-2026-11-08.json) binds its actual installation. Earlier later-task M5 used `b35b631aca9ec37da9955903b87463201a6b3630`; initial-task M5 used `6ac601546daf32d96e38b6689a5818e4cc8dafa8`; M3 video used `7306a53085ddc31d53d2ba50e91975c80cfc0014`. All preserved IQ3_S and quality. A source merge does not update those installations. | +| Current work | Compiler/video handoffs, known-result reconciliation and automatic desktop worker recovery are integrated. The latest physical trial passed 49 closed-audit checks at the completed-tool/surviving-host boundary. Observer shutdown remains unresolved; active-tool interruption and whole-host loss remain open. PR #34's budget source is integrated, but its expired physical restart attempt does not qualify successful reboot continuation. All consumed trials stay closed. | | Preserved M5 same-boot result | The installed same-boot application restart preserved one input read, the task/native session and original deadline; three calls total completed the correct 23-unit answer with one physical write. A third launch viewed the result without additional calls or changes to the eight canonical files. All three app parents joined normally; bounded observer shutdown errors remain retained. See the source-bound M5 record above. | | Preserved M2 recovery result | Case17 passed both original audits: 99 primary recovery/handover checks plus the foreground/lineage supplement. Its correct inventory result survived safe expiry and one Continue; a separate welcome task completed before one normal Stop. Six calls produced two physical writes. Welcome has format-only acceptance. Full M2 is broader than that run. | | Preserved limits and failures | Original quality, 900-second execution budgets, 2,700-second outer bound and task accounting remain unchanged. Cases14/15/16 keep their original failed outcomes. Case17 retains the rejected file-picker input and 30-second operating-target misses; neither repeated a task control nor violated the strict foreground gate. | | Foreground evidence | Case17 retained 25 real document actions, 24 wholly preseal, maximum adjacent interval 54.680872 seconds and preseal tail 37.453106 seconds. The original supplement passed onset, continuous hold and saved-read lineage; the postseal action is excluded from coverage. This is not a desktop-performance or GPU-preemption measurement. | -| Next experiment | Qualify installed useful-tool interruption and worker recovery on the existing compiler/video journal and owner, preserving completed effects and feedback. Do not repeat consumed tasks. A future M5 attempt needs fresh identity, prompt postboot return and unchanged allowance. M3's next bounded diagnostic remains video-surface attribution without inference. | +| Next bounded work | Diagnose the retained observer shutdown failure first, then qualify active-tool interruption using the existing compiler/video journal and owner, preserving completed effects and feedback. Whole-host loss remains separate. A future M5 attempt needs fresh identity, prompt postboot return and unchanged allowance. M3's bounded diagnostic remains video-surface attribution without inference. Follow PLAN.md before starting any fresh case. | | Latest release validation | Corrected source `57f673f` passed all 14 original hosted jobs, final archive/runtime verification and merge-tree verification. All five uploaded assets and the tag were verified at publication. The 48-check watchdog and 21-check refusal corrections retain their own local scope; earlier failed runs remain failed. | | M3 publication | [PR #29](https://github.com/midhatn/adaptive-inference/pull/29) merged at `f6c0da1` with the tested `3136e35` tree. Its original 14 head jobs and seven post-merge jobs passed, including portable archive checks. Earlier archive/fixture failures remain retained; these preparation checks do not requalify the installed runtime. | | Diagnostic publication and test correction | [PR #30](https://github.com/midhatn/adaptive-inference/pull/30) merged at `c348398` with the tested `72ac717` tree after 14 head jobs and seven post-merge jobs passed. Its original `fd4d483` pair retains 13 passes and one Windows SQLite concurrency-fixture failure. The [bounded correction](docs/evidence.md#sqlite-concurrency-fixture-ownership) keeps test threads in a retained child and joins before cleanup; 18 focused checks passed. Runtime, models and physical records are unchanged. | diff --git a/PLAN.md b/PLAN.md index a5f67f5..afc0403 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,6 +1,6 @@ # Adaptive Inference roadmap -**Updated: 2 November 2026. Status: living plan, with evidence-based completion gates.** +**Updated: 9 November 2026. Status: living plan, with evidence-based completion gates.** ## Destination @@ -32,11 +32,13 @@ A roadmap entry is not a qualification receipt. | Area | Position at this revision | | --- | --- | -| Released foundation | Version 0.3.0 includes the portable gateway, normal/background queue, measured execution-cost feedback, durable task foundation, source preservation and scoped integration recipes. | +| Released foundation | Version 0.4.0a1 is the supported Windows reference alpha, including task preparation and desktop setup on the portable gateway, durable task and source-preservation foundation. Later development changes do not alter its published assets. | | Real task evidence | A recorded IQ3_S Hermes parent/child file workflow completed with independently checked artifacts. It does not establish foreground handoff or reboot recovery. | | Native foreground and tool work | One complete source-bound Windows/IQ3_S workflow is accepted: real foreground interaction, verified release, checked GPU encode, restoration and clean shutdown. | | Resource-aware agent waiting | Immediate admission and the pinned Hermes recovery adapter completed the same accepted workflow, with durable waiting following foreground interruption and one same-boot worker restart. | | Desktop protection observation | A later qualification withheld model startup while a foreground game was active. That observation is not a gaming-performance benchmark or a completed qualification. | +| Integrated M4 development | Bounded IQ3_S compiler/video foreground handoffs and automatic worker recovery with an exact completed result retained by the surviving tool host are accepted at their recorded scopes. The independent shutdown observer remains unresolved. | +| Restart development | Same-boot application close/reopen is accepted for the recorded tasks. The reboot-budget foundation is integrated, but the first actual reboot attempt expired before continuation; successful OS-reboot recovery remains open. | | Full always-on experience | Persistent everyday operation, broad foreground coverage, physical reboot recovery and native qualification of other engines remain development work. | The [evidence ledger](docs/evidence.md) and its source-bound receipts define @@ -46,6 +48,29 @@ interrupted runs will remain identified as such. ## Milestones and dependency order +**Current continuation — 9 November 2026:** PRs #34–#39 are merged in dependency +order at `f362ed0ed8c0f9f6552bd71c9c3ba9e8a847aa3a`. Its tree +`7d17387075e4ac11be05384c5c1b109eaa76f149` matches reviewed development head +`08053a00d92c065b88bda3ce14f80a7cfe078f0f`. The [Unreleased notes](CHANGELOG.md#unreleased--development-after-040a1) +describe this source increment; the published version remains `0.4.0a1`. +The latest automatic-recovery source passed 1,595 local core tests and all +16 remaining workflow commands. Its real IQ3_S foreground/worker-loss trial +passed 49 closed-audit checks at the exact-known-result boundary described in +the [evidence ledger](docs/evidence.md#physical-iq3_s-foreground-recovery). +That result does not complete M4 or qualify successful computer-reboot recovery. + +**Next bounded work:** diagnose the retained observer shutdown failure using its +closed evidence and current primary documentation, then continue M4's active-tool +interruption gate through the existing owned-process and handoff mechanisms. +Preserve completed effects, original task/budget and unchanged IQ3_S quality. +Whole-host loss and the M5 restart/reboot sequence remain separate gates. The +expired reboot trial stays closed; successful continuation needs a fresh, +separately coordinated case. Keep this main workflow ahead of optional ports or +optimization. Do not replay accepted trials merely because their source merged. + +The dated delivery paragraphs below retain their original evidence and sequence; +the current milestone table and continuation above determine the next work. + **Reference alpha delivered — 1 November 2026:** [v0.4.0a1](https://github.com/midhatn/adaptive-inference/releases/tag/v0.4.0a1) is published from `57f673f8e934e275bc2bd51e85d434dc7025bc9a`, after all 14 original hosted jobs, final source/wheel verification and actual merge-tree @@ -202,13 +227,13 @@ checker/reporting head `f049342060b3ae8d04f237bf5632d2b9f459869d` passed all 14 hosted jobs on their original attempts. Source/CI, installed physical acceptance and public reproduction retain their separate evidence boundaries. -| ID | Deliverable | Depends on | Initial status | +| ID | Deliverable | Depends on | Current status | | --- | --- | --- | --- | | M1 | One complete corrected handoff and agent-restart workflow | Existing foundation | Accepted for the bounded Windows/Strata/Hermes IQ3_S composition | -| M2 | A practical persistent controller with durable waiting and clear user intent | M1 for live integration acceptance | Active: bounded foreground/continuation/two-task run passed both audits; public reproduction, task preparation and setup/launcher startup checks available; narrow reference alpha delivery is next | +| M2 | A practical persistent controller with durable waiting and clear user intent | M1 for live integration acceptance | Active: bounded foreground/continuation/two-task run passed both audits; public reproduction and task preparation/setup are available; reference alpha delivered; persistent everyday use remains open | | M3 | Measured coexistence with real desktop applications | M2 for broad qualification; a bounded control-window policy now feeds M2 | Early policy work active; broad desktop qualification planned | -| M4 | Agent-driven compiler and GPU-tool handoffs | M1 and M2; foreground acceptance from M3 | Active: bounded IQ3_S foreground/compiler handoff passed; actual GPU video component passed; installed-agent video qualification is next | -| M5 | Recovery across application restart, sleep/wake and computer reboot | M2 and supported M4 effect contracts | Planned | +| M4 | Agent-driven compiler and GPU-tool handoffs | M1 and M2; foreground acceptance from M3 | Active: bounded IQ3_S compiler/video foreground handoffs and exact-known-result automatic worker recovery accepted; observer shutdown, active-tool interruption, whole-host loss and broader workflows remain open | +| M5 | Recovery across application restart, sleep/wake and computer reboot | M2 and supported M4 effect contracts | Active: bounded same-boot application close/reopen accepted; durable reboot-budget checks passed; first actual reboot attempt expired; successful OS reboot and sleep/wake remain open | | M6 | A reproducible daily-use release with fault and soak qualification | M3, M4 and M5 for its advertised scope | Planned | | M7 | Broader engine and agent adapters using the same contracts | M2 contract stability; native acceptance follows M3–M6 | Staged expansion | | M8 | Better policy and efficiency from measured evidence | Stable M3/M4 workloads and M6 baseline | Optional optimization | @@ -993,10 +1018,10 @@ coordinator exported `CompilerHost.terminal_receipt` after the worker joined and before stopping the mailbox runtime or retiring the host binding, ran ordinary reconciliation, and finished the task with a replacement worker under the original 900-second deadline. Independent closed review passed 99 checks with 74 original -case files unchanged. Next wire automatic desktop recovery through the same -verified host export and reconciliation path, then qualify automatic SDK recovery -and unchanged-IQ3_S recovery. Interruption during active tool execution remains a -subsequent gate. +case files unchanged. This increment's next gate was automatic desktop recovery +through the same verified host export and reconciliation path; the subsequent +automatic SDK and unchanged-IQ3_S results are recorded below. Interruption during +active tool execution remains a subsequent gate. **Automatic desktop recovery result:** `DesktopComposition` now runs that verified export, reconcile and replacement sequence itself, through a narrow recovered-state @@ -1008,8 +1033,9 @@ then passed 1,595 core tests and all 16 remaining workflow commands. The [installed SDK trial](provenance/automatic-tool-recovery/installed-worker-loss-2026-11-08.json) (21 closed-review checks) and the [physical IQ3_S foreground trial](provenance/automatic-tool-recovery/physical-foreground-recovery-2026-11-08.json) (49 closed-audit checks, two earlier failed attempts retained) both completed with -automatic recovery after worker exit 86. Next: interruption during an active tool, -host loss, observer shutdown diagnosis and the M5 restart/reboot gates. +automatic recovery after worker exit 86. Next: diagnose the retained observer +shutdown failure, then qualify interruption during an active tool. Host loss and +the M5 restart/reboot gates remain separate. **Validation adjustment:** the [original video CI](provenance/video-tool/hosted-ci-timeout-2026-11-02.json) closed with all 12 core-test steps passing, but two final distribution checks diff --git a/docs/evidence.md b/docs/evidence.md index 0d1fcf6..cb0589d 100644 --- a/docs/evidence.md +++ b/docs/evidence.md @@ -2,6 +2,34 @@ This ledger separates portable software tests, source reconstruction, actual SDK/host fixtures, real model tasks and historical predecessor results. A pass in one does not establish a pass in another. The older prototype checks below did not execute the new gateway; the new gateway has its own evidence here. +## Development integration — 9 November 2026 + +The six reviewed development PRs merged in dependency order. Before each merge, +all fourteen original hosted jobs for its exact head had completed successfully; +the prospective merge tree matched that head. After each merge, the actual +merge tree was checked again and the reviewed head was an ancestor of main. +The following identities preserve the integration record independently of PR +descriptions. Hosted source checks do not extend the installed or physical +qualification recorded in the receipts below. + +| PR | Reviewed head | Merge commit | +| --- | --- | --- | +| [#34](https://github.com/midhatn/adaptive-inference/pull/34) | `f94e11b360c15d41b621ee9641301521846774b0` | `3819a8209a66800901654279a01639dd5a305739` | +| [#35](https://github.com/midhatn/adaptive-inference/pull/35) | `1f7f28e1388d4aa39fa6ff7f8ddc2445f88e127d` | `771692fa5abc650679130790a302a03e9c079f24` | +| [#36](https://github.com/midhatn/adaptive-inference/pull/36) | `e62a2915759eb32a42d607b1a0837103bb54e426` | `6753acad46df17b5d35dd9e8a89a176132592fb8` | +| [#37](https://github.com/midhatn/adaptive-inference/pull/37) | `3a07a7776ed41fc811aa92ef2f8ee0b9639d13f9` | `ba6bf2cc665d6851c528791920e591fe4b29d86c` | +| [#38](https://github.com/midhatn/adaptive-inference/pull/38) | `9242434991f2c2c21461061d61d898649299b847` | `4796c680920a4af3ee0ed7cef8b0b153d66852ba` | +| [#39](https://github.com/midhatn/adaptive-inference/pull/39) | `08053a00d92c065b88bda3ce14f80a7cfe078f0f` | `f362ed0ed8c0f9f6552bd71c9c3ba9e8a847aa3a` | + +The final merge tree is `7d17387075e4ac11be05384c5c1b109eaa76f149`, identical to +the reviewed #39 head. This is development integration, not a new release or +installation. The existing `0.4.0a1` tag and assets remain unchanged. Successful +physical computer-reboot continuation remains unqualified: the recorded expired +attempt stays failed. The automatic recovery result still requires an exact +completed tool result in the surviving host, and its separate shutdown observer +remains failed pending diagnosis. See the [Unreleased notes](../CHANGELOG.md#unreleased--development-after-040a1) +and [current plan](../PLAN.md) for the applicable capabilities and next work. + ## Agent-requested GPU video and foreground handoff The [second physical video task](../provenance/video-tool/physical-foreground-video-2026-11-02.json) diff --git a/docs/reboot-budget.md b/docs/reboot-budget.md index 5bfd6a3..e0e255f 100644 --- a/docs/reboot-budget.md +++ b/docs/reboot-budget.md @@ -1,9 +1,14 @@ # Preserve a task's time allowance across a normal reboot -Status: source, package, hosted, isolated installation and scripted SDK gates -passed. The next gate is an actual clean Stop/Windows Restart/resume trial. -The accepted same-boot application restart does not establish computer-reboot -recovery; no physical reboot result is claimed here. +Status as of 9 November 2026: the source is integrated on main after source, +package, hosted, isolated installation and scripted SDK gates passed. The +[first actual clean Stop/Windows Restart attempt](../provenance/installed-reopen/reboot-budget/physical-restart-expired-2026-11-02.json) +preserved saved work, but its original allowance expired before continuation. +The installed reader refused execution before another application/worker launch. +That failed attempt stays closed; do not replay it or reset its budget. +Successful physical reboot continuation remains an open gate requiring a fresh, +separately coordinated case. Accepted same-boot application reopening does not +establish computer-reboot recovery. The first source component is the pure [`execution_budget` helper](../src/adaptive_inference/desktop/execution_budget.py). diff --git a/docs/upstream-delivery.md b/docs/upstream-delivery.md index b665d5c..ff849a9 100644 --- a/docs/upstream-delivery.md +++ b/docs/upstream-delivery.md @@ -12,6 +12,33 @@ contains the engine profiles and client entry points. All actual inference qualification uses the established IQ3_S model; fixture responses are labeled separately. No smaller model or reduced reasoning allowance substitutes for it. +## Current status: 9 November 2026 + +The read-only refresh through **2026-11-09 at 03:03 UTC** confirms **ten open, +one merged and one closed unmerged** among the original twelve propagated PRs. +The nine open documentation/example PRs apply cleanly; this does not clear their +review, CLA, preview or draft gates. The remaining open PR, Strata #1493, is a +conflicting experimental draft. Its next step remains a narrow adapter using +current upstream controls, followed by a reproduced missing contract or defect +and exact target validation before revising the broader proposal. + +Hermes #143827 remains merged for the routing-plugin catalog entry. OpenClaw +#182080 remains closed in favor of merged #182565's existing provider-routing +recipe. Neither accepts this repository's newer desktop controller or qualifies +an installed autonomous workflow. Aider #5899 now reports one successful +`license/cla` status dated 8 November; it is not a code-test pass. Other recorded +contribution and check gates remain unresolved, with no new maintainer correction +identified in the original twelve. + +The account used for this refresh has read access, without push, maintain, admin +or triage permissions, in all 13 external repositories checked. Upstream merges +and maintainer-only labels therefore remain with those repositories; fixing our +own fork branches is still possible when a concrete target change is validated. +No upstream comment or branch update was warranted by this status refresh. +Canonical release results are propagated only when they validate a specific +mergeable change in the receiving PR, not as a general progress announcement. +Earlier head identities and validation dates below retain their original scope. + ## Delivery sequence 1. Preserve the implementation, exact source inputs, licenses and runnable checks here. @@ -47,7 +74,7 @@ MLX-LM and vLLM. | Target | Submitted or retained integration | Recorded delivery status | | --- | --- | --- | -| Strata | [Existing #1493](https://github.com/Niko1221/Strata/pull/1493), head `06ba725d8b9bb42b457302f71dd85204e9760c32` | Draft and conflicting as observed on 30 October. Its 46 CPU/fake-engine routing checks remain historical; `metal-link` and native/public-workflow gates remain unresolved. First evaluate existing upstream load/unload/VRAM controls through a narrow canonical adapter before revising the broader draft. [#3399](https://github.com/Niko1221/Strata/pull/3399) is closed unmerged: [e2e99ae5](https://github.com/Niko1221/Strata/commit/e2e99ae54aae9a5a28938223679ab9a5ec4ef33c) ported its exact child-identity test and the token-default fix with credit, alongside kossum's QoS test. This is source/attribution verification, not a new runtime result. | +| Strata | [Existing #1493](https://github.com/Niko1221/Strata/pull/1493), head `06ba725d8b9bb42b457302f71dd85204e9760c32` | Draft and conflicting on 30 October, reconfirmed 9 November. Its 46 CPU/fake-engine routing checks remain historical; `metal-link` and native/public-workflow gates remain unresolved. First evaluate existing upstream load/unload/VRAM controls through a narrow canonical adapter before revising the broader draft. [#3399](https://github.com/Niko1221/Strata/pull/3399) is closed unmerged: [e2e99ae5](https://github.com/Niko1221/Strata/commit/e2e99ae54aae9a5a28938223679ab9a5ec4ef33c) ported its exact child-identity test and the token-default fix with credit, alongside kossum's QoS test. This is source/attribution verification, not a new runtime result. | | llama.cpp | Existing OpenAI-compatible server profile and preserved native source inputs | Human-authored upstream submission required by its contribution policy | | vLLM | Existing compatible server profile; no duplicate provider | Human validation and DCO required before an upstream submission | | SGLang | [#46199](https://github.com/sgl-project/sglang/pull/46199), head `c3aaf7ec` | Ready for review. All 121 returned checks on the unchanged head comprise 33 successes, 82 skips, five failures and one neutral result; the latest processor workflow succeeded. Prior draft/title/aggregate failures remain on this head, and fork preview requires maintainer approval. Six gateway/SDK fixture checks are not native SGLang qualification. | @@ -57,7 +84,7 @@ MLX-LM and vLLM. | TGI | Retained legacy profile | Repository archived; no new upstream PR possible | | Hermes | [Catalog #143827](https://github.com/NousResearch/hermes-agent/pull/143827), head `2c1411b3` | Merged on 24 October at 20:23:02 UTC (`e7b15ab1`); 17 checks succeeded and 23 skipped. The catalog entry pins `d6f4e201` and `plugins/hermes`. This is not upstream acceptance of the desktop controller or new native/agent qualification. | | OpenClaw | [Docs #182080](https://github.com/openclaw/openclaw/pull/182080), head `f64d74b0`, linked to [issue #182077](https://github.com/openclaw/openclaw/issues/182077) | Closed unmerged on 30 October as superseded by [#182565](https://github.com/openclaw/openclaw/pull/182565), merged at `45fc29dcf159095be6251dba21c249af08a5d05a` on 27 October and closing the issue. The successor documents existing same-model routes, settings, fallbacks and precedence. Original patch, schema/MDX checks and failed/skipped CI remain historical. Documentation acceptance does not qualify runtime scheduling or agent performance. | -| Aider | [#5899](https://github.com/Aider-AI/aider/pull/5899), head `d5e113fb` | Open base-path clarification, distinct from existing runtime-normalization #5488. No hosted checks reported. | +| Aider | [#5899](https://github.com/Aider-AI/aider/pull/5899), head `d5e113fb` | Open base-path clarification, distinct from existing runtime-normalization #5488. The earlier snapshot reported no hosted checks; the 9 November refresh reports one successful `license/cla` status dated 8 November, not a code-test pass. | | OpenHands | [SDK #6180](https://github.com/OpenHands/software-agent-sdk/pull/6180), head `415785b5`, and [docs #997](https://github.com/OpenHands/docs/pull/997), head `4ef32371` | Both drafts. The SDK's human-only template section remains unchanged; companion docs should land with or after the example. SDK and gateway checks use HTTP fixtures. | | Continue | [#13415](https://github.com/continuedev/continue/pull/13415), head `12eba630` | Open base-path guide. CLA, Snyk quota, FFmpeg setup and aggregate checks remain unresolved; formatting passed. | | Cline | [Docs #15308](https://github.com/cline/cline/pull/15308), head `cbbc9626`, plus [feature discussion #15309](https://github.com/cline/cline/discussions/15309) | Open route clarification; fork preview needs maintainer approval. Explicit budget-field support is proposed separately and not implemented. | diff --git a/docs/upstream-prs.md b/docs/upstream-prs.md index 69186bc..7e30faf 100644 --- a/docs/upstream-prs.md +++ b/docs/upstream-prs.md @@ -6,6 +6,30 @@ The newer generalized integrations and their published PRs are tracked in the [current delivery ledger](upstream-delivery.md). That ledger supplements this historical inventory; it does not relabel earlier proposals as merged or tested. +## Current status: 9 November 2026 + +A read-only refresh through **2026-11-09 at 03:03 UTC** checked all 57 unique PRs +linked across this registry and the delivery ledger: **24 open, three merged and +30 closed unmerged**. This includes third-party source references, not 57 authored +submissions. Comments, reviews, inline threads and current check results were +read for all 24 open PRs and four relevant closed PRs; the other closed entries +received a metadata/state refresh. No unresolved inline review threads were +returned in that inspected set. This adds no runtime or performance validation. + +The [original twelve propagated PRs](upstream-delivery.md#current-status-9-november-2026) +remain ten open, one merged and one closed with a merged successor. Nine open +documentation/example PRs apply cleanly; our broad Strata #1493 remains a +conflicting draft. Strata #1324 and #1937 are third-party experimental proposals, +not additional owned propagation work. Their second-Windows-PC and upstream +disk-design gates remain separate from this project's recovery qualification. + +The fresh accessible-account search found no involved PR in Comfy-Org/ComfyUI; +the historical repository alias was not searchable. Existing attribution and +dated validation below remain unchanged. No generic project-progress comments, +upstream merges or unrelated propagation were made during this refresh. + +## Historical inventory and refreshes + The inventory was checked at **2026-10-23T22:51:07Z** (24 October 2026 in the reviewer's local date). It includes **29 Strata**, **7 Hermes**, **1 related fork** authored/involved PRs and **6 explicitly cited source/successor PRs**, including closed work. Metadata and complete conversation/review timelines were read for all 43 entries through the GitHub connector at that time. States below are timestamped observations, not a live status feed. The same accessible-account search found no involved PR in Comfy-Org/ComfyUI or its historical comfyanonymous/ComfyUI name. This is not a claim about inaccessible repositories. Titles below are GitHub's current titles. A PR marked closed may have been ported with credit; it is not labeled merged unless GitHub reports a merge. From 2998745091aa9843f43cc01ea21d7904f79ae94b Mon Sep 17 00:00:00 2001 From: Midhat Nashar <35053211+midhatn@users.noreply.github.com> Date: Mon, 9 Nov 2026 06:33:32 +0300 Subject: [PATCH 2/5] Stabilize Windows validation fixtures and require agent engineering practices --- AGENTS.md | 1 + CHANGELOG.md | 2 + HANDOFF.md | 7 + PLAN.md | 16 + REFERENCES.bib | 16 + docs/evidence.md | 21 + docs/multi-agent-collaboration.md | 34 + .../tests/test_continuation_host.py | 801 ++++++++++++++++++ .../tests/test_desktop_mailbox_runtime.py | 571 +++++++++++++ provenance/ci-fixtures/2026-11-09/record.json | 84 ++ tests/test_continuation_host.py | 10 +- tests/test_desktop_mailbox_runtime.py | 8 +- 12 files changed, 1569 insertions(+), 2 deletions(-) create mode 100644 provenance/ci-fixtures/2026-11-09/predecessor/tests/test_continuation_host.py create mode 100644 provenance/ci-fixtures/2026-11-09/predecessor/tests/test_desktop_mailbox_runtime.py create mode 100644 provenance/ci-fixtures/2026-11-09/record.json diff --git a/AGENTS.md b/AGENTS.md index 982bfab..2208800 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,6 +27,7 @@ that goal. A convenient component pass does not shrink it. ## Shared working rules - This repository is the canonical source. Preserve engine/agent adapters as consumers of shared contracts. +- Apply the active goal's [engineering practices](docs/multi-agent-collaboration.md#engineering-practices): small reviewable increments, explicit acceptance, meaningful validation, independent review and durable handoffs. Improve the process from evidence while prioritizing the working main workflow. - Keep citations, source revisions and license notices with every reused mechanism. - Research first before designing a solution to each new problem: search current primary research, GitHub implementations/issues/PRs and official documentation. Reuse or adapt a suitable existing mechanism before inventing one; record the sources inspected, applicability, licensing and why any necessary new work remains. Reuse prior findings when their relevance and revision are checked. - Use the existing queue, feedback and capability boundaries; do not invent unsupported backend controls. diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d2d634..a861ffd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ These changes belong to the development source. The published `0.4.0a1` tag and - Add **automatic desktop worker replacement** for that narrowly defined pending-result boundary. In the accepted real IQ3_S trial, foreground activity parked inference, one video operation finished, and the first worker exited before recording completion. The live host reconciled the saved result and launched one replacement automatically; the same task completed within its original deadline after two total model requests. Independent closed review passed 49 checks. The separate shutdown observer failed and remains an open diagnostic; this is not active-tool interruption, whole-host loss or reboot recovery. See [automatic recovery](docs/video-tool-recipe.md#automatic-desktop-recovery) and its [physical receipt](provenance/automatic-tool-recovery/physical-foreground-recovery-2026-11-08.json). - Add the **durable reboot-budget foundation**: a fixed expiry and saved remaining-time cap prevent a restart from granting a new execution allowance. Source, package, installation and scripted SDK checks passed. The first actual Windows restart preserved saved work but the original allowance expired before continuation; execution was correctly refused. Successful computer-reboot continuation remains open. See [budget behavior](docs/reboot-budget.md) and the [retained expired attempt](provenance/installed-reopen/reboot-budget/physical-restart-expired-2026-11-02.json). - Add browser-video observation and comparison tooling, retain unsuccessful measurement attempts and correct continuation-test cleanup. The attempted comparison did not establish desktop/video performance because its observation conditions differed; no broad coexistence or speed claim follows from it. +- Make evidence-based agent engineering an explicit goal requirement: bounded changes, meaningful acceptance, independent review, reproducible handoffs and a protected main branch. See the [collaboration practices](docs/multi-agent-collaboration.md#engineering-practices). +- Correct two Windows CI fixture timing assumptions after retaining their original failures. Use a logical observation clock for an ordinary continuation test and a 30-second whole-fixture join guard for retained control work. Twelve targeted tests passed, including stale-observation refusals; production freshness and task deadlines are unchanged. See the [fixture evidence](provenance/ci-fixtures/2026-11-09/record.json). The automatic-recovery source passed the complete **1,595-test local core suite** and all **16 additional workflow commands**, with the earlier failed test-double attempt retained. Source checks, installed SDK fixtures and physical IQ3_S evidence remain separately identified in the [evidence ledger](docs/evidence.md) and [local validation record](provenance/automatic-tool-recovery/local-validation-2026-11-08.json). These results do not complete M3, M4, M5, daily-use qualification or the full Adaptive-Hermes-Media-Autonomous Test. diff --git a/HANDOFF.md b/HANDOFF.md index 5e26a83..7d10171 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -51,6 +51,13 @@ describe these development capabilities; published `0.4.0a1` artifacts retain their original scope. [PLAN.md](PLAN.md) is the authoritative goal, milestone order and next-work plan. M3–M5 and daily-use qualification remain incomplete. +The active goal also requires the [agent engineering practices](docs/multi-agent-collaboration.md#engineering-practices). +The follow-up [Windows CI fixture correction](docs/evidence.md#follow-up-windows-ci-fixture-correction) +retains two original hosted failures and twelve passing targeted checks, including +negative expiry checks. Production deadlines and runtime source are unchanged. +Use the exact current PR checks before integrating; historical or local passes +do not override a failed required check. Main branch protection is enabled. + **Latest accepted M4 increment:** automatic desktop recovery of a pending compiler/video operation after worker loss. `DesktopComposition` now detects a non-zero worker exit with one pending effect whose exact terminal result is diff --git a/PLAN.md b/PLAN.md index afc0403..af32642 100644 --- a/PLAN.md +++ b/PLAN.md @@ -25,6 +25,22 @@ evidence and citations. See the [always-on vision](docs/always-on-controller.md) [integration contract](docs/integration-contract.md) and [source-preservation contract](docs/source-of-truth.md). +### Engineering requirement for the active goal + +Develop this project using current, evidence-based best practices for agent-led +software engineering. Apply the [collaboration workflow](docs/multi-agent-collaboration.md#engineering-practices) +throughout every milestone: research existing solutions, define a bounded user +result and acceptance criteria, implement a small reviewable change, run the +appropriate checks, obtain independent review where useful, integrate through +protected pull requests, and leave a reproducible handoff. Use observed failures +and review feedback to improve both the implementation and this plan. + +Prefer a working end-to-end increment over speculative frameworks, optional +optimization or exhaustive outlier handling. Preserve failed evidence and +qualification standards; agent agreement alone is not validation. Revisit a +practice when new research or project evidence shows a better approach, keeping +the supporting citations and the reason for the change in this repository. + ## Starting point This snapshot distinguishes released work, development work and future goals. diff --git a/REFERENCES.bib b/REFERENCES.bib index 08ee883..1edda32 100644 --- a/REFERENCES.bib +++ b/REFERENCES.bib @@ -1131,3 +1131,19 @@ @misc{erlangOtpSupervision url = {https://www.erlang.org/doc/system/sup_princ.html}, note = {Inspected 2026-11-08 for automatic desktop tool recovery. Design reference only for a supervisor-owned, bounded replacement of a failed worker (restart intensity). Implemented through this repository's existing retained-child lifecycle, task journal and reconcile command; no Erlang/OTP code copied} } + +@misc{openaiAgentFirstHarnessEngineering, + author = {Ryan Lopopolo and OpenAI}, + title = {Harness engineering: leveraging Codex in an agent-first world}, + year = {2026}, + url = {https://openai.com/index/harness-engineering/}, + note = {Reviewed 2026-11-09. Repository knowledge as the system of record, concise agent entry instructions, bounded work and inspectable feedback inform this project's existing collaboration workflow. Practice reference only; no implementation copied or reported throughput inherited} +} + +@misc{anthropicApplicationHarnessDesign, + author = {{Anthropic}}, + title = {Harness design for long-running application development}, + year = {2026}, + url = {https://www.anthropic.com/engineering/harness-design-long-running-apps}, + note = {Reviewed 2026-11-09. Structured handoffs and separation of implementation from evaluation inform the evidence-based agent engineering requirement. Independent review still requires actual acceptance evidence; no agent harness code or provider dependency copied} +} diff --git a/docs/evidence.md b/docs/evidence.md index cb0589d..88805be 100644 --- a/docs/evidence.md +++ b/docs/evidence.md @@ -30,6 +30,27 @@ completed tool result in the surviving host, and its separate shutdown observer remains failed pending diagnosis. See the [Unreleased notes](../CHANGELOG.md#unreleased--development-after-040a1) and [current plan](../PLAN.md) for the applicable capabilities and next work. +### Follow-up Windows CI fixture correction + +The original post-merge [PR38 main run](https://github.com/midhatn/adaptive-inference/actions/runs/42554614440) +failed on Windows/Python 3.12 when an ordinary continuation test used an expired +observation. The production one-second freshness check correctly refused it. +The original [documentation push](https://github.com/midhatn/adaptive-inference/actions/runs/42555520520) +then failed on Windows/Python 3.11 because a three-second test-only join guard +expired while retained control work was pending. Both original runs stay failed. + +The [correction record](../provenance/ci-fixtures/2026-11-09/record.json) retains +exact predecessor test files, failure identities and local validation. The first +fixture now reuses the existing logical observation-clock pattern. The second +uses the existing project/CPython 30-second whole-fixture guard while preserving +shielded ownership and final-intent assertions. Twelve selected tests passed: +both affected paths, all six helper callers and five negative expiry checks. +Three owned inert children joined with their expected exits, without cleanup +kills. Runtime files, production freshness, real execution clocks and task/tool +allowances are unchanged. These source tests do not qualify installed agents, +hardware, physical responsiveness or reboot recovery. Successor hosted CI is a +separate gate; a local pass does not relabel an original failed run. + ## Agent-requested GPU video and foreground handoff The [second physical video task](../provenance/video-tool/physical-foreground-video-2026-11-02.json) diff --git a/docs/multi-agent-collaboration.md b/docs/multi-agent-collaboration.md index 93dc879..5305661 100644 --- a/docs/multi-agent-collaboration.md +++ b/docs/multi-agent-collaboration.md @@ -20,6 +20,40 @@ also draws on [research on long-running agent harnesses](https://www.anthropic.c This is a coordination practice, not evidence that adding more agents guarantees success. Sources and the scope of reuse are retained in [REFERENCES.bib](../REFERENCES.bib). +## Engineering practices + +These practices are an explicit requirement of the active project goal, added +9 November 2026. The workflow below applies them without requiring a particular +agent product or a second human reviewer for every change. + +- Keep `AGENTS.md` a concise entry map; use the canonical plan, current handoff + and linked source/evidence instead of relying on an agent's chat memory. +- Define the user-visible outcome, acceptance and ownership before a bounded + change. Prefer a small working increment and reuse established mechanisms. +- Give independent workers disjoint scopes. Review the actual diff and evidence; + agreement between agents does not establish correctness. +- Match checks to the change. Separate deterministic functional tests from real + timing and hardware measurements. Diagnose failures before retrying; never + weaken production requirements to hide a test-environment problem. +- Retain exact inputs, dependencies, citations, failed attempts and rollback + points. Close owned processes and document the next action before handoff. +- Integrate through pull requests with required checks and resolved discussions. + Update the plan when evidence changes the next useful step. Avoid process or + optional edge-case work that does not advance the functional main workflow. + +This extends the existing incremental-session approach using OpenAI's +[repository knowledge and feedback-loop guidance](https://openai.com/index/harness-engineering/) +and Anthropic's [separate implementation/evaluation and structured-handoff work](https://www.anthropic.com/engineering/harness-design-long-running-apps), +reviewed on 9 November 2026. These are engineering references, not inherited +performance guarantees or copied harness code. + +The canonical GitHub `main` branch is protected as of this date: a pull request, +an up-to-date branch, all seven CI job contexts from GitHub Actions and resolved +conversations are required. Force-push and deletion are disabled, including for +administrators. Required human approvals are zero; independent agent review and +the evidence gates above still apply. Fork maintainers should configure their +own equivalent protection; repository files cannot enforce remote settings. + ## Divide work by a concrete contract One work item has a coordinator/integration owner, bounded implementation or diff --git a/provenance/ci-fixtures/2026-11-09/predecessor/tests/test_continuation_host.py b/provenance/ci-fixtures/2026-11-09/predecessor/tests/test_continuation_host.py new file mode 100644 index 0000000..a573611 --- /dev/null +++ b/provenance/ci-fixtures/2026-11-09/predecessor/tests/test_continuation_host.py @@ -0,0 +1,801 @@ +"""Actual bounded CPU children/SQLite/async host; injected native/window boundary. + +No installed SDK/model/native engine/network service/real UI acceptance. +""" +import asyncio +from contextlib import closing +from copy import deepcopy +import ctypes +from ctypes import wintypes +from datetime import datetime, timezone +import hashlib +import json +import os +from pathlib import Path +import sqlite3 +import subprocess +import sys +import tempfile +import threading +import time +from types import SimpleNamespace +import unittest +from unittest import mock + +from adaptive_inference.config import Config +from adaptive_inference.handoff import HandoffController,ToolRequirements +from adaptive_inference.scheduler import Scheduler +from adaptive_inference.desktop import composition as product +from adaptive_inference.desktop.desktop_controls import ControlledDesktopHost,DesktopControlLifecycle +from adaptive_inference.desktop.continuation_host import HostContinuation,SelectedWorkerStopBinding,selection +from adaptive_inference.desktop.mailbox_runtime import MailboxRuntime +from adaptive_inference.desktop.mailbox_runtime import RuntimeUnavailable +from adaptive_inference.desktop.hermes.continuation import ContinuationAuthority +from adaptive_inference.desktop.control_transport import PreparedTask,ControlTransport,Refused +from adaptive_inference.desktop.ui_intent import UIIntentGate,UIBinding,VerifiedUIObservation +from adaptive_inference.desktop.hermes import recovery as rec,run_recovery as worker +from adaptive_inference.desktop.hermes.service_stop import WorkerStopBinding +from adaptive_inference.execution_clock import CLOCK_ID +from adaptive_inference.task_store import ReconciliationRequired +import test_continuation_worker as seed_fixture +import test_desktop_lifecycle as lifecycle_fixture +from test_desktop_control_transport import Reader,Writer +import continuation_timeout_diagnostics as timeout_diagnostics + +CHILD=Path(__file__).with_name('continuation_cpu_child.py') + + +async def wait_original_ready(child,path,*,timeout_s=30,clock=time.monotonic): + """Bound only this CPU fixture's import/startup; never retry its work.""" + deadline=clock()+timeout_s + while True: + try: + readiness=path.read_text(encoding='utf-8') + except (FileNotFoundError,PermissionError): + # Publication/Windows sharing can briefly prevent observation. + # The same child and original startup bound still govern the wait. + pass + else: + if json.loads(readiness)!={'pid':child.pid,'work_guard_s':15}: + raise AssertionError('CPU fixture readiness identity differs') + return + if child.poll() is not None: + raise AssertionError(f'CPU fixture exited before readiness: {child.returncode}') + if clock()>=deadline:raise TimeoutError('CPU fixture startup deadline exceeded') + await asyncio.sleep(.01) + + +class Adapter: + """Explicit native boundary fixture; status never represents a real engine.""" + def __init__(self): + self._barrier=object();self._cold_start_error=self._startup_failure=self._released=None + self.started_owner=False;self._cold_prepared=True;self.parked_observation={'fixture':True} + self.driver=SimpleNamespace(status=lambda:{'running':False,'state':'stopped','ready':False,'loaded':False}, + owner_idle=lambda:True,port_free=lambda port:True) + def _stopped_identity(self,state): + if state.get('running') is not False:raise RuntimeError('Fixture owner not parked') + async def _call(self,callback,*args):return await asyncio.to_thread(callback,*args) + + +@unittest.skipUnless(os.name == 'nt', 'Windows retained-process creation identity fixture') +class HostTests(unittest.IsolatedAsyncioTestCase): + # Whole-fixture orchestration guard, not a product latency requirement. + # Reuse CPython's short test timeout for real SQLite/filesystem/child work: + # https://github.com/python/cpython/blob/v3.12.10/Lib/test/support/__init__.py + continuation_timeout_s = 30 + + async def asyncSetUp(self): + self.seed=seed_fixture.SelectedWorkerTests();self.seed.setUp();self.seed.seed_report() + self.addCleanup(self.cleanup_seed) + f=self.seed.f + f.plan['base_python']=sys.executable + (f.run/'plan.json').write_text(json.dumps(f.plan),encoding='utf-8') + c=product.DesktopComposition.__new__(product.DesktopComposition);self.c=c + c.work=f.run;c.root=f.run/'host';c.root.mkdir() + c.handles=[];c.logs=[];c.waits={};c.result={};c.store=f.store;c.plan=f.plan + self.child_receipts=[];self.patches=[] + # unittest skips asyncTearDown when asyncSetUp fails. Register retained + # process/stream cleanup before launch, ahead of the seed directory's + # cleanup (LIFO), just as in test_task_store_verification.py. + self.addCleanup(self.cleanup_retained) + launch=c.launch + def retained_launch(command,kind): + before=len(c.handles) + try:return launch(command,kind) + finally: + for child in c.handles[before:]: + created=wintypes.FILETIME();exited=wintypes.FILETIME() + kernel=wintypes.FILETIME();user=wintypes.FILETIME() + get_times=ctypes.WinDLL('kernel32',use_last_error=True).GetProcessTimes + get_times.argtypes=[wintypes.HANDLE,*([ctypes.POINTER(wintypes.FILETIME)]*4)] + get_times.restype=wintypes.BOOL + if not get_times(int(child._handle),ctypes.byref(created),ctypes.byref(exited),ctypes.byref(kernel),ctypes.byref(user)): + raise ctypes.WinError(ctypes.get_last_error()) + self.child_receipts.append({'pid':child.pid,'created_filetime_100ns':(created.dwHighDateTime<<32)|created.dwLowDateTime, + 'observed_created_utc':datetime.now(timezone.utc).isoformat(),'kind':kind, + 'actual_args':child.args,'executable':sys.executable, + 'executable_sha256':hashlib.sha256(Path(sys.executable).read_bytes()).hexdigest(), + 'fixture_sha256':hashlib.sha256(CHILD.read_bytes()).hexdigest(),'child':child}) + c.launch=retained_launch + c.registry=f.box.registry;c.box=f.box;c.capability=f.cap;c.identity=f.identity;c.original=f.original + c.deadline=10.;c.stop_id='original-stop';c.worker_cap=object();c.worker_exit_verified=False + c.host_epoch='same-host';c.qualification_hold_after_report=False;c.continuation=None + c.worker=c.launch([sys.executable,str(CHILD),'--original','--run',str(c.work)],'worker') + await wait_original_ready(c.worker,c.work/'original-fixture-ready.json') + # Imports have completed and the existing 15-second emergency guard is + # armed. Its exit124 remains a failure; only the original exit5 passes. + self.assertEqual(await asyncio.to_thread(c.worker.wait,18),5) + (c.work/'wire-observations.json').write_text('[]',encoding='utf-8') + self.old=c.store.load('orders-report');self.old_hash=rec.digest(self.old.state) + c.intent_cap=object() + c.gate=UIIntentGate(host_epoch='same-host',capability=c.intent_cap, + verify_current=lambda binding:VerifiedUIObservation(binding,time.monotonic(),True,True)) + c.gui=lifecycle_fixture.GUI();c.gui.release.set() + c.gui.binding=UIBinding('same-host','session','orders-report',101,202,303) + c.gui.process=SimpleNamespace(pid=202,poll=lambda:None) + c.gui.verifier=SimpleNamespace(verify_current=lambda binding:VerifiedUIObservation(binding,time.monotonic(),True,True)) + host=ControlledDesktopHost.__new__(ControlledDesktopHost);c.host=host + host.adapter=Adapter() + policy=SimpleNamespace(reason='foreground_clear',allowed=True,fresh=lambda now:True) + controller=HandoffController(host.adapter,policy,demand=c.registry,restore_requirements=ToolRequirements(1,1,1)) + scheduler=Scheduler(Config(),capacity=controller);controller.bind_scheduler(scheduler);controller.state='parked' + host.controller=controller;host.gateway=SimpleNamespace(scheduler=scheduler,instance='same-gateway') + host.closing=host.closed=False;host.phase='running' + host.primary_error=host.cleanup_error=host.demand_error=None + host.control_runtime=None;host._control_thread=threading.current_thread() + host._control_loop=asyncio.get_running_loop();host._control_rebinding=False;host.closes=0 + async def close(*,timeout_s): + self.assertTrue(all(child.poll() is not None for child in c.handles));self.assertTrue(c.gui.joined) + host.closed=True;host.closes+=1 + return {'closed':True,'primary_error':None,'cleanup_error':None,'controller_fault':None} + host.close=close + c.stop_binding=WorkerStopBinding(c.worker,c.work,c.box,requester_capability=c.capability, + capability=c.worker_cap,stop_request_id=c.stop_id,original=c.original,deadline_monotonic=10., + model=worker.MODEL,quality=worker.QUALITY,fence=c.fence,deadline_clock=CLOCK_ID) + c.prepared=PreparedTask('same-host','orders-report',worker.PROMPT) + c.facade=DesktopControlLifecycle(host,c.prepared,worker_stop=c.stop_binding, + worker_capability=c.worker_cap,gui=c.gui,shutdown_timeout_s=5) + c.facade.bind_window(c.gui.binding,c.gui.verifier, + SimpleNamespace(host_epoch='same-host',task_id='orders-report',session_id='session',objective=worker.PROMPT)) + c.runtime=MailboxRuntime(c.prepared,c.box,requester_capability=c.capability, + gate=c.gate,intent_capability=c.intent_cap,lifecycle=c.facade) + await c.runtime.refresh();host.attach_control_runtime(c.runtime) + c.continuation=HostContinuation(c,c.runtime) + c.runtime.window_ready('session') + c.job=SimpleNamespace(_HANDLE=object()) + c.launch_api=SimpleNamespace(handle=lambda child:child,identity=lambda child:(child.pid,child.pid*10), + in_job=lambda child,job:True) + self.behavior='complete';self.pause_delay=0.;self.tasks=[];self.releases=[] + self.actual_popen=subprocess.Popen + def popen(command,**kwargs): + self.assertNotIn('--qualification-hold-after-report',command) + command=[sys.executable,str(CHILD),*command[command.index('--run'):], + '--behavior',self.behavior,'--pause-delay',str(self.pause_delay)] + return self.actual_popen(command,**kwargs) + patch=mock.patch.object(product.subprocess,'Popen',side_effect=popen);patch.start();self.patches.append(patch) + + async def asyncTearDown(self): + # A completed failed transition is already removed from its set, + # but the fixture restore loop still needs its Stop fence. + self.c.runtime.fence_admission() + for release in self.releases:release.set() + c=self.c + if c.runtime._continuation_tasks: + c.runtime.fence_admission() + await asyncio.gather(*(asyncio.shield(task) for task in tuple(c.runtime._continuation_tasks)),return_exceptions=True) + binding=None + if c.facade._selected is not None and c.facade._selected.get('binding') is not None: + binding=c.facade._selected['binding'] + if binding.worker.poll() is None: + binding.request_stop(c.facade._selected['capability']) + await binding.join(timeout=3) + for child in c.handles: + # CPU emergency bounds are 15 s original / 60 s selected; + # cleanup always joins the exact retained Popen, never PID lookup. + await asyncio.to_thread(child.wait,18) + # join(timeout=3) can return while its shielded post-exit SQLite + # verification still runs. Join that exact retained obligation before + # synchronous seed cleanup removes the files it is verifying. + # https://docs.python.org/3.12/library/asyncio-task.html#shielding-from-cancellation + if binding is not None and binding._work is not None: + await asyncio.shield(binding._work) + if c.facade._wake is not None:await c.facade._wake + if c.facade._stop is not None: + await asyncio.gather(asyncio.shield(c.facade._stop),return_exceptions=True) + for task in self.tasks: + await asyncio.gather(asyncio.shield(task),return_exceptions=True) + + def cleanup_seed(self): + # These nested TestCases were setUp(), not run(). Their default + # doCleanups() catches exceptions without a TestResult. Route both + # levels through this running case's existing outcome instead. + # https://github.com/python/cpython/blob/v3.12.10/Lib/unittest/case.py + with mock.patch.object(self.seed,'_outcome',self._outcome), \ + mock.patch.object(self.seed.f,'_outcome',self._outcome): + self.seed.doCleanups() + + def cleanup_retained(self): + # This also runs after incomplete setup or an asyncTearDown exception. + # A forced stop cleans a failed fixture; it never turns it into a pass. + c=self.c;joined=set();killed=set();errors=[] + for child in c.handles: + try: + if child.poll() is None:child.kill();killed.add(child) + child.wait(timeout=10);joined.add(child) + except Exception as exc:errors.append(exc) + for stream in c.logs: + try:stream.close() + except Exception as exc:errors.append(exc) + for patch in reversed(self.patches): + try:patch.stop() + except Exception as exc:errors.append(exc) + destination=os.environ.get('CONTINUATION_TEST_RECEIPTS') + if destination: + rows=[] + for item in self.child_receipts: + row={key:value for key,value in item.items() if key!='child'} + row.update(exit_code=item['child'].returncode,joined=item['child'] in joined, + cleanup_killed=item['child'] in killed, + join_observed_utc=datetime.now(timezone.utc).isoformat()) + # The existing opt-in CI receipt must outlive temporary logs. + # Capture only this exact retained label, after join and close. + log_path=c.root/(item['kind']+'.log') + try: + matching=[stream for stream in c.logs if Path(stream.name)==log_path] + if len(matching)!=1 or not matching[0].closed: + raise ValueError('Retained child log handle differs or remains open') + with log_path.open('rb') as stream: + stream.seek(0,os.SEEK_END);total=stream.tell() + stream.seek(max(0,total-65536));tail=stream.read(65536) + row['output_log']={'name':log_path.name,'total_bytes':total, + 'captured_bytes':len(tail),'truncated':total>len(tail), + 'tail_sha256':hashlib.sha256(tail).hexdigest(), + 'tail_utf8':tail.decode('utf-8',errors='replace')} + except (OSError,ValueError) as exc: + row['output_log']={'name':log_path.name,'capture_error':type(exc).__name__} + rows.append(row) + payload={'test':self.id(),'children':rows, + 'scope':'CPU Python child and actual SQLite; injected native/window/job boundaries; no SDK/model'} + if hasattr(self,'timeout_observation'): + try:after=timeout_diagnostics.after_join(self,joined) + except Exception as exc:after={'capture_error':type(exc).__name__} + payload['timeout_observation']={'before_teardown':self.timeout_observation,'after_join':after} + output=Path(destination);output.mkdir(exist_ok=True) + (output/(self._testMethodName+'.json')).write_text(json.dumps(payload,indent=2),encoding='utf-8') + if errors:raise ExceptionGroup('CPU fixture cleanup failed',errors) + + def request(self,ident='continue-1'): + return {'host_epoch':'same-host','task_id':'orders-report','session_id':'session','method':'continue', + 'request_id':ident,'execution_id':'orders-report','execution_generation':0, + 'expected_task_revision':self.old.revision} + + def intent(self):return not self.c.runtime._stopping and self.c.gui.session_live('session') + + async def select(self):return await self.c.runtime.select_continuation(self.request(),intent_valid=self.intent) + + async def restore(self): + while True: + record=self.c.continuation.current + if record and record.get('resume_id'): + snap=await asyncio.to_thread(record['mailbox'].snapshot) + if any(row['request_id']==record['resume_id'] and row['outcome']=='applied' for row in snap.receipts): + self.c.host.controller.state='ready' + return + if self.c.runtime._stopping:return + await asyncio.sleep(.01) + + async def continue_task(self,grant,*,restore=True): + if restore: + task=asyncio.create_task(self.restore());self.tasks.append(task) + return await self.c.runtime.continue_selected(grant,intent_valid=self.intent) + + async def start_transport(self): + c=self.c + self.transport=ControlTransport(c.prepared,c.runtime,capability=object(),runner_credential='b'*64,io_timeout=.1) + self.transport.bind_session(self.transport._capability,'session','a'*64) + self.assertTrue((await self.wire('window_ready'))['ok']) + + async def wire(self,method,**values): + request={'host_epoch':'same-host','task_id':'orders-report','session_id':'session','token':'a'*64,'method':method} + if method=='continue':request.update(self.request()) + request.update(values);writer=Writer() + await self.transport.admit(Reader((json.dumps(request)+'\n').encode()),writer) + return json.loads(writer.data) + + async def test_transport_continue_preserves_live_intent_and_duplicate_does_not_launch(self): + c=self.c;await self.start_transport() + restoring=asyncio.create_task(self.restore());self.tasks.append(restoring) + first=await self.wire('continue');self.assertTrue(first['ok']) + task=self.transport._records['continue-1']['task'];self.tasks.append(task) + try: + await asyncio.wait_for(asyncio.shield(task), self.continuation_timeout_s) + except TimeoutError: + if os.environ.get('CONTINUATION_TEST_RECEIPTS'): + try:self.timeout_observation=timeout_diagnostics.before_teardown(self,task) + except Exception as exc:self.timeout_observation={'capture_error':type(exc).__name__} + raise + record=c.continuation.current + self.assertEqual(await c.join_child(record['child']),0) + duplicate=await self.wire('continue') + self.assertEqual(duplicate['continuation']['phase'],'running') + self.assertEqual(len(c.handles),2) + self.assertEqual([r['command'] for r in record['mailbox'].snapshot().requests],['pause','resume']) + # This fixture has no host observation loop to refresh after child exit. + await c.runtime.refresh() + reply=await self.wire('status') + self.assertTrue(reply['ok'],reply) + self.assertEqual(reply['status']['execution_id'],record['grant']['payload']['successor_id']) + await self.transport.aclose(timeout=3) + self.assertTrue(self.transport.lifecycle()['stop_confirmed']) + + async def test_old_read_error_still_faults_and_prevents_selection(self): + c=self.c;entered=threading.Event();release=threading.Event();self.releases.append(release) + def failed():entered.set();release.wait(4);raise OSError('actual old read failure') + with mock.patch.object(c.box,'snapshot',side_effect=failed): + read_task=asyncio.create_task(c.runtime.refresh());self.tasks.append(read_task) + self.assertTrue(await asyncio.to_thread(entered.wait,2)) + select_task=asyncio.create_task(self.select());self.tasks.append(select_task) + await asyncio.sleep(.05);release.set() + with self.assertRaisesRegex(OSError,'actual old read failure'):await read_task + with self.assertRaisesRegex(OSError,'actual old read failure'):await select_task + self.assertIn('database_operation_failed',c.runtime._faults) + self.assertIsNone(c.continuation.current);self.assertEqual(len(c.handles),1) + + async def test_inflight_mutation_error_is_not_retired_as_refresh(self): + c=self.c;entered=threading.Event();release=threading.Event();self.releases.append(release) + before=c.box.snapshot() + def failed(*args):entered.set();release.wait(4);raise OSError('actual mutation failure') + with mock.patch.object(c.box,'submit',side_effect=failed): + control=asyncio.create_task(c.runtime.submit_control('old-pause','pause'));self.tasks.append(control) + self.assertTrue(await asyncio.to_thread(entered.wait,2)) + choosing=asyncio.create_task(self.select());self.tasks.append(choosing) + await asyncio.sleep(.05);release.set() + with self.assertRaisesRegex(OSError,'actual mutation failure'):await control + with self.assertRaisesRegex(OSError,'actual mutation failure'):await choosing + self.assertIn('database_operation_failed',c.runtime._faults) + self.assertIsNone(c.continuation.current);self.assertEqual(c.box.snapshot(),before) + + async def test_normal_refresh_still_refuses_stale_observation(self): + with mock.patch.object(self.c.runtime,'_clock',side_effect=[10.,12.,12.]): + with self.assertRaisesRegex(RuntimeUnavailable,'stale'):await self.c.runtime.refresh() + self.assertIn('database_operation_failed',self.c.runtime._faults) + + async def test_stop_during_committed_authorization_retains_no_child_obligation(self): + c=self.c;entered=threading.Event();release=threading.Event();self.releases.append(release) + original=ContinuationAuthority.authorize + def held(authority,*args): + grant=original(authority,*args);entered.set();release.wait(4);return grant + with mock.patch.object(ContinuationAuthority,'authorize',new=held): + choosing=asyncio.create_task(self.select());self.tasks.append(choosing) + self.assertTrue(await asyncio.to_thread(entered.wait,2)) + stop=asyncio.create_task(c.runtime.cooperative_stop());self.tasks.append(stop) + await asyncio.sleep(.05);self.assertTrue(c.runtime._stopping);self.assertFalse(stop.done()) + release.set();grant=await choosing;receipt=await stop + self.assertFalse(receipt['uncertain']);self.assertTrue(receipt['host_closed']) + self.assertEqual(c.facade.worker_receipt['outcome'],'selected_not_launched') + self.assertEqual((await c.runtime.continue_selected(grant,intent_valid=self.intent))['phase'],'blocked') + self.assertEqual(len(c.handles),1) + + async def test_cancelled_select_waiter_does_not_cancel_committed_selection(self): + c=self.c;entered=threading.Event();release=threading.Event();self.releases.append(release) + original=ContinuationAuthority.authorize + def held(authority,*args): + grant=original(authority,*args);entered.set();release.wait(4);return grant + with mock.patch.object(ContinuationAuthority,'authorize',new=held): + choosing=asyncio.create_task(self.select());self.tasks.append(choosing) + self.assertTrue(await asyncio.to_thread(entered.wait,2));choosing.cancel() + with self.assertRaises(asyncio.CancelledError):await choosing + release.set() + await asyncio.gather(*(asyncio.shield(task) for task in tuple(c.runtime._continuation_tasks))) + self.assertIs(c.facade._selected,c.continuation.current) + self.assertEqual(len(c.handles),1);self.assertFalse(c.runtime._faults) + self.assertTrue((await c.runtime.cooperative_stop())['host_closed']) + + async def test_staging_failure_keeps_grant_obligation_without_relabelling_old_cache(self): + c=self.c;old_box=c.box;old_observe=c.runtime._observe + def observe(box=None): + if box is not None and box is not old_box:raise OSError('staging read failure') + return old_observe(box) + with mock.patch.object(c.runtime,'_observe',side_effect=observe): + with self.assertRaisesRegex(OSError,'staging read failure'):await self.select() + record=c.continuation.current + self.assertTrue(record['committed']);self.assertIs(c.facade._selected,record) + self.assertEqual(c.runtime.status()['execution_id'],'orders-report') + self.assertEqual(c.runtime._cache.snapshot.target.task_id,'orders-report') + self.assertEqual((await c.runtime.lookup_continuation(self.request()))['phase'],'blocked') + receipt=await c.runtime.cooperative_stop() + self.assertTrue(receipt['host_closed']);self.assertIsNotNone(receipt['fault']) + self.assertEqual(c.facade.worker_receipt['outcome'],'selected_not_launched') + + async def test_unknown_launch_slot_cannot_use_old_worker_receipt(self): + c=self.c;await self.select();record=c.continuation.current + record['launch_attempted']=True + with self.assertRaisesRegex(ReconciliationRequired,'known unstarted'): + await c.runtime.cooperative_stop() + self.assertFalse(c.host.closed);self.assertIsNone(c.facade.worker_receipt) + + async def test_committed_grant_lost_return_is_retained_but_never_launched(self): + c=self.c;original=ContinuationAuthority.authorize + def lost(authority,*args): + original(authority,*args);raise OSError('committed grant return lost') + with mock.patch.object(ContinuationAuthority,'authorize',new=lost): + with self.assertRaisesRegex(OSError,'return lost'):await self.select() + record=c.continuation.current + self.assertTrue(record['committed']);self.assertIs(c.facade._selected,record) + self.assertEqual(record['phase'],'blocked');self.assertIn('continuation_failed',c.runtime._faults) + self.assertEqual(c.runtime.selected_execution(),{'execution_id':'orders-report','execution_generation':0}) + self.assertEqual((await c.runtime.lookup_continuation(self.request()))['phase'],'blocked') + self.assertEqual(len(c.handles),1) + receipt=await c.runtime.cooperative_stop() + self.assertTrue(receipt['host_closed']);self.assertIsNotNone(receipt['fault']) + self.assertEqual(c.facade.worker_receipt['outcome'],'selected_not_launched') + + async def test_unknown_grant_lookup_fences_clean_stop(self): + c=self.c;original=ContinuationAuthority.authorize + def lost(authority,*args): + original(authority,*args);raise OSError('committed grant return lost') + with mock.patch.object(ContinuationAuthority,'authorize',new=lost), \ + mock.patch.object(ContinuationAuthority,'lookup',side_effect=OSError('lookup unavailable')): + with self.assertRaisesRegex(OSError,'return lost'):await self.select() + self.assertIsNone(c.continuation.current) + self.assertFalse(c.continuation.records['continue-1']['authorization_resolved']) + with self.assertRaisesRegex(RuntimeUnavailable,'authorization outcome is unresolved'): + await c.runtime.cooperative_stop() + self.assertFalse(c.host.closed);self.assertIsNone(c.facade.worker_receipt) + + async def test_readiness_has_240_second_cap_with_longer_grant(self): + c=self.c;await self.select();record=c.continuation.current + # No new child: isolate the readiness timer with an explicitly live fixture. + record['child']=SimpleNamespace(pid=999,poll=lambda:None) + record['diagnostics']=c.work/'unused-readiness' + start=record['grant']['deadline_monotonic']-900 + with mock.patch.object(product,'execution_now',side_effect=[start,start+240]): + with self.assertRaisesRegex(TimeoutError,'readiness boundary'): + await c.ready_continuation(record,self.intent) + self.assertEqual(record['readiness_deadline_monotonic'],start+240) + record['child']=None + + async def test_readiness_never_extends_shorter_grant(self): + c=self.c;await self.select();record=c.continuation.current + record['child']=SimpleNamespace(pid=999,poll=lambda:None);record['diagnostics']=c.work/'unused-readiness' + deadline=record['grant']['deadline_monotonic'] + with mock.patch.object(product,'execution_now',side_effect=[deadline-30,deadline]): + with self.assertRaisesRegex(TimeoutError,'readiness boundary'): + await c.ready_continuation(record,self.intent) + self.assertEqual(record['readiness_deadline_monotonic'],deadline);record['child']=None + + async def test_long_bootstrap_status_preserves_selected_pair_and_live_intent(self): + c=self.c;self.pause_delay=1.3;self.behavior='hold' + grant=await self.select() + continuing=asyncio.create_task(self.continue_task(grant,restore=False));self.tasks.append(continuing) + while c.continuation.current['child'] is None:await asyncio.sleep(.01) + await asyncio.sleep(1.1) + status=c.runtime.status() + self.assertEqual(status['execution_id'],grant['execution_id']);self.assertFalse(c.runtime._faults) + self.assertEqual(c.runtime._cache.snapshot.target.task_id,grant['execution_id']) + receipt=await c.runtime.cooperative_stop() + self.assertTrue(receipt['host_closed']);self.assertFalse(receipt['uncertain']) + + async def test_atomic_select_keeps_logical_identity_and_new_snapshot(self): + c=self.c;old_box=c.box;old_binding=c.gate._binding + grant=await self.select();record=c.continuation.current + self.assertEqual(c.runtime.selected_execution(),selection(record['grant'])) + self.assertIs(c.host.controller.demand,c.registry);self.assertIs(c.runtime.mailbox,c.box) + self.assertIsNot(c.box,old_box);self.assertEqual(c.runtime.status()['task_revision'],0) + self.assertEqual(c.runtime._cache.snapshot.target.task_id,grant['execution_id']) + self.assertEqual(c.prepared.task_id,'orders-report');self.assertEqual(c.gate._binding,old_binding) + self.assertFalse(c.host.controller._demand_valid);self.assertEqual(c.host.controller._demand_rows,()) + self.assertEqual(rec.digest(c.store.load('orders-report').state),self.old_hash) + self.assertEqual(len(c.handles),1) + self.assertEqual(await c.runtime.lookup_continuation(self.request()),grant) + self.assertEqual(len(c.handles),1) + receipt=await c.runtime.cooperative_stop() + self.assertEqual(receipt,{'worker_joined':True,'host_closed':True,'uncertain':False,'fault':None}) + self.assertEqual(c.facade.worker_receipt['outcome'],'selected_not_launched') + + async def test_actual_cpu_child_one_resume_shared_report_and_lifetime(self): + c=self.c;report=(c.work/'work/report.json').read_bytes();effects=(c.work/'effect-receipts.jsonl').read_bytes() + grant=await self.select() + original_save = rec.save + readiness_transitions = [] + def observe_save(path, value): + if Path(path).name == 'controller-ready.json': + readiness_transitions.append(c.runtime._transitioning) + return original_save(path, value) + with mock.patch.object(rec, 'save', side_effect=observe_save): + reply=await self.continue_task(grant) + self.assertEqual(readiness_transitions, [False]) + record=c.continuation.current + self.assertEqual(reply['phase'],'running') + self.assertEqual(await c.join_child(record['child']),0) + self.assertEqual(record['binding']._verify_exit(0),'task_completed') + snapshot=record['mailbox'].snapshot() + self.assertEqual([r['command'] for r in snapshot.requests],['pause','resume']) + self.assertEqual(snapshot.requests[-1]['request_id'],record['resume_id']) + ready=json.loads((record['diagnostics']/'controller-ready.json').read_bytes()) + self.assertEqual(ready['selected_execution'],selection(record['grant'])) + self.assertEqual((c.work/'work/report.json').read_bytes(),report) + self.assertEqual((c.work/'effect-receipts.jsonl').read_bytes(),effects) + self.assertEqual(snapshot.target.state['requests'],0) + self.assertEqual(await c.runtime.lookup_continuation(self.request()),{**grant,'phase':'blocked'}) + self.assertEqual(len(c.handles),2) + receipt=await c.runtime.cooperative_stop();self.assertTrue(receipt['host_closed']);self.assertFalse(receipt['uncertain']) + + async def test_new_task_admission_reuses_exact_completed_successor_verifier(self): + from adaptive_inference.desktop.task_admission import TaskAdmission + from adaptive_inference.task_store import TaskStore, TaskBusyError + from adaptive_inference.task_spec import TaskSpec, BUDGET + from adaptive_inference.demand import DemandIdentity + from test_task_admission import create_task + c=self.c + original_binding=c.stop_binding + grant=await self.select();await self.continue_task(grant) + record=c.continuation.current + self.assertEqual(await c.join_child(record['child']),0) + cp=record['mailbox'].snapshot().target + history=c.store.history(cp.task_id) + tasks=c.root/'new-tasks';tasks.mkdir() + admission=TaskAdmission(TaskStore(c.root/'admission.sqlite3'),host_generation=c.host_epoch, + tasks_root=tasks,initial_task_id='orders-report',initial_run=c.work) + with self.assertRaisesRegex(ReconciliationRequired,'Original worker no longer'): + with admission._released_checkpoint(admission.initial,'orders-report',self.old.revision, + stop_binding=original_binding):pass + task_spec=TaskSpec({'schema':'adaptive.task-spec.v1','prompt':'Explain one idea.', + 'inputs':[],'outputs':[],'budget':BUDGET,'validation':{'validator':None}}) + payload=admission.request(command_id='new-task',expected_selection_revision=0, + predecessor_execution_id=cp.task_id,predecessor_revision=cp.revision,spec=task_spec, + arrived_at_utc='2026-10-26T12:00:00Z') + authority_cp=admission.store.load(admission.task_id) + with admission.store.claim(admission.task_id,expected_revision=authority_cp.revision) as owner: + with self.assertRaisesRegex(ReconciliationRequired,'Unverified successor'): + admission.reserve(owner,payload,predecessor_cleanup_sha256='1'*64) + admission.retain_execution_binding(record['binding']) + # The continuation authority cannot be claimed while admission holds + # its stable lock; this prevents a concurrent selection during write. + saved=owner.save_checkpoint + def guarded_save(*args,**kwargs): + authority=record['authority'];current=c.store.load(authority.task_id) + with self.assertRaises(TaskBusyError): + with c.store.claim(current.task_id,expected_revision=current.revision):pass + return saved(*args,**kwargs) + with mock.patch.object(owner,'save_checkpoint',side_effect=guarded_save): + reservation=admission.reserve(owner,payload,predecessor_cleanup_sha256='1'*64) + operation=admission.begin(owner,reservation,action='prepare') + run=Path(payload['run']);run.mkdir();(run/'work').mkdir() + identity=DemandIdentity(**{key:payload['demand_identity'][key] for key in + ('registration_id','task_id','owner_id','arrived_at_utc')}) + create_task(TaskStore(run/'task.sqlite3'),identity,task_spec,payload['session_id']) + result_path=record['diagnostics']/f"result-{record['child'].pid}.json" + original=result_path.read_bytes() + try: + changed=json.loads(original);changed['continuation_cleanup_verified']=False + result_path.write_text(json.dumps(changed),encoding='utf-8') + with self.assertRaisesRegex(ReconciliationRequired,'Selected result'): + admission.prepared(owner,reservation,operation_id=operation) + finally: + result_path.write_bytes(original) + admission.prepared(owner,reservation,operation_id=operation) + self.assertEqual(admission.status()['selected']['task_id'],payload['task_id']) + self.assertEqual(c.store.history(cp.task_id),history) + + async def test_new_task_stop_reuses_committed_unlaunched_verifier(self): + from adaptive_inference.desktop.task_admission import TaskAdmission + from adaptive_inference.task_store import TaskStore, TaskBusyError + c=self.c;grant=await self.select();record=c.continuation.current + tasks=c.root/'new-tasks';tasks.mkdir() + admission=TaskAdmission(TaskStore(c.root/'admission.sqlite3'),host_generation=c.host_epoch, + tasks_root=tasks,initial_task_id='orders-report',initial_run=c.work) + history=c.store.history(grant['execution_id']) + with admission._unlaunched_checkpoint(admission.initial,c.continuation) as (cp,outcome): + self.assertEqual(cp.task_id,grant['execution_id']) + self.assertEqual(outcome,'selected_not_launched') + for ident in (record['authority'].task_id,cp.task_id): + current=c.store.load(ident) + with self.assertRaises(TaskBusyError): + with c.store.claim(ident,expected_revision=current.revision):pass + self.assertEqual(c.store.history(grant['execution_id']),history) + record['launch_attempted']=True + try: + with self.assertRaisesRegex(ReconciliationRequired,'known unstarted'): + with admission._unlaunched_checkpoint(admission.initial,c.continuation):pass + finally:record['launch_attempted']=False + + async def test_new_task_release_locks_first_continuation_authority_before_row_exists(self): + from adaptive_inference.desktop.task_admission import TaskAdmission + from adaptive_inference.task_store import TaskStore, TaskBusyError + from adaptive_inference.desktop.hermes.continuation_worker import scope_for + c=self.c;tasks=c.root/'new-tasks';tasks.mkdir() + admission=TaskAdmission(TaskStore(c.root/'admission.sqlite3'),host_generation=c.host_epoch, + tasks_root=tasks,initial_task_id='orders-report',initial_run=c.work) + with admission._released_checkpoint(admission.initial,'orders-report',self.old.revision, + stop_binding=c.stop_binding): + authority=ContinuationAuthority(c.store,'orders-report', + scope_for(c.work,c.plan,(c.work/'plan.json').read_bytes()),host_generation=c.host_epoch) + cp=c.store.load(authority.task_id) + with self.assertRaises(TaskBusyError): + with c.store.claim(cp.task_id,expected_revision=cp.revision):pass + + async def test_stop_before_initial_pause_ack_joins_new_child_without_resume(self): + self.pause_delay=.5;self.behavior='hold';c=self.c + grant=await self.select() + task=asyncio.create_task(self.continue_task(grant,restore=False));self.tasks.append(task) + while c.continuation.current['child'] is None:await asyncio.sleep(.01) + receipt=await c.runtime.cooperative_stop() + self.assertFalse(receipt['uncertain']);self.assertTrue(receipt['host_closed']) + record=c.continuation.current + self.assertEqual(record['child'].poll(),4) + self.assertFalse(any(row['command']=='resume' for row in record['mailbox'].snapshot().requests)) + self.assertEqual((await task)['phase'],'blocked') + + async def test_stop_while_native_readiness_waits_keeps_resume_once(self): + self.behavior='hold';c=self.c + grant=await self.select() + task=asyncio.create_task(self.continue_task(grant,restore=False));self.tasks.append(task) + while c.continuation.current['phase']!='running':await asyncio.sleep(.01) + receipt=await c.runtime.cooperative_stop() + self.assertTrue(receipt['host_closed']);self.assertFalse(receipt['uncertain']) + record=c.continuation.current + self.assertEqual(record['child'].poll(),4) + self.assertEqual(sum(r['command']=='resume' for r in record['mailbox'].snapshot().requests),1) + + async def test_stale_queued_control_cannot_mutate_selected_mailbox(self): + await self.select();c=self.c + with self.assertRaisesRegex(Refused,'stale_execution'): + await c.runtime.submit_control('old-click','resume',execution={'execution_id':'orders-report','execution_generation':0}) + self.assertEqual(c.box.snapshot().requests,()) + + async def test_status_during_old_read_drain_remains_exact_after_cache_expiry(self): + c=self.c;entered=threading.Event();release=threading.Event();self.releases.append(release) + actual=c.box.snapshot + def blocked():entered.set();release.wait(4);return actual() + with mock.patch.object(c.box,'snapshot',side_effect=blocked): + read_task=asyncio.create_task(c.runtime.refresh());self.tasks.append(read_task) + self.assertTrue(await asyncio.to_thread(entered.wait,2)) + select_task=asyncio.create_task(self.select());self.tasks.append(select_task) + await asyncio.sleep(1.1) + status=c.runtime.status() + self.assertEqual(status['execution_id'],'orders-report');self.assertEqual(status['task_revision'],self.old.revision) + self.assertFalse(c.runtime._faults);release.set() + await read_task;grant=await select_task + self.assertEqual(c.runtime.status()['execution_id'],grant['execution_id']) + self.assertEqual(c.runtime.status()['task_revision'],0) + + async def test_inflight_controller_read_is_drained_before_registry_commit(self): + c=self.c;entered=threading.Event();release=threading.Event();self.releases.append(release) + actual=c.registry.observations + def blocked():entered.set();release.wait(3);return actual() + with mock.patch.object(c.registry,'observations',side_effect=blocked): + old_read=asyncio.create_task(c.host.controller.refresh_demand());self.tasks.append(old_read) + self.assertTrue(await asyncio.to_thread(entered.wait,2)) + choosing=asyncio.create_task(self.select());self.tasks.append(choosing) + await asyncio.sleep(.05);self.assertFalse(choosing.done());release.set() + await old_read;await choosing + self.assertEqual(c.host.controller._demand_rows,());self.assertFalse(c.host.controller._demand_valid) + await c.host.controller.refresh_demand() + self.assertEqual({row.task_id for row in c.host.controller._demand_rows},{c.identity.task_id}) + + async def test_launch_diagnostic_failure_keeps_child_and_binding_for_stop(self): + c=self.c;grant=await self.select();actual=c.event + def event(kind,**fields): + if kind=='worker_launched':raise OSError('after retained CPU child') + return actual(kind,**fields) + with mock.patch.object(c,'event',side_effect=event): + with self.assertRaisesRegex(OSError,'retained CPU child'):await self.continue_task(grant,restore=False) + record=c.continuation.current + self.assertIs(record['child'],c.handles[-1]);self.assertIsNotNone(record['binding']) + receipt=await c.runtime.cooperative_stop() + self.assertEqual(record['child'].poll(),4);self.assertIsNotNone(receipt['fault']) + + async def test_old_or_wrong_generation_result_refused(self): + c=self.c;grant=await self.select();await self.continue_task(grant) + record=c.continuation.current;self.assertEqual(await c.join_child(record['child']),0) + path=record['diagnostics']/f"result-{record['child'].pid}.json";raw=path.read_bytes() + value=json.loads(raw);value['selected_execution']['execution_generation']=0;rec.save(path,value) + with self.assertRaisesRegex(ReconciliationRequired,'Selected result identity'):record['binding']._verify_exit(0) + path.write_bytes(raw) + self.assertEqual(record['binding']._verify_exit(0),'task_completed') + # Even a valid selected result in the old shared root cannot substitute + # for this execution's scoped diagnostic file. + legacy=c.work/path.name;legacy.write_bytes(raw);path.unlink() + try: + with self.assertRaises(FileNotFoundError):record['binding']._verify_exit(0) + finally:path.write_bytes(raw);legacy.unlink() + + async def test_selected_result_requires_matching_lifetime_returncode(self): + c=self.c;grant=await self.select();await self.continue_task(grant) + record=c.continuation.current;self.assertEqual(await c.join_child(record['child']),0) + authority=record['authority'];history=c.store.history(authority.task_id) + completed=next(row for row in reversed(history) if row['event']=='operation_completed') + changed=deepcopy(completed['payload']);changed['event_data']['result']['returncode']=4 + # Connection's transaction context does not close the SQLite handle. + # https://docs.python.org/3.11/library/sqlite3.html#how-to-use-the-connection-context-manager + with closing(sqlite3.connect(c.store.path)) as db, db: + db.execute('UPDATE history SET payload=? WHERE task_id=? AND revision=?', + (json.dumps(changed),authority.task_id,completed['revision'])) + with self.assertRaisesRegex(ReconciliationRequired,'exact result'):record['binding']._verify_exit(0) + with closing(sqlite3.connect(c.store.path)) as db, db: + db.execute('UPDATE history SET payload=? WHERE task_id=? AND revision=?', + (json.dumps(completed['payload']),authority.task_id,completed['revision'])) + + +class FixtureStartupTests(unittest.TestCase): + def test_delayed_import_readiness_has_its_own_bound(self): + with tempfile.TemporaryDirectory() as directory: + path=Path(directory)/'ready.json' + child=SimpleNamespace(pid=123,poll=lambda:None) + async def release(_): + path.write_text(json.dumps({'pid':123,'work_guard_s':15}),encoding='utf-8') + # Deterministically simulate 11 seconds before ready, without wall + # clock sleeps or a tiny scheduling threshold on the CI runner. + clock=mock.Mock(side_effect=[0.,11.]) + with mock.patch.object(asyncio,'sleep',side_effect=release) as paused: + asyncio.run(wait_original_ready(child,path,clock=clock)) + self.assertEqual(paused.call_count,1) + + def test_readiness_timeout_wrong_identity_and_emergency_exit_refuse(self): + with tempfile.TemporaryDirectory() as directory: + path=Path(directory)/'ready.json' + child=SimpleNamespace(pid=123,poll=lambda:None) + with self.assertRaisesRegex(TimeoutError,'startup deadline'): + asyncio.run(wait_original_ready(child,path,clock=mock.Mock(side_effect=[0.,31.]))) + path.write_text(json.dumps({'pid':999,'work_guard_s':15}),encoding='utf-8') + with self.assertRaisesRegex(AssertionError,'identity differs'): + asyncio.run(wait_original_ready(child,path)) + path.unlink();child.poll=lambda:124;child.returncode=124 + with self.assertRaisesRegex(AssertionError,'before readiness: 124'): + asyncio.run(wait_original_ready(child,path)) + + @unittest.skipUnless(os.name=='nt','Windows retained-process setup fixture') + def test_async_setup_failure_still_joins_child_and_closes_streams(self): + class FailedSetup(HostTests): + async def test_injected_setup_failure(self): + self.fail('Test body must not run') + case=FailedSetup('test_injected_setup_failure') + result=unittest.TestResult() + with mock.patch.object(sys.modules[__name__],'wait_original_ready', + side_effect=RuntimeError('injected readiness failure')): + case.run(result) + nested='\n'.join(trace for _,trace in result.errors+result.failures) + self.assertEqual(len(result.errors),1,nested) + self.assertIn('injected readiness failure',result.errors[0][1]) + self.assertFalse(result.failures,nested) + self.assertIsNotNone(case.c.worker.returncode) + self.assertTrue(all(stream.closed for stream in case.c.logs)) + self.assertFalse(case.c.work.exists()) + + + @unittest.skipUnless(os.name=='nt','Windows retained-process body failure fixture') + def test_async_body_error_still_finishes_restore_and_retained_cleanup(self): + class FailedBody(HostTests): + async def test_injected_body_failure(self): + grant=await self.select();launch=self.c.launch_continuation + async def fail_after_pause(record,coordinator,intent_valid): + await launch(record,coordinator,intent_valid) + raise RuntimeError('injected failure after initial Pause before Resume') + self.c.launch_continuation=fail_after_pause + await self.continue_task(grant) + + async def asyncTearDown(self): + try: + # Bound this regression itself if the old teardown bug + # returns; original production/fixture budgets stay intact. + await asyncio.wait_for(super().asyncTearDown(),20) + finally: + self.c.runtime.fence_admission() + await asyncio.gather(*self.tasks,return_exceptions=True) + case=FailedBody('test_injected_body_failure');result=unittest.TestResult() + case.run(result) + nested='\n'.join(trace for _,trace in result.errors+result.failures) + self.assertEqual(len(result.errors),1,nested) + self.assertIn('injected failure after initial Pause before Resume',result.errors[0][1]) + self.assertFalse(result.failures,nested) + self.assertTrue(case.c.runtime._stopping) + self.assertTrue(all(task.done() and not task.cancelled() for task in case.tasks)) + self.assertFalse(case.c.runtime._continuation_tasks) + self.assertFalse(case.c.runtime._pending) + self.assertEqual([child.returncode for child in case.c.handles],[5,4]) + self.assertTrue(all(stream.closed for stream in case.c.logs)) + self.assertFalse(case.c.work.exists()) + + +if __name__=='__main__':unittest.main() diff --git a/provenance/ci-fixtures/2026-11-09/predecessor/tests/test_desktop_mailbox_runtime.py b/provenance/ci-fixtures/2026-11-09/predecessor/tests/test_desktop_mailbox_runtime.py new file mode 100644 index 0000000..21c462a --- /dev/null +++ b/provenance/ci-fixtures/2026-11-09/predecessor/tests/test_desktop_mailbox_runtime.py @@ -0,0 +1,571 @@ +"""Real SQLite/mailbox and bounded owned threads; fake streams/UI/lifecycle.""" +import asyncio +from contextlib import contextmanager +from copy import deepcopy +import json +from pathlib import Path +import tempfile +import threading +import time +import unittest +from unittest import mock + +from adaptive_inference.control_mailbox import Mailbox, ControlBlocked, ACKS +from adaptive_inference.demand import DemandIdentity, DemandRegistry, Registration, NAMESPACE +from adaptive_inference.task_store import TaskStore, TaskBusyError +from adaptive_inference.desktop.control_transport import ControlTransport, PreparedTask +from adaptive_inference.desktop.mailbox_runtime import MailboxRuntime, RuntimeUnavailable +from test_desktop_control_transport import Reader, Writer +from adaptive_inference.desktop.ui_intent import UIBinding, UIIntentGate, VerifiedUIObservation + +NOW = '2026-10-24T18:00:00Z' +OBJECTIVE = 'Keep the original task and finish the validated artifact.' + + +class DataFixture: + def data(self): + self.temp = tempfile.TemporaryDirectory(prefix='ui-mailbox-fixture-') + self.addCleanup(self.temp.cleanup) + self.store = TaskStore(Path(self.temp.name) / 'task.sqlite') + self.identity = DemandIdentity('registered', 'task', 'trusted-host', NOW) + self.store.create('task', OBJECTIVE, metadata={NAMESPACE: self.identity.metadata()}) + self.registry = DemandRegistry((Registration(self.store, self.identity),)) + self.cap = object() + self.box = Mailbox(self.registry, 'registered', requester_capability=self.cap) + + def claim(self): + return self.store.claim('task', expected_revision=self.store.load('task').revision) + + def demand(self, state='waiting'): + with self.claim() as owned: + self.registry.transition('registered', owned, state, + expected_revision=owned.checkpoint.revision, updated_at_utc=NOW) + + def submit(self, ident, command): + return self.box.submit(self.cap, ident, command) + + def consume(self): + with self.claim() as owned: + return self.box.consume(owned, updated_at_utc=NOW) + + + + +class FakeLifecycle: + """Only explicit fixture events establish simulated join; no native proof.""" + def __init__(self): + self.bindings = {'session': UIBinding('host', 'session', 'task', 101, 202, 303)} + self.live = {'session'} + self.fenced = False + self.join_entered = asyncio.Event() + self.join = asyncio.Event() + self.join.set() + self.stop_calls = 0 + self.result = {'worker_joined': True, 'host_closed': True, 'uncertain': False, 'fault': None} + + def session_is_live(self, session): + return session in self.live + + def window_ready(self, session): + return self.bindings.get(session) + + def window_closed(self, session): + self.live.discard(session) + + def status(self): + return {'host_phase': 'ready', 'waiting_reason': None} + + def fence_admission(self): + self.fenced = True + + async def cooperative_stop(self): + self.stop_calls += 1 + self.join_entered.set() + await self.join.wait() + return deepcopy(self.result) + + +class RuntimeTests(DataFixture, unittest.IsolatedAsyncioTestCase): + async def asyncSetUp(self): + self.data() + self.demand() + self.intent = object() + self.gate = UIIntentGate(host_epoch='host', capability=self.intent, + verify_current=lambda binding: VerifiedUIObservation(binding, time.monotonic(), True, True)) + self.host = FakeLifecycle() + self.prepared = PreparedTask('host', 'task', OBJECTIVE) + # Observation age is a logical fixture input; real SQLite setup speed + # does not establish expiry. The freshness tests advance it explicitly. + self.observation_time = 20. + self.runtime = MailboxRuntime(self.prepared, self.box, requester_capability=self.cap, + gate=self.gate, intent_capability=self.intent, lifecycle=self.host, max_age_s=3, + clock=lambda: self.observation_time) + self.transport_cap = object() + self.transport = ControlTransport(self.prepared, self.runtime, capability=self.transport_cap, + runner_credential='b' * 64, io_timeout=.25) + self.transport.bind_session(self.transport_cap, 'session', 'a' * 64) + self.releases = [] + await self.runtime.refresh() + self.assertTrue((await self.request('window_ready'))['ok']) + + async def asyncTearDown(self): + for event in self.releases: + event.set() + self.host.join.set() + await self.transport.aclose(timeout=3) + await self.runtime.cooperative_stop() + + def frame(self, method, **changes): + value = {'host_epoch': 'host', 'task_id': 'task', 'session_id': 'session', + 'token': 'a' * 64, 'method': method} + if method in ('pause', 'resume', 'cancel'): + value['request_id'] = 'one' + value.update(self.runtime.selected_execution()) + value.update(changes) + return (json.dumps(value) + '\n').encode() + + async def request(self, method, **changes): + writer = Writer() + task = self.transport.admit(Reader(self.frame(method, **changes)), writer) + await task + return json.loads(writer.data) + + async def completed_control(self, method, *, request_id): + # A handler may truthfully return pending before the retained mutation + # and its authority read finish. Final-intent assertions need that task. + reply = await self.request(method, request_id=request_id) + self.assertTrue(reply['ok'], reply) + await asyncio.wait_for(asyncio.shield(self.transport._records[request_id]['task']), 3) + return reply + + def matches(self): + now = time.monotonic() + return self.gate.matches({'monotonic_s': now, 'foreground': { + 'valid': True, 'hwnd': 101, 'pid': 202, 'creation_time_100ns': 303}}, now) + + async def signal(self, event): + # Bounded waiting uses the actual threading event, not a timing premise. + self.assertTrue(await asyncio.to_thread(event.wait, 2)) + + def block_observation(self): + entered, release = threading.Event(), threading.Event() + self.releases.append(release) + actual = self.box.snapshot + + def blocked(): + entered.set() + if not release.wait(3): + raise AssertionError('Fixture snapshot release timed out') + return actual() + return entered, release, blocked + + async def test_real_latest_resume_authority_pending_and_applied(self): + self.demand('paused') + value = await self.runtime.submit_control('new-resume', 'resume') + self.assertEqual(value['outcome'], 'pending') + self.assertFalse(value['owner_acknowledged']) + self.assertIs(await self.runtime.resume_authority('new-resume'), True) + self.consume() + self.assertIs(await self.runtime.resume_authority('new-resume'), True) + self.assertTrue(self.runtime.status()['controls'][0]['owner_acknowledged']) + + async def test_historical_applied_resume_followed_by_pause_never_activates(self): + self.submit('old-resume', 'resume'); self.consume() + self.submit('later-pause', 'pause'); self.consume() + history = self.store.history(self.box.task_id) + value = await self.completed_control('resume', request_id='old-resume') + self.assertTrue(value['ok']) + self.assertFalse(self.matches()) + self.assertEqual(history, self.store.history(self.box.task_id)) + self.assertIs(await self.runtime.resume_authority('old-resume'), False) + + async def test_new_resume_activates_real_gate_then_pause_revokes(self): + result = await self.completed_control('resume', request_id='fresh') + self.assertTrue(result['ok']) + self.assertTrue(self.matches()) + await self.completed_control('pause', request_id='pause') + self.assertFalse(self.matches()) + self.assertEqual(self.store.load('task').prompt, OBJECTIVE) + self.assertEqual(self.runtime.status()['controls'][0]['outcome'], 'pending') + + async def test_missing_demand_refuses_authority(self): + with self.claim() as owned: + state = deepcopy(owned.checkpoint.state) + state.pop(NAMESPACE) + owned.save_checkpoint(state, expected_revision=owned.checkpoint.revision) + await self.runtime.submit_control('resume', 'resume') + self.assertIs(await self.runtime.resume_authority('resume'), False) + + async def test_inflight_and_interrupted_replacement_are_never_authority(self): + self.submit('resume', 'resume') + with self.claim() as owned: + owned.begin_operation('effect', details={'fixture': 'not executed'}, + expected_revision=owned.checkpoint.revision) + self.assertIs(await self.runtime.resume_authority('resume'), False) + self.assertEqual(self.runtime.status()['waiting_reason'], 'operation_inflight') + with self.claim() as replacement: + self.assertTrue(replacement.checkpoint.requires_reconciliation) + self.assertIs(await self.runtime.resume_authority('resume'), False) + self.assertEqual(self.runtime.status()['waiting_reason'], 'reconciliation') + + async def test_terminal_target_revokes_intent_and_refuses_authority(self): + await self.completed_control('resume', request_id='resume') + self.assertTrue(self.matches()) + with self.claim() as owned: + state = deepcopy(owned.checkpoint.state) + state.update(status='accepted', output='Validated fixture result') + owned.save_checkpoint(state, expected_revision=owned.checkpoint.revision) + self.assertIs(await self.runtime.resume_authority('resume'), False) + self.assertFalse(self.matches()) + + async def test_changed_objective_is_refused_without_claiming_target(self): + self.runtime.prepared = PreparedTask('host', 'task', 'Different objective') + with self.assertRaises(RuntimeUnavailable): + await self.runtime.refresh() + self.assertIsNone(self.store.load('task').owner_epoch) + + async def test_mismatched_objective_refuses_before_any_new_control_commit(self): + self.runtime.prepared = PreparedTask('host', 'task', 'Different objective') + before = self.store.history(self.box.task_id) + with self.assertRaises(RuntimeUnavailable): + await self.runtime.submit_control('must-not-record', 'cancel') + self.assertEqual(before, self.store.history(self.box.task_id)) + + async def test_wrong_intent_capability_is_rejected_before_async_work(self): + with self.assertRaises(ValueError): + MailboxRuntime(self.prepared, self.box, requester_capability=self.cap, + gate=self.gate, intent_capability=object(), lifecycle=self.host) + + async def test_refresh_revokes_external_later_pause_even_before_owner_ack(self): + await self.completed_control('resume', request_id='resume') + self.assertTrue(self.matches()) + self.consume() + self.submit('external-pause', 'pause') + await self.runtime.refresh() + self.assertFalse(self.matches()) + self.assertEqual(self.runtime.status()['controls'][-1]['outcome'], 'pending') + self.consume() + await self.runtime.refresh() + self.assertEqual(self.runtime.status()['demand'], 'paused') + self.assertFalse(self.matches()) + + async def test_latest_pending_resume_can_keep_intent_while_owner_is_paused(self): + self.demand('paused') + # The explicit SQLite setup mutation may outlive the cached observation. + # Establish fresh admission before testing retained Resume intent. + await self.runtime.refresh() + await self.completed_control('resume', request_id='resume') + self.assertTrue(self.matches()) + await self.runtime.refresh() + self.assertEqual(self.runtime.status()['demand'], 'paused') + self.assertTrue(self.matches()) + + async def test_pending_reply_precedes_retained_resume_authority_completion(self): + entered, release, blocked = self.block_observation() + authority_entered = asyncio.Event() + actual_authority = self.runtime.resume_authority + actual_request = self.request + responded = asyncio.get_running_loop().create_future() + async def held_authority(request_id,**kwargs): + authority_entered.set() + with mock.patch.object(self.box, 'snapshot', side_effect=blocked): + return await actual_authority(request_id,**kwargs) + async def observed_request(*args, **kwargs): + reply = await actual_request(*args, **kwargs) + responded.set_result(reply) + return reply + with mock.patch.object(self.runtime, 'resume_authority', side_effect=held_authority), \ + mock.patch.object(self, 'request', side_effect=observed_request): + completed = asyncio.create_task(self.completed_control('resume', request_id='resume')) + try: + # Establish authority entry after SQLite submission before the + # unchanged two-second held-snapshot entry check. + await asyncio.wait_for(authority_entered.wait(), 30) + await self.signal(entered) + operation = self.transport._records['resume']['task'] + # Observe the actual response before releasing authority; no sleep + # or assumption about SQLite/CI scheduling establishes this order. + reply = await asyncio.wait_for(asyncio.shield(responded), 2) + self.assertEqual(reply, {'ok': True, **self.runtime.selected_execution(), 'state': 'pending', 'request_id': 'resume', 'recorded': None}) + self.assertFalse(operation.done()) + self.assertFalse(completed.done()) + self.assertFalse(self.matches()) + finally: + release.set() + # Settle the extra fixture task even if an assertion failed; + # shield keeps its retained control operation uncancelled. + await asyncio.wait_for(asyncio.shield( + asyncio.gather(completed, return_exceptions=True)), 3) + self.assertEqual(completed.result(), reply) + self.assertTrue(operation.done()) + self.assertTrue(self.matches()) + self.assertEqual(len(self.box.snapshot().requests), 1) + + async def test_crash_gap_pending_resume_does_not_invent_ack_or_reapply(self): + self.demand('paused') + self.submit('resume', 'resume') + with self.claim() as owned: + real_save = owned.save_checkpoint + def fail_ack(state, **kwargs): + if ACKS in state: + raise OSError('Fixture failed ACK commit') + return real_save(state, **kwargs) + with mock.patch.object(owned, 'save_checkpoint', side_effect=fail_ack): + with self.assertRaises(OSError): + self.box.consume(owned, updated_at_utc=NOW) + revision = owned.checkpoint.revision + self.assertIs(await self.runtime.resume_authority('resume'), True) + self.assertEqual(self.runtime.status()['controls'][0]['outcome'], 'pending') + self.assertEqual(owned.checkpoint.revision, revision) + with self.claim() as replacement: + before = replacement.checkpoint.revision + self.box.consume(replacement, updated_at_utc=NOW) + self.assertEqual(replacement.checkpoint.revision, before + 1) + await self.runtime.refresh() + self.assertEqual(self.runtime.status()['controls'][0]['outcome'], 'applied') + + async def test_cancelled_refresh_retains_thread_and_stop_waits_for_it(self): + entered, release, blocked = self.block_observation() + with mock.patch.object(self.box, 'snapshot', side_effect=blocked): + waiter = asyncio.create_task(self.runtime.refresh()) + await self.signal(entered) + waiter.cancel() + with self.assertRaises(asyncio.CancelledError): + await waiter + self.assertEqual(self.runtime.diagnostics()['pending_database'], 1) + stopping = asyncio.create_task(self.runtime.cooperative_stop()) + await asyncio.sleep(0) + self.assertFalse(self.host.join_entered.is_set()) + self.assertFalse(stopping.done()) + release.set() + result = await asyncio.wait_for(stopping, 3) + self.assertTrue(result['worker_joined'] and result['host_closed']) + self.assertEqual(self.runtime.diagnostics()['pending_database'], 0) + + async def test_cancelled_submit_commits_once_then_explicit_retry_observes_it(self): + entered, release = threading.Event(), threading.Event() + self.releases.append(release) + actual = self.box.submit + def blocked(*args): + entered.set() + if not release.wait(3): + raise AssertionError('Fixture submit timed out') + return actual(*args) + with mock.patch.object(self.box, 'submit', side_effect=blocked): + waiter = asyncio.create_task(self.runtime.submit_control('stable', 'pause')) + await self.signal(entered) + waiter.cancel() + with self.assertRaises(asyncio.CancelledError): + await waiter + self.assertEqual(self.runtime.diagnostics()['pending_database'], 1) + release.set() + await self.runtime.refresh() + before = self.store.history(self.box.task_id) + result = await self.runtime.submit_control('stable', 'pause') + self.assertEqual(result['outcome'], 'pending') + self.assertEqual(before, self.store.history(self.box.task_id)) + + async def test_disconnected_transport_keeps_real_commit_and_stop_join_obligation(self): + entered, release = threading.Event(), threading.Event() + self.releases.append(release) + actual = self.box.submit + def hold_after_commit(*args): + value = actual(*args) + entered.set() + if not release.wait(3): + raise AssertionError('Fixture postcommit wait exceeded bound') + return value + with mock.patch.object(self.box, 'submit', side_effect=hold_after_commit): + writer = Writer(broken=True) + handler = self.transport.admit(Reader(self.frame('pause', request_id='durable')), writer) + await self.signal(entered) + handler.cancel() + await asyncio.gather(handler, return_exceptions=True) + pending = await self.transport.aclose(timeout=.02) + self.assertFalse(pending['closed']) + self.assertFalse(self.host.join_entered.is_set()) + self.assertEqual(self.runtime.diagnostics()['pending_database'], 1) + self.assertEqual(self.box.status('durable')['outcome'], 'pending') + release.set() + final = await self.transport.aclose(timeout=3) + self.assertTrue(final['closed']) + self.assertTrue(writer.closed and writer.joined) + self.assertEqual(len(self.box.snapshot().requests), 1) + + async def test_capacity_remains_bound_while_cancelled_waiter_thread_runs(self): + self.runtime.max_pending = 1 + entered, release, blocked = self.block_observation() + with mock.patch.object(self.box, 'snapshot', side_effect=blocked): + waiter = asyncio.create_task(self.runtime.refresh()) + await self.signal(entered) + waiter.cancel() + await asyncio.gather(waiter, return_exceptions=True) + with self.assertRaises(RuntimeUnavailable): + await self.runtime.refresh() + release.set() + await self.runtime.cooperative_stop() + + async def test_status_cache_expires_and_does_not_do_database_io(self): + self.runtime.max_age_s = .01 + self.observation_time += .02 + with mock.patch.object(self.box, 'snapshot', side_effect=AssertionError('Blocking status read')): + with self.assertRaises(RuntimeUnavailable): + self.runtime.status() + self.runtime.max_age_s = 3 + await self.runtime.refresh() + + async def test_slow_snapshot_uses_start_time_not_completion_for_freshness(self): + self.runtime.max_age_s = .02 + entered, release, blocked = self.block_observation() + with mock.patch.object(self.box, 'snapshot', side_effect=blocked): + waiter = asyncio.create_task(self.runtime.refresh()) + await self.signal(entered) + self.observation_time += .03 + release.set() + with self.assertRaises(RuntimeUnavailable): + await waiter + + async def test_failed_snapshot_does_not_turn_into_successful_cleanup(self): + with mock.patch.object(self.box, 'snapshot', side_effect=OSError('Private fixture path')): + with self.assertRaises(OSError): + await self.runtime.resume_authority('one') + receipt = await self.runtime.cooperative_stop() + self.assertTrue(receipt['worker_joined'] and receipt['host_closed']) + self.assertEqual(receipt['fault'], 'runtime_operation_failed') + self.assertNotIn('Private fixture path', str(self.runtime.diagnostics())) + + async def test_prior_runtime_fault_cannot_be_hidden_by_later_resume_observation(self): + self.submit('resume', 'resume') + with mock.patch.object(self.box, 'snapshot', side_effect=OSError('Fixture read failed')): + with self.assertRaises(OSError): + await self.runtime.refresh() + self.assertIs(await self.runtime.resume_authority('resume'), False) + self.assertEqual(self.runtime.status()['host_phase'], 'fault') + self.assertFalse(self.matches()) + + async def test_postcommit_read_failure_retry_observes_commit_but_does_not_clear_fault(self): + actual_snapshot = self.box.snapshot + count = 0 + def fail_second_read(): + nonlocal count + count += 1 + if count == 2: + raise OSError('Fixture observation lost after actual commit') + return actual_snapshot() + with mock.patch.object(self.box, 'snapshot', side_effect=fail_second_read): + with self.assertRaises(OSError): + await self.runtime.submit_control('resume', 'resume') + before = self.store.history(self.box.task_id) + self.assertEqual(self.box.status('resume')['outcome'], 'pending') + retried = await self.runtime.submit_control('resume', 'resume') + self.assertEqual(retried['outcome'], 'pending') + self.assertEqual(before, self.store.history(self.box.task_id)) + self.assertIs(await self.runtime.resume_authority('resume'), False) + stopped = await self.runtime.cooperative_stop() + self.assertTrue(stopped['worker_joined'] and stopped['host_closed']) + self.assertEqual(stopped['fault'], 'runtime_operation_failed') + + async def test_malformed_real_ack_prefix_never_becomes_resume_authority(self): + self.submit('resume', 'resume'); self.consume() + with self.claim() as owned: + state = deepcopy(owned.checkpoint.state) + state[ACKS]['receipts'][0]['request_id'] = 'different' + owned.save_checkpoint(state, expected_revision=owned.checkpoint.revision) + with self.assertRaises(ControlBlocked): + await self.runtime.resume_authority('resume') + self.assertFalse(self.matches()) + + async def test_failed_host_fence_remains_visible_even_if_worker_later_joins(self): + with mock.patch.object(self.host, 'fence_admission', side_effect=RuntimeError('Fixture fence failure')): + with self.assertRaises(RuntimeError): + self.runtime.fence_admission() + receipt = await self.runtime.cooperative_stop() + self.assertTrue(receipt['worker_joined'] and receipt['host_closed']) + self.assertEqual(receipt['fault'], 'runtime_operation_failed') + + async def test_stop_waiter_cancellation_preserves_worker_join_and_host_fault(self): + self.host.join.clear() + self.host.result['fault'] = 'fixture_host_fault' + waiter = asyncio.create_task(self.runtime.cooperative_stop()) + await self.host.join_entered.wait() + waiter.cancel() + await asyncio.gather(waiter, return_exceptions=True) + self.assertFalse(self.runtime._stop_task.done()) + self.host.join.set() + result = await self.runtime.cooperative_stop() + self.assertEqual(result['fault'], 'fixture_host_fault') + self.assertEqual(self.host.stop_calls, 1) + + async def test_pause_during_real_authority_read_prevents_late_activation(self): + entered, release, blocked = self.block_observation() + authority_entered = asyncio.Event() + actual_authority = self.runtime.resume_authority + async def held_authority(request_id,**kwargs): + authority_entered.set() + with mock.patch.object(self.box, 'snapshot', side_effect=blocked): + return await actual_authority(request_id,**kwargs) + with mock.patch.object(self.runtime, 'resume_authority', side_effect=held_authority): + writer = Writer() + first = self.transport.admit(Reader(self.frame('resume', request_id='resume')), writer) + # As in the rotation test, first establish SQLite submission, then + # retain the unchanged two-second bound on the held thread entry. + try: + await asyncio.wait_for(authority_entered.wait(), 30) + except TimeoutError: + operation = self.transport._records.get('resume', {}).get('task') + error = (type(operation.exception()).__name__ if operation is not None + and operation.done() and not operation.cancelled() else None) + reply = json.loads(writer.data) if writer.data else None + self.fail(f'Authority setup did not complete: reply={reply!r}, error={error}') + await self.signal(entered) + pause = await self.request('pause', request_id='pause') + self.assertTrue(pause['ok']) + self.assertFalse(self.matches()) + release.set() + await first + await self.transport._records['pause']['task'] + self.assertFalse(self.matches()) + + async def test_rotation_during_real_authority_read_prevents_late_activation(self): + entered, release, blocked = self.block_observation() + authority_entered = asyncio.Event() + actual_authority = self.runtime.resume_authority + async def held_authority(request_id,**kwargs): + authority_entered.set() + with mock.patch.object(self.box, 'snapshot', side_effect=blocked): + return await actual_authority(request_id,**kwargs) + with mock.patch.object(self.runtime, 'resume_authority', side_effect=held_authority): + writer = Writer() + first = self.transport.admit(Reader(self.frame('resume', request_id='resume')), writer) + # SQLite submission precedes the held read. Establish that setup + # explicitly before the unchanged two-second thread-entry check. + try: + await asyncio.wait_for(authority_entered.wait(), 30) + except TimeoutError: + operation = self.transport._records.get('resume', {}).get('task') + error = (type(operation.exception()).__name__ if operation is not None + and operation.done() and not operation.cancelled() else None) + reply = json.loads(writer.data) if writer.data else None + self.fail(f'Authority setup did not complete: reply={reply!r}, error={error}') + await self.signal(entered) + self.transport.bind_session(self.transport_cap, 'new-session', 'c' * 64) + self.host.live.add('new-session') + self.host.bindings['new-session'] = UIBinding('host', 'new-session', 'task', 404, 505, 606) + self.assertTrue((await self.request('window_ready', session_id='new-session', token='c' * 64))['ok']) + release.set() + await first + await asyncio.wait_for(asyncio.shield(self.transport._records['resume']['task']), 3) + self.assertFalse(self.matches()) + + async def test_projection_keeps_only_bounded_fields_and_objective(self): + await self.request('pause', request_id='pause') + value = await self.request('status') + text = json.dumps(value) + self.assertTrue(value['ok']) + self.assertEqual(value['status']['objective'], OBJECTIVE) + for forbidden in ('owner_epoch', 'metadata', str(self.store.path), 'a' * 64, 'b' * 64): + self.assertNotIn(forbidden, text) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/provenance/ci-fixtures/2026-11-09/record.json b/provenance/ci-fixtures/2026-11-09/record.json new file mode 100644 index 0000000..8ccf737 --- /dev/null +++ b/provenance/ci-fixtures/2026-11-09/record.json @@ -0,0 +1,84 @@ +{ + "date": "2026-11-09", + "scope": "Test-fixture timing correction; no production runtime or task-budget change", + "base_revision": "0ee8d3726b8184648b8f5e003e527e6409c37f76", + "files": { + "tests/test_continuation_host.py": { + "predecessor_source": "provenance/ci-fixtures/2026-11-09/predecessor/tests/test_continuation_host.py", + "predecessor_sha256": "4858cf5d589dd3960ccf16f16896c8c6c085672d8c6aa4425c37184f02810bbf", + "successor_sha256": "aadeefab7622f5dba4fb1c604cfeca9d09c52263b44a98e62e794a09001ddee0" + }, + "tests/test_desktop_mailbox_runtime.py": { + "predecessor_source": "provenance/ci-fixtures/2026-11-09/predecessor/tests/test_desktop_mailbox_runtime.py", + "predecessor_sha256": "02cb14272823904f022307a5757f3a13daa9fe68bf734d6e6238554d57268697", + "successor_sha256": "12c11cf6cf1bc3007e18a5051e192d8dd43af34aa3b44d34eae4458091de9430" + } + }, + "original_failures": [ + { + "run": 42554614440, + "job": 128957438758, + "attempt": 1, + "head": "4796c680920a4af3ee0ed7cef8b0b153d66852ba", + "platform": "windows-latest / Python 3.12", + "test": "test_continuation_host.HostTests.test_transport_continue_preserves_live_intent_and_duplicate_does_not_launch", + "failure": "ExpiredObservation during resume_authority -> MailboxRuntime._install", + "original_outcome": "failed; not rerun or relabeled", + "log_sha256": "deb14ea4ecc09d3e7ebdd01d2089a489e7219832d1eb40661b2adb0033018018" + }, + { + "run": 42555520520, + "job": 128960228351, + "attempt": 1, + "head": "0ee8d3726b8184648b8f5e003e527e6409c37f76", + "platform": "windows-latest / Python 3.11", + "test": "test_desktop_mailbox_runtime.RuntimeTests.test_refresh_revokes_external_later_pause_even_before_owner_ack", + "failure": "TimeoutError in the three-second whole-fixture completed_control join guard", + "original_outcome": "failed; not rerun or relabeled", + "log_sha256": "0ae8d1ee261c9bc59cfee0d1553157b1f2ded137e7329a23998a2b5c076cbede" + } + ], + "changes": [ + "Use the existing logical-observation-clock fixture pattern for one ordinary continuation transport test, refreshing its cache under that clock.", + "Use the existing project/CPython 30-second whole-fixture guard for joining retained control work; keep shielding and final-intent assertions." + ], + "unchanged": [ + "All 77 production runtime modules", + "Production observation freshness and read timeouts", + "Real child, UI, execution and orchestration clocks", + "Task and tool allowances", + "Stale-observation refusals and duplicate-launch assertions" + ], + "validation": { + "selected_tests": 12, + "failures": 0, + "errors": 0, + "skips": 0, + "scope": "Both previously failing cases, all six completed_control callers and five expiry/refusal cases", + "test_elapsed_s": 3.108, + "receipt_sha256": "debe2466a111ceec2c3186c44fd6f8ca6b329ff443c4da80a67de6c65a7eaffa", + "owned_child_exit_codes": [ + 5, + 5, + 0 + ], + "all_owned_children_joined": true, + "cleanup_killed": false, + "new_hosted_result": "Not claimed by this local record; observe the new commit original workflows separately" + }, + "research": [ + { + "url": "https://github.com/python/cpython/blob/v3.12.10/Lib/test/support/__init__.py", + "use": "Existing SHORT_TIMEOUT whole-test orchestration guard, not a product latency threshold" + }, + { + "url": "https://docs.python.org/3.12/library/asyncio-task.html#shielding-from-cancellation", + "use": "Keep retained asynchronous work shielded and joined after the caller waits" + } + ], + "limits": [ + "These are CPU/source fixture checks, not installed-agent, model, native-engine or physical responsiveness results.", + "Historical failure logs stay failed. Increasing a fixture join allowance does not extend any product task allowance.", + "Retained predecessor files are historical data for offline inspection, not the selected test implementation." + ] +} diff --git a/tests/test_continuation_host.py b/tests/test_continuation_host.py index a573611..864a424 100644 --- a/tests/test_continuation_host.py +++ b/tests/test_continuation_host.py @@ -303,7 +303,15 @@ async def wire(self,method,**values): return json.loads(writer.data) async def test_transport_continue_preserves_live_intent_and_duplicate_does_not_launch(self): - c=self.c;await self.start_transport() + c=self.c + # As in test_desktop_mailbox_runtime, observation age is a logical + # fixture input, not SQLite/CI scheduling speed. Keep the actual child, + # UI, execution and orchestration clocks unchanged; expiry has its own + # explicit negative tests. Refresh before using this clock's cache. + observation_clock=mock.patch.object(c.runtime,'_clock',return_value=time.monotonic()) + observation_clock.start();self.addCleanup(observation_clock.stop) + await c.runtime.refresh() + await self.start_transport() restoring=asyncio.create_task(self.restore());self.tasks.append(restoring) first=await self.wire('continue');self.assertTrue(first['ok']) task=self.transport._records['continue-1']['task'];self.tasks.append(task) diff --git a/tests/test_desktop_mailbox_runtime.py b/tests/test_desktop_mailbox_runtime.py index 5396f70..28b9627 100644 --- a/tests/test_desktop_mailbox_runtime.py +++ b/tests/test_desktop_mailbox_runtime.py @@ -86,6 +86,11 @@ async def cooperative_stop(self): class RuntimeTests(DataFixture, unittest.IsolatedAsyncioTestCase): + # Whole-fixture join guard for actual SQLite/thread work, not a product + # freshness or response-latency requirement. Reuse CPython SHORT_TIMEOUT: + # https://github.com/python/cpython/blob/v3.12.10/Lib/test/support/__init__.py + control_completion_timeout_s = 30 + async def asyncSetUp(self): self.data() self.demand() @@ -135,7 +140,8 @@ async def completed_control(self, method, *, request_id): # and its authority read finish. Final-intent assertions need that task. reply = await self.request(method, request_id=request_id) self.assertTrue(reply['ok'], reply) - await asyncio.wait_for(asyncio.shield(self.transport._records[request_id]['task']), 3) + await asyncio.wait_for(asyncio.shield(self.transport._records[request_id]['task']), + self.control_completion_timeout_s) return reply def matches(self): From a83844b5fed20bf01dcfd61d4061b00961b787cc Mon Sep 17 00:00:00 2001 From: Midhat Nashar <35053211+midhatn@users.noreply.github.com> Date: Mon, 9 Nov 2026 06:34:16 +0300 Subject: [PATCH 3/5] Preserve exact historical CI fixture bytes --- .gitattributes | 2 + .../tests/test_desktop_mailbox_runtime.py | 1142 ++++++++--------- 2 files changed, 573 insertions(+), 571 deletions(-) diff --git a/.gitattributes b/.gitattributes index c87814d..c97f69c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -76,3 +76,5 @@ provenance/compiler/predecessor/src/adaptive_inference/desktop/hermes/report_con # Preserve exact compiler predecessors and video qualification records. provenance/video-tool/** -text whitespace=cr-at-eol provenance/video-tool/predecessor/src/adaptive_inference/desktop/hermes/report_contract.py whitespace=cr-at-eol,-blank-at-eof +# Exact historical CI fixture bytes include their original CRLF line endings. +provenance/ci-fixtures/** -text whitespace=cr-at-eol diff --git a/provenance/ci-fixtures/2026-11-09/predecessor/tests/test_desktop_mailbox_runtime.py b/provenance/ci-fixtures/2026-11-09/predecessor/tests/test_desktop_mailbox_runtime.py index 21c462a..5396f70 100644 --- a/provenance/ci-fixtures/2026-11-09/predecessor/tests/test_desktop_mailbox_runtime.py +++ b/provenance/ci-fixtures/2026-11-09/predecessor/tests/test_desktop_mailbox_runtime.py @@ -1,571 +1,571 @@ -"""Real SQLite/mailbox and bounded owned threads; fake streams/UI/lifecycle.""" -import asyncio -from contextlib import contextmanager -from copy import deepcopy -import json -from pathlib import Path -import tempfile -import threading -import time -import unittest -from unittest import mock - -from adaptive_inference.control_mailbox import Mailbox, ControlBlocked, ACKS -from adaptive_inference.demand import DemandIdentity, DemandRegistry, Registration, NAMESPACE -from adaptive_inference.task_store import TaskStore, TaskBusyError -from adaptive_inference.desktop.control_transport import ControlTransport, PreparedTask -from adaptive_inference.desktop.mailbox_runtime import MailboxRuntime, RuntimeUnavailable -from test_desktop_control_transport import Reader, Writer -from adaptive_inference.desktop.ui_intent import UIBinding, UIIntentGate, VerifiedUIObservation - -NOW = '2026-10-24T18:00:00Z' -OBJECTIVE = 'Keep the original task and finish the validated artifact.' - - -class DataFixture: - def data(self): - self.temp = tempfile.TemporaryDirectory(prefix='ui-mailbox-fixture-') - self.addCleanup(self.temp.cleanup) - self.store = TaskStore(Path(self.temp.name) / 'task.sqlite') - self.identity = DemandIdentity('registered', 'task', 'trusted-host', NOW) - self.store.create('task', OBJECTIVE, metadata={NAMESPACE: self.identity.metadata()}) - self.registry = DemandRegistry((Registration(self.store, self.identity),)) - self.cap = object() - self.box = Mailbox(self.registry, 'registered', requester_capability=self.cap) - - def claim(self): - return self.store.claim('task', expected_revision=self.store.load('task').revision) - - def demand(self, state='waiting'): - with self.claim() as owned: - self.registry.transition('registered', owned, state, - expected_revision=owned.checkpoint.revision, updated_at_utc=NOW) - - def submit(self, ident, command): - return self.box.submit(self.cap, ident, command) - - def consume(self): - with self.claim() as owned: - return self.box.consume(owned, updated_at_utc=NOW) - - - - -class FakeLifecycle: - """Only explicit fixture events establish simulated join; no native proof.""" - def __init__(self): - self.bindings = {'session': UIBinding('host', 'session', 'task', 101, 202, 303)} - self.live = {'session'} - self.fenced = False - self.join_entered = asyncio.Event() - self.join = asyncio.Event() - self.join.set() - self.stop_calls = 0 - self.result = {'worker_joined': True, 'host_closed': True, 'uncertain': False, 'fault': None} - - def session_is_live(self, session): - return session in self.live - - def window_ready(self, session): - return self.bindings.get(session) - - def window_closed(self, session): - self.live.discard(session) - - def status(self): - return {'host_phase': 'ready', 'waiting_reason': None} - - def fence_admission(self): - self.fenced = True - - async def cooperative_stop(self): - self.stop_calls += 1 - self.join_entered.set() - await self.join.wait() - return deepcopy(self.result) - - -class RuntimeTests(DataFixture, unittest.IsolatedAsyncioTestCase): - async def asyncSetUp(self): - self.data() - self.demand() - self.intent = object() - self.gate = UIIntentGate(host_epoch='host', capability=self.intent, - verify_current=lambda binding: VerifiedUIObservation(binding, time.monotonic(), True, True)) - self.host = FakeLifecycle() - self.prepared = PreparedTask('host', 'task', OBJECTIVE) - # Observation age is a logical fixture input; real SQLite setup speed - # does not establish expiry. The freshness tests advance it explicitly. - self.observation_time = 20. - self.runtime = MailboxRuntime(self.prepared, self.box, requester_capability=self.cap, - gate=self.gate, intent_capability=self.intent, lifecycle=self.host, max_age_s=3, - clock=lambda: self.observation_time) - self.transport_cap = object() - self.transport = ControlTransport(self.prepared, self.runtime, capability=self.transport_cap, - runner_credential='b' * 64, io_timeout=.25) - self.transport.bind_session(self.transport_cap, 'session', 'a' * 64) - self.releases = [] - await self.runtime.refresh() - self.assertTrue((await self.request('window_ready'))['ok']) - - async def asyncTearDown(self): - for event in self.releases: - event.set() - self.host.join.set() - await self.transport.aclose(timeout=3) - await self.runtime.cooperative_stop() - - def frame(self, method, **changes): - value = {'host_epoch': 'host', 'task_id': 'task', 'session_id': 'session', - 'token': 'a' * 64, 'method': method} - if method in ('pause', 'resume', 'cancel'): - value['request_id'] = 'one' - value.update(self.runtime.selected_execution()) - value.update(changes) - return (json.dumps(value) + '\n').encode() - - async def request(self, method, **changes): - writer = Writer() - task = self.transport.admit(Reader(self.frame(method, **changes)), writer) - await task - return json.loads(writer.data) - - async def completed_control(self, method, *, request_id): - # A handler may truthfully return pending before the retained mutation - # and its authority read finish. Final-intent assertions need that task. - reply = await self.request(method, request_id=request_id) - self.assertTrue(reply['ok'], reply) - await asyncio.wait_for(asyncio.shield(self.transport._records[request_id]['task']), 3) - return reply - - def matches(self): - now = time.monotonic() - return self.gate.matches({'monotonic_s': now, 'foreground': { - 'valid': True, 'hwnd': 101, 'pid': 202, 'creation_time_100ns': 303}}, now) - - async def signal(self, event): - # Bounded waiting uses the actual threading event, not a timing premise. - self.assertTrue(await asyncio.to_thread(event.wait, 2)) - - def block_observation(self): - entered, release = threading.Event(), threading.Event() - self.releases.append(release) - actual = self.box.snapshot - - def blocked(): - entered.set() - if not release.wait(3): - raise AssertionError('Fixture snapshot release timed out') - return actual() - return entered, release, blocked - - async def test_real_latest_resume_authority_pending_and_applied(self): - self.demand('paused') - value = await self.runtime.submit_control('new-resume', 'resume') - self.assertEqual(value['outcome'], 'pending') - self.assertFalse(value['owner_acknowledged']) - self.assertIs(await self.runtime.resume_authority('new-resume'), True) - self.consume() - self.assertIs(await self.runtime.resume_authority('new-resume'), True) - self.assertTrue(self.runtime.status()['controls'][0]['owner_acknowledged']) - - async def test_historical_applied_resume_followed_by_pause_never_activates(self): - self.submit('old-resume', 'resume'); self.consume() - self.submit('later-pause', 'pause'); self.consume() - history = self.store.history(self.box.task_id) - value = await self.completed_control('resume', request_id='old-resume') - self.assertTrue(value['ok']) - self.assertFalse(self.matches()) - self.assertEqual(history, self.store.history(self.box.task_id)) - self.assertIs(await self.runtime.resume_authority('old-resume'), False) - - async def test_new_resume_activates_real_gate_then_pause_revokes(self): - result = await self.completed_control('resume', request_id='fresh') - self.assertTrue(result['ok']) - self.assertTrue(self.matches()) - await self.completed_control('pause', request_id='pause') - self.assertFalse(self.matches()) - self.assertEqual(self.store.load('task').prompt, OBJECTIVE) - self.assertEqual(self.runtime.status()['controls'][0]['outcome'], 'pending') - - async def test_missing_demand_refuses_authority(self): - with self.claim() as owned: - state = deepcopy(owned.checkpoint.state) - state.pop(NAMESPACE) - owned.save_checkpoint(state, expected_revision=owned.checkpoint.revision) - await self.runtime.submit_control('resume', 'resume') - self.assertIs(await self.runtime.resume_authority('resume'), False) - - async def test_inflight_and_interrupted_replacement_are_never_authority(self): - self.submit('resume', 'resume') - with self.claim() as owned: - owned.begin_operation('effect', details={'fixture': 'not executed'}, - expected_revision=owned.checkpoint.revision) - self.assertIs(await self.runtime.resume_authority('resume'), False) - self.assertEqual(self.runtime.status()['waiting_reason'], 'operation_inflight') - with self.claim() as replacement: - self.assertTrue(replacement.checkpoint.requires_reconciliation) - self.assertIs(await self.runtime.resume_authority('resume'), False) - self.assertEqual(self.runtime.status()['waiting_reason'], 'reconciliation') - - async def test_terminal_target_revokes_intent_and_refuses_authority(self): - await self.completed_control('resume', request_id='resume') - self.assertTrue(self.matches()) - with self.claim() as owned: - state = deepcopy(owned.checkpoint.state) - state.update(status='accepted', output='Validated fixture result') - owned.save_checkpoint(state, expected_revision=owned.checkpoint.revision) - self.assertIs(await self.runtime.resume_authority('resume'), False) - self.assertFalse(self.matches()) - - async def test_changed_objective_is_refused_without_claiming_target(self): - self.runtime.prepared = PreparedTask('host', 'task', 'Different objective') - with self.assertRaises(RuntimeUnavailable): - await self.runtime.refresh() - self.assertIsNone(self.store.load('task').owner_epoch) - - async def test_mismatched_objective_refuses_before_any_new_control_commit(self): - self.runtime.prepared = PreparedTask('host', 'task', 'Different objective') - before = self.store.history(self.box.task_id) - with self.assertRaises(RuntimeUnavailable): - await self.runtime.submit_control('must-not-record', 'cancel') - self.assertEqual(before, self.store.history(self.box.task_id)) - - async def test_wrong_intent_capability_is_rejected_before_async_work(self): - with self.assertRaises(ValueError): - MailboxRuntime(self.prepared, self.box, requester_capability=self.cap, - gate=self.gate, intent_capability=object(), lifecycle=self.host) - - async def test_refresh_revokes_external_later_pause_even_before_owner_ack(self): - await self.completed_control('resume', request_id='resume') - self.assertTrue(self.matches()) - self.consume() - self.submit('external-pause', 'pause') - await self.runtime.refresh() - self.assertFalse(self.matches()) - self.assertEqual(self.runtime.status()['controls'][-1]['outcome'], 'pending') - self.consume() - await self.runtime.refresh() - self.assertEqual(self.runtime.status()['demand'], 'paused') - self.assertFalse(self.matches()) - - async def test_latest_pending_resume_can_keep_intent_while_owner_is_paused(self): - self.demand('paused') - # The explicit SQLite setup mutation may outlive the cached observation. - # Establish fresh admission before testing retained Resume intent. - await self.runtime.refresh() - await self.completed_control('resume', request_id='resume') - self.assertTrue(self.matches()) - await self.runtime.refresh() - self.assertEqual(self.runtime.status()['demand'], 'paused') - self.assertTrue(self.matches()) - - async def test_pending_reply_precedes_retained_resume_authority_completion(self): - entered, release, blocked = self.block_observation() - authority_entered = asyncio.Event() - actual_authority = self.runtime.resume_authority - actual_request = self.request - responded = asyncio.get_running_loop().create_future() - async def held_authority(request_id,**kwargs): - authority_entered.set() - with mock.patch.object(self.box, 'snapshot', side_effect=blocked): - return await actual_authority(request_id,**kwargs) - async def observed_request(*args, **kwargs): - reply = await actual_request(*args, **kwargs) - responded.set_result(reply) - return reply - with mock.patch.object(self.runtime, 'resume_authority', side_effect=held_authority), \ - mock.patch.object(self, 'request', side_effect=observed_request): - completed = asyncio.create_task(self.completed_control('resume', request_id='resume')) - try: - # Establish authority entry after SQLite submission before the - # unchanged two-second held-snapshot entry check. - await asyncio.wait_for(authority_entered.wait(), 30) - await self.signal(entered) - operation = self.transport._records['resume']['task'] - # Observe the actual response before releasing authority; no sleep - # or assumption about SQLite/CI scheduling establishes this order. - reply = await asyncio.wait_for(asyncio.shield(responded), 2) - self.assertEqual(reply, {'ok': True, **self.runtime.selected_execution(), 'state': 'pending', 'request_id': 'resume', 'recorded': None}) - self.assertFalse(operation.done()) - self.assertFalse(completed.done()) - self.assertFalse(self.matches()) - finally: - release.set() - # Settle the extra fixture task even if an assertion failed; - # shield keeps its retained control operation uncancelled. - await asyncio.wait_for(asyncio.shield( - asyncio.gather(completed, return_exceptions=True)), 3) - self.assertEqual(completed.result(), reply) - self.assertTrue(operation.done()) - self.assertTrue(self.matches()) - self.assertEqual(len(self.box.snapshot().requests), 1) - - async def test_crash_gap_pending_resume_does_not_invent_ack_or_reapply(self): - self.demand('paused') - self.submit('resume', 'resume') - with self.claim() as owned: - real_save = owned.save_checkpoint - def fail_ack(state, **kwargs): - if ACKS in state: - raise OSError('Fixture failed ACK commit') - return real_save(state, **kwargs) - with mock.patch.object(owned, 'save_checkpoint', side_effect=fail_ack): - with self.assertRaises(OSError): - self.box.consume(owned, updated_at_utc=NOW) - revision = owned.checkpoint.revision - self.assertIs(await self.runtime.resume_authority('resume'), True) - self.assertEqual(self.runtime.status()['controls'][0]['outcome'], 'pending') - self.assertEqual(owned.checkpoint.revision, revision) - with self.claim() as replacement: - before = replacement.checkpoint.revision - self.box.consume(replacement, updated_at_utc=NOW) - self.assertEqual(replacement.checkpoint.revision, before + 1) - await self.runtime.refresh() - self.assertEqual(self.runtime.status()['controls'][0]['outcome'], 'applied') - - async def test_cancelled_refresh_retains_thread_and_stop_waits_for_it(self): - entered, release, blocked = self.block_observation() - with mock.patch.object(self.box, 'snapshot', side_effect=blocked): - waiter = asyncio.create_task(self.runtime.refresh()) - await self.signal(entered) - waiter.cancel() - with self.assertRaises(asyncio.CancelledError): - await waiter - self.assertEqual(self.runtime.diagnostics()['pending_database'], 1) - stopping = asyncio.create_task(self.runtime.cooperative_stop()) - await asyncio.sleep(0) - self.assertFalse(self.host.join_entered.is_set()) - self.assertFalse(stopping.done()) - release.set() - result = await asyncio.wait_for(stopping, 3) - self.assertTrue(result['worker_joined'] and result['host_closed']) - self.assertEqual(self.runtime.diagnostics()['pending_database'], 0) - - async def test_cancelled_submit_commits_once_then_explicit_retry_observes_it(self): - entered, release = threading.Event(), threading.Event() - self.releases.append(release) - actual = self.box.submit - def blocked(*args): - entered.set() - if not release.wait(3): - raise AssertionError('Fixture submit timed out') - return actual(*args) - with mock.patch.object(self.box, 'submit', side_effect=blocked): - waiter = asyncio.create_task(self.runtime.submit_control('stable', 'pause')) - await self.signal(entered) - waiter.cancel() - with self.assertRaises(asyncio.CancelledError): - await waiter - self.assertEqual(self.runtime.diagnostics()['pending_database'], 1) - release.set() - await self.runtime.refresh() - before = self.store.history(self.box.task_id) - result = await self.runtime.submit_control('stable', 'pause') - self.assertEqual(result['outcome'], 'pending') - self.assertEqual(before, self.store.history(self.box.task_id)) - - async def test_disconnected_transport_keeps_real_commit_and_stop_join_obligation(self): - entered, release = threading.Event(), threading.Event() - self.releases.append(release) - actual = self.box.submit - def hold_after_commit(*args): - value = actual(*args) - entered.set() - if not release.wait(3): - raise AssertionError('Fixture postcommit wait exceeded bound') - return value - with mock.patch.object(self.box, 'submit', side_effect=hold_after_commit): - writer = Writer(broken=True) - handler = self.transport.admit(Reader(self.frame('pause', request_id='durable')), writer) - await self.signal(entered) - handler.cancel() - await asyncio.gather(handler, return_exceptions=True) - pending = await self.transport.aclose(timeout=.02) - self.assertFalse(pending['closed']) - self.assertFalse(self.host.join_entered.is_set()) - self.assertEqual(self.runtime.diagnostics()['pending_database'], 1) - self.assertEqual(self.box.status('durable')['outcome'], 'pending') - release.set() - final = await self.transport.aclose(timeout=3) - self.assertTrue(final['closed']) - self.assertTrue(writer.closed and writer.joined) - self.assertEqual(len(self.box.snapshot().requests), 1) - - async def test_capacity_remains_bound_while_cancelled_waiter_thread_runs(self): - self.runtime.max_pending = 1 - entered, release, blocked = self.block_observation() - with mock.patch.object(self.box, 'snapshot', side_effect=blocked): - waiter = asyncio.create_task(self.runtime.refresh()) - await self.signal(entered) - waiter.cancel() - await asyncio.gather(waiter, return_exceptions=True) - with self.assertRaises(RuntimeUnavailable): - await self.runtime.refresh() - release.set() - await self.runtime.cooperative_stop() - - async def test_status_cache_expires_and_does_not_do_database_io(self): - self.runtime.max_age_s = .01 - self.observation_time += .02 - with mock.patch.object(self.box, 'snapshot', side_effect=AssertionError('Blocking status read')): - with self.assertRaises(RuntimeUnavailable): - self.runtime.status() - self.runtime.max_age_s = 3 - await self.runtime.refresh() - - async def test_slow_snapshot_uses_start_time_not_completion_for_freshness(self): - self.runtime.max_age_s = .02 - entered, release, blocked = self.block_observation() - with mock.patch.object(self.box, 'snapshot', side_effect=blocked): - waiter = asyncio.create_task(self.runtime.refresh()) - await self.signal(entered) - self.observation_time += .03 - release.set() - with self.assertRaises(RuntimeUnavailable): - await waiter - - async def test_failed_snapshot_does_not_turn_into_successful_cleanup(self): - with mock.patch.object(self.box, 'snapshot', side_effect=OSError('Private fixture path')): - with self.assertRaises(OSError): - await self.runtime.resume_authority('one') - receipt = await self.runtime.cooperative_stop() - self.assertTrue(receipt['worker_joined'] and receipt['host_closed']) - self.assertEqual(receipt['fault'], 'runtime_operation_failed') - self.assertNotIn('Private fixture path', str(self.runtime.diagnostics())) - - async def test_prior_runtime_fault_cannot_be_hidden_by_later_resume_observation(self): - self.submit('resume', 'resume') - with mock.patch.object(self.box, 'snapshot', side_effect=OSError('Fixture read failed')): - with self.assertRaises(OSError): - await self.runtime.refresh() - self.assertIs(await self.runtime.resume_authority('resume'), False) - self.assertEqual(self.runtime.status()['host_phase'], 'fault') - self.assertFalse(self.matches()) - - async def test_postcommit_read_failure_retry_observes_commit_but_does_not_clear_fault(self): - actual_snapshot = self.box.snapshot - count = 0 - def fail_second_read(): - nonlocal count - count += 1 - if count == 2: - raise OSError('Fixture observation lost after actual commit') - return actual_snapshot() - with mock.patch.object(self.box, 'snapshot', side_effect=fail_second_read): - with self.assertRaises(OSError): - await self.runtime.submit_control('resume', 'resume') - before = self.store.history(self.box.task_id) - self.assertEqual(self.box.status('resume')['outcome'], 'pending') - retried = await self.runtime.submit_control('resume', 'resume') - self.assertEqual(retried['outcome'], 'pending') - self.assertEqual(before, self.store.history(self.box.task_id)) - self.assertIs(await self.runtime.resume_authority('resume'), False) - stopped = await self.runtime.cooperative_stop() - self.assertTrue(stopped['worker_joined'] and stopped['host_closed']) - self.assertEqual(stopped['fault'], 'runtime_operation_failed') - - async def test_malformed_real_ack_prefix_never_becomes_resume_authority(self): - self.submit('resume', 'resume'); self.consume() - with self.claim() as owned: - state = deepcopy(owned.checkpoint.state) - state[ACKS]['receipts'][0]['request_id'] = 'different' - owned.save_checkpoint(state, expected_revision=owned.checkpoint.revision) - with self.assertRaises(ControlBlocked): - await self.runtime.resume_authority('resume') - self.assertFalse(self.matches()) - - async def test_failed_host_fence_remains_visible_even_if_worker_later_joins(self): - with mock.patch.object(self.host, 'fence_admission', side_effect=RuntimeError('Fixture fence failure')): - with self.assertRaises(RuntimeError): - self.runtime.fence_admission() - receipt = await self.runtime.cooperative_stop() - self.assertTrue(receipt['worker_joined'] and receipt['host_closed']) - self.assertEqual(receipt['fault'], 'runtime_operation_failed') - - async def test_stop_waiter_cancellation_preserves_worker_join_and_host_fault(self): - self.host.join.clear() - self.host.result['fault'] = 'fixture_host_fault' - waiter = asyncio.create_task(self.runtime.cooperative_stop()) - await self.host.join_entered.wait() - waiter.cancel() - await asyncio.gather(waiter, return_exceptions=True) - self.assertFalse(self.runtime._stop_task.done()) - self.host.join.set() - result = await self.runtime.cooperative_stop() - self.assertEqual(result['fault'], 'fixture_host_fault') - self.assertEqual(self.host.stop_calls, 1) - - async def test_pause_during_real_authority_read_prevents_late_activation(self): - entered, release, blocked = self.block_observation() - authority_entered = asyncio.Event() - actual_authority = self.runtime.resume_authority - async def held_authority(request_id,**kwargs): - authority_entered.set() - with mock.patch.object(self.box, 'snapshot', side_effect=blocked): - return await actual_authority(request_id,**kwargs) - with mock.patch.object(self.runtime, 'resume_authority', side_effect=held_authority): - writer = Writer() - first = self.transport.admit(Reader(self.frame('resume', request_id='resume')), writer) - # As in the rotation test, first establish SQLite submission, then - # retain the unchanged two-second bound on the held thread entry. - try: - await asyncio.wait_for(authority_entered.wait(), 30) - except TimeoutError: - operation = self.transport._records.get('resume', {}).get('task') - error = (type(operation.exception()).__name__ if operation is not None - and operation.done() and not operation.cancelled() else None) - reply = json.loads(writer.data) if writer.data else None - self.fail(f'Authority setup did not complete: reply={reply!r}, error={error}') - await self.signal(entered) - pause = await self.request('pause', request_id='pause') - self.assertTrue(pause['ok']) - self.assertFalse(self.matches()) - release.set() - await first - await self.transport._records['pause']['task'] - self.assertFalse(self.matches()) - - async def test_rotation_during_real_authority_read_prevents_late_activation(self): - entered, release, blocked = self.block_observation() - authority_entered = asyncio.Event() - actual_authority = self.runtime.resume_authority - async def held_authority(request_id,**kwargs): - authority_entered.set() - with mock.patch.object(self.box, 'snapshot', side_effect=blocked): - return await actual_authority(request_id,**kwargs) - with mock.patch.object(self.runtime, 'resume_authority', side_effect=held_authority): - writer = Writer() - first = self.transport.admit(Reader(self.frame('resume', request_id='resume')), writer) - # SQLite submission precedes the held read. Establish that setup - # explicitly before the unchanged two-second thread-entry check. - try: - await asyncio.wait_for(authority_entered.wait(), 30) - except TimeoutError: - operation = self.transport._records.get('resume', {}).get('task') - error = (type(operation.exception()).__name__ if operation is not None - and operation.done() and not operation.cancelled() else None) - reply = json.loads(writer.data) if writer.data else None - self.fail(f'Authority setup did not complete: reply={reply!r}, error={error}') - await self.signal(entered) - self.transport.bind_session(self.transport_cap, 'new-session', 'c' * 64) - self.host.live.add('new-session') - self.host.bindings['new-session'] = UIBinding('host', 'new-session', 'task', 404, 505, 606) - self.assertTrue((await self.request('window_ready', session_id='new-session', token='c' * 64))['ok']) - release.set() - await first - await asyncio.wait_for(asyncio.shield(self.transport._records['resume']['task']), 3) - self.assertFalse(self.matches()) - - async def test_projection_keeps_only_bounded_fields_and_objective(self): - await self.request('pause', request_id='pause') - value = await self.request('status') - text = json.dumps(value) - self.assertTrue(value['ok']) - self.assertEqual(value['status']['objective'], OBJECTIVE) - for forbidden in ('owner_epoch', 'metadata', str(self.store.path), 'a' * 64, 'b' * 64): - self.assertNotIn(forbidden, text) - - -if __name__ == '__main__': - unittest.main(verbosity=2) +"""Real SQLite/mailbox and bounded owned threads; fake streams/UI/lifecycle.""" +import asyncio +from contextlib import contextmanager +from copy import deepcopy +import json +from pathlib import Path +import tempfile +import threading +import time +import unittest +from unittest import mock + +from adaptive_inference.control_mailbox import Mailbox, ControlBlocked, ACKS +from adaptive_inference.demand import DemandIdentity, DemandRegistry, Registration, NAMESPACE +from adaptive_inference.task_store import TaskStore, TaskBusyError +from adaptive_inference.desktop.control_transport import ControlTransport, PreparedTask +from adaptive_inference.desktop.mailbox_runtime import MailboxRuntime, RuntimeUnavailable +from test_desktop_control_transport import Reader, Writer +from adaptive_inference.desktop.ui_intent import UIBinding, UIIntentGate, VerifiedUIObservation + +NOW = '2026-10-24T18:00:00Z' +OBJECTIVE = 'Keep the original task and finish the validated artifact.' + + +class DataFixture: + def data(self): + self.temp = tempfile.TemporaryDirectory(prefix='ui-mailbox-fixture-') + self.addCleanup(self.temp.cleanup) + self.store = TaskStore(Path(self.temp.name) / 'task.sqlite') + self.identity = DemandIdentity('registered', 'task', 'trusted-host', NOW) + self.store.create('task', OBJECTIVE, metadata={NAMESPACE: self.identity.metadata()}) + self.registry = DemandRegistry((Registration(self.store, self.identity),)) + self.cap = object() + self.box = Mailbox(self.registry, 'registered', requester_capability=self.cap) + + def claim(self): + return self.store.claim('task', expected_revision=self.store.load('task').revision) + + def demand(self, state='waiting'): + with self.claim() as owned: + self.registry.transition('registered', owned, state, + expected_revision=owned.checkpoint.revision, updated_at_utc=NOW) + + def submit(self, ident, command): + return self.box.submit(self.cap, ident, command) + + def consume(self): + with self.claim() as owned: + return self.box.consume(owned, updated_at_utc=NOW) + + + + +class FakeLifecycle: + """Only explicit fixture events establish simulated join; no native proof.""" + def __init__(self): + self.bindings = {'session': UIBinding('host', 'session', 'task', 101, 202, 303)} + self.live = {'session'} + self.fenced = False + self.join_entered = asyncio.Event() + self.join = asyncio.Event() + self.join.set() + self.stop_calls = 0 + self.result = {'worker_joined': True, 'host_closed': True, 'uncertain': False, 'fault': None} + + def session_is_live(self, session): + return session in self.live + + def window_ready(self, session): + return self.bindings.get(session) + + def window_closed(self, session): + self.live.discard(session) + + def status(self): + return {'host_phase': 'ready', 'waiting_reason': None} + + def fence_admission(self): + self.fenced = True + + async def cooperative_stop(self): + self.stop_calls += 1 + self.join_entered.set() + await self.join.wait() + return deepcopy(self.result) + + +class RuntimeTests(DataFixture, unittest.IsolatedAsyncioTestCase): + async def asyncSetUp(self): + self.data() + self.demand() + self.intent = object() + self.gate = UIIntentGate(host_epoch='host', capability=self.intent, + verify_current=lambda binding: VerifiedUIObservation(binding, time.monotonic(), True, True)) + self.host = FakeLifecycle() + self.prepared = PreparedTask('host', 'task', OBJECTIVE) + # Observation age is a logical fixture input; real SQLite setup speed + # does not establish expiry. The freshness tests advance it explicitly. + self.observation_time = 20. + self.runtime = MailboxRuntime(self.prepared, self.box, requester_capability=self.cap, + gate=self.gate, intent_capability=self.intent, lifecycle=self.host, max_age_s=3, + clock=lambda: self.observation_time) + self.transport_cap = object() + self.transport = ControlTransport(self.prepared, self.runtime, capability=self.transport_cap, + runner_credential='b' * 64, io_timeout=.25) + self.transport.bind_session(self.transport_cap, 'session', 'a' * 64) + self.releases = [] + await self.runtime.refresh() + self.assertTrue((await self.request('window_ready'))['ok']) + + async def asyncTearDown(self): + for event in self.releases: + event.set() + self.host.join.set() + await self.transport.aclose(timeout=3) + await self.runtime.cooperative_stop() + + def frame(self, method, **changes): + value = {'host_epoch': 'host', 'task_id': 'task', 'session_id': 'session', + 'token': 'a' * 64, 'method': method} + if method in ('pause', 'resume', 'cancel'): + value['request_id'] = 'one' + value.update(self.runtime.selected_execution()) + value.update(changes) + return (json.dumps(value) + '\n').encode() + + async def request(self, method, **changes): + writer = Writer() + task = self.transport.admit(Reader(self.frame(method, **changes)), writer) + await task + return json.loads(writer.data) + + async def completed_control(self, method, *, request_id): + # A handler may truthfully return pending before the retained mutation + # and its authority read finish. Final-intent assertions need that task. + reply = await self.request(method, request_id=request_id) + self.assertTrue(reply['ok'], reply) + await asyncio.wait_for(asyncio.shield(self.transport._records[request_id]['task']), 3) + return reply + + def matches(self): + now = time.monotonic() + return self.gate.matches({'monotonic_s': now, 'foreground': { + 'valid': True, 'hwnd': 101, 'pid': 202, 'creation_time_100ns': 303}}, now) + + async def signal(self, event): + # Bounded waiting uses the actual threading event, not a timing premise. + self.assertTrue(await asyncio.to_thread(event.wait, 2)) + + def block_observation(self): + entered, release = threading.Event(), threading.Event() + self.releases.append(release) + actual = self.box.snapshot + + def blocked(): + entered.set() + if not release.wait(3): + raise AssertionError('Fixture snapshot release timed out') + return actual() + return entered, release, blocked + + async def test_real_latest_resume_authority_pending_and_applied(self): + self.demand('paused') + value = await self.runtime.submit_control('new-resume', 'resume') + self.assertEqual(value['outcome'], 'pending') + self.assertFalse(value['owner_acknowledged']) + self.assertIs(await self.runtime.resume_authority('new-resume'), True) + self.consume() + self.assertIs(await self.runtime.resume_authority('new-resume'), True) + self.assertTrue(self.runtime.status()['controls'][0]['owner_acknowledged']) + + async def test_historical_applied_resume_followed_by_pause_never_activates(self): + self.submit('old-resume', 'resume'); self.consume() + self.submit('later-pause', 'pause'); self.consume() + history = self.store.history(self.box.task_id) + value = await self.completed_control('resume', request_id='old-resume') + self.assertTrue(value['ok']) + self.assertFalse(self.matches()) + self.assertEqual(history, self.store.history(self.box.task_id)) + self.assertIs(await self.runtime.resume_authority('old-resume'), False) + + async def test_new_resume_activates_real_gate_then_pause_revokes(self): + result = await self.completed_control('resume', request_id='fresh') + self.assertTrue(result['ok']) + self.assertTrue(self.matches()) + await self.completed_control('pause', request_id='pause') + self.assertFalse(self.matches()) + self.assertEqual(self.store.load('task').prompt, OBJECTIVE) + self.assertEqual(self.runtime.status()['controls'][0]['outcome'], 'pending') + + async def test_missing_demand_refuses_authority(self): + with self.claim() as owned: + state = deepcopy(owned.checkpoint.state) + state.pop(NAMESPACE) + owned.save_checkpoint(state, expected_revision=owned.checkpoint.revision) + await self.runtime.submit_control('resume', 'resume') + self.assertIs(await self.runtime.resume_authority('resume'), False) + + async def test_inflight_and_interrupted_replacement_are_never_authority(self): + self.submit('resume', 'resume') + with self.claim() as owned: + owned.begin_operation('effect', details={'fixture': 'not executed'}, + expected_revision=owned.checkpoint.revision) + self.assertIs(await self.runtime.resume_authority('resume'), False) + self.assertEqual(self.runtime.status()['waiting_reason'], 'operation_inflight') + with self.claim() as replacement: + self.assertTrue(replacement.checkpoint.requires_reconciliation) + self.assertIs(await self.runtime.resume_authority('resume'), False) + self.assertEqual(self.runtime.status()['waiting_reason'], 'reconciliation') + + async def test_terminal_target_revokes_intent_and_refuses_authority(self): + await self.completed_control('resume', request_id='resume') + self.assertTrue(self.matches()) + with self.claim() as owned: + state = deepcopy(owned.checkpoint.state) + state.update(status='accepted', output='Validated fixture result') + owned.save_checkpoint(state, expected_revision=owned.checkpoint.revision) + self.assertIs(await self.runtime.resume_authority('resume'), False) + self.assertFalse(self.matches()) + + async def test_changed_objective_is_refused_without_claiming_target(self): + self.runtime.prepared = PreparedTask('host', 'task', 'Different objective') + with self.assertRaises(RuntimeUnavailable): + await self.runtime.refresh() + self.assertIsNone(self.store.load('task').owner_epoch) + + async def test_mismatched_objective_refuses_before_any_new_control_commit(self): + self.runtime.prepared = PreparedTask('host', 'task', 'Different objective') + before = self.store.history(self.box.task_id) + with self.assertRaises(RuntimeUnavailable): + await self.runtime.submit_control('must-not-record', 'cancel') + self.assertEqual(before, self.store.history(self.box.task_id)) + + async def test_wrong_intent_capability_is_rejected_before_async_work(self): + with self.assertRaises(ValueError): + MailboxRuntime(self.prepared, self.box, requester_capability=self.cap, + gate=self.gate, intent_capability=object(), lifecycle=self.host) + + async def test_refresh_revokes_external_later_pause_even_before_owner_ack(self): + await self.completed_control('resume', request_id='resume') + self.assertTrue(self.matches()) + self.consume() + self.submit('external-pause', 'pause') + await self.runtime.refresh() + self.assertFalse(self.matches()) + self.assertEqual(self.runtime.status()['controls'][-1]['outcome'], 'pending') + self.consume() + await self.runtime.refresh() + self.assertEqual(self.runtime.status()['demand'], 'paused') + self.assertFalse(self.matches()) + + async def test_latest_pending_resume_can_keep_intent_while_owner_is_paused(self): + self.demand('paused') + # The explicit SQLite setup mutation may outlive the cached observation. + # Establish fresh admission before testing retained Resume intent. + await self.runtime.refresh() + await self.completed_control('resume', request_id='resume') + self.assertTrue(self.matches()) + await self.runtime.refresh() + self.assertEqual(self.runtime.status()['demand'], 'paused') + self.assertTrue(self.matches()) + + async def test_pending_reply_precedes_retained_resume_authority_completion(self): + entered, release, blocked = self.block_observation() + authority_entered = asyncio.Event() + actual_authority = self.runtime.resume_authority + actual_request = self.request + responded = asyncio.get_running_loop().create_future() + async def held_authority(request_id,**kwargs): + authority_entered.set() + with mock.patch.object(self.box, 'snapshot', side_effect=blocked): + return await actual_authority(request_id,**kwargs) + async def observed_request(*args, **kwargs): + reply = await actual_request(*args, **kwargs) + responded.set_result(reply) + return reply + with mock.patch.object(self.runtime, 'resume_authority', side_effect=held_authority), \ + mock.patch.object(self, 'request', side_effect=observed_request): + completed = asyncio.create_task(self.completed_control('resume', request_id='resume')) + try: + # Establish authority entry after SQLite submission before the + # unchanged two-second held-snapshot entry check. + await asyncio.wait_for(authority_entered.wait(), 30) + await self.signal(entered) + operation = self.transport._records['resume']['task'] + # Observe the actual response before releasing authority; no sleep + # or assumption about SQLite/CI scheduling establishes this order. + reply = await asyncio.wait_for(asyncio.shield(responded), 2) + self.assertEqual(reply, {'ok': True, **self.runtime.selected_execution(), 'state': 'pending', 'request_id': 'resume', 'recorded': None}) + self.assertFalse(operation.done()) + self.assertFalse(completed.done()) + self.assertFalse(self.matches()) + finally: + release.set() + # Settle the extra fixture task even if an assertion failed; + # shield keeps its retained control operation uncancelled. + await asyncio.wait_for(asyncio.shield( + asyncio.gather(completed, return_exceptions=True)), 3) + self.assertEqual(completed.result(), reply) + self.assertTrue(operation.done()) + self.assertTrue(self.matches()) + self.assertEqual(len(self.box.snapshot().requests), 1) + + async def test_crash_gap_pending_resume_does_not_invent_ack_or_reapply(self): + self.demand('paused') + self.submit('resume', 'resume') + with self.claim() as owned: + real_save = owned.save_checkpoint + def fail_ack(state, **kwargs): + if ACKS in state: + raise OSError('Fixture failed ACK commit') + return real_save(state, **kwargs) + with mock.patch.object(owned, 'save_checkpoint', side_effect=fail_ack): + with self.assertRaises(OSError): + self.box.consume(owned, updated_at_utc=NOW) + revision = owned.checkpoint.revision + self.assertIs(await self.runtime.resume_authority('resume'), True) + self.assertEqual(self.runtime.status()['controls'][0]['outcome'], 'pending') + self.assertEqual(owned.checkpoint.revision, revision) + with self.claim() as replacement: + before = replacement.checkpoint.revision + self.box.consume(replacement, updated_at_utc=NOW) + self.assertEqual(replacement.checkpoint.revision, before + 1) + await self.runtime.refresh() + self.assertEqual(self.runtime.status()['controls'][0]['outcome'], 'applied') + + async def test_cancelled_refresh_retains_thread_and_stop_waits_for_it(self): + entered, release, blocked = self.block_observation() + with mock.patch.object(self.box, 'snapshot', side_effect=blocked): + waiter = asyncio.create_task(self.runtime.refresh()) + await self.signal(entered) + waiter.cancel() + with self.assertRaises(asyncio.CancelledError): + await waiter + self.assertEqual(self.runtime.diagnostics()['pending_database'], 1) + stopping = asyncio.create_task(self.runtime.cooperative_stop()) + await asyncio.sleep(0) + self.assertFalse(self.host.join_entered.is_set()) + self.assertFalse(stopping.done()) + release.set() + result = await asyncio.wait_for(stopping, 3) + self.assertTrue(result['worker_joined'] and result['host_closed']) + self.assertEqual(self.runtime.diagnostics()['pending_database'], 0) + + async def test_cancelled_submit_commits_once_then_explicit_retry_observes_it(self): + entered, release = threading.Event(), threading.Event() + self.releases.append(release) + actual = self.box.submit + def blocked(*args): + entered.set() + if not release.wait(3): + raise AssertionError('Fixture submit timed out') + return actual(*args) + with mock.patch.object(self.box, 'submit', side_effect=blocked): + waiter = asyncio.create_task(self.runtime.submit_control('stable', 'pause')) + await self.signal(entered) + waiter.cancel() + with self.assertRaises(asyncio.CancelledError): + await waiter + self.assertEqual(self.runtime.diagnostics()['pending_database'], 1) + release.set() + await self.runtime.refresh() + before = self.store.history(self.box.task_id) + result = await self.runtime.submit_control('stable', 'pause') + self.assertEqual(result['outcome'], 'pending') + self.assertEqual(before, self.store.history(self.box.task_id)) + + async def test_disconnected_transport_keeps_real_commit_and_stop_join_obligation(self): + entered, release = threading.Event(), threading.Event() + self.releases.append(release) + actual = self.box.submit + def hold_after_commit(*args): + value = actual(*args) + entered.set() + if not release.wait(3): + raise AssertionError('Fixture postcommit wait exceeded bound') + return value + with mock.patch.object(self.box, 'submit', side_effect=hold_after_commit): + writer = Writer(broken=True) + handler = self.transport.admit(Reader(self.frame('pause', request_id='durable')), writer) + await self.signal(entered) + handler.cancel() + await asyncio.gather(handler, return_exceptions=True) + pending = await self.transport.aclose(timeout=.02) + self.assertFalse(pending['closed']) + self.assertFalse(self.host.join_entered.is_set()) + self.assertEqual(self.runtime.diagnostics()['pending_database'], 1) + self.assertEqual(self.box.status('durable')['outcome'], 'pending') + release.set() + final = await self.transport.aclose(timeout=3) + self.assertTrue(final['closed']) + self.assertTrue(writer.closed and writer.joined) + self.assertEqual(len(self.box.snapshot().requests), 1) + + async def test_capacity_remains_bound_while_cancelled_waiter_thread_runs(self): + self.runtime.max_pending = 1 + entered, release, blocked = self.block_observation() + with mock.patch.object(self.box, 'snapshot', side_effect=blocked): + waiter = asyncio.create_task(self.runtime.refresh()) + await self.signal(entered) + waiter.cancel() + await asyncio.gather(waiter, return_exceptions=True) + with self.assertRaises(RuntimeUnavailable): + await self.runtime.refresh() + release.set() + await self.runtime.cooperative_stop() + + async def test_status_cache_expires_and_does_not_do_database_io(self): + self.runtime.max_age_s = .01 + self.observation_time += .02 + with mock.patch.object(self.box, 'snapshot', side_effect=AssertionError('Blocking status read')): + with self.assertRaises(RuntimeUnavailable): + self.runtime.status() + self.runtime.max_age_s = 3 + await self.runtime.refresh() + + async def test_slow_snapshot_uses_start_time_not_completion_for_freshness(self): + self.runtime.max_age_s = .02 + entered, release, blocked = self.block_observation() + with mock.patch.object(self.box, 'snapshot', side_effect=blocked): + waiter = asyncio.create_task(self.runtime.refresh()) + await self.signal(entered) + self.observation_time += .03 + release.set() + with self.assertRaises(RuntimeUnavailable): + await waiter + + async def test_failed_snapshot_does_not_turn_into_successful_cleanup(self): + with mock.patch.object(self.box, 'snapshot', side_effect=OSError('Private fixture path')): + with self.assertRaises(OSError): + await self.runtime.resume_authority('one') + receipt = await self.runtime.cooperative_stop() + self.assertTrue(receipt['worker_joined'] and receipt['host_closed']) + self.assertEqual(receipt['fault'], 'runtime_operation_failed') + self.assertNotIn('Private fixture path', str(self.runtime.diagnostics())) + + async def test_prior_runtime_fault_cannot_be_hidden_by_later_resume_observation(self): + self.submit('resume', 'resume') + with mock.patch.object(self.box, 'snapshot', side_effect=OSError('Fixture read failed')): + with self.assertRaises(OSError): + await self.runtime.refresh() + self.assertIs(await self.runtime.resume_authority('resume'), False) + self.assertEqual(self.runtime.status()['host_phase'], 'fault') + self.assertFalse(self.matches()) + + async def test_postcommit_read_failure_retry_observes_commit_but_does_not_clear_fault(self): + actual_snapshot = self.box.snapshot + count = 0 + def fail_second_read(): + nonlocal count + count += 1 + if count == 2: + raise OSError('Fixture observation lost after actual commit') + return actual_snapshot() + with mock.patch.object(self.box, 'snapshot', side_effect=fail_second_read): + with self.assertRaises(OSError): + await self.runtime.submit_control('resume', 'resume') + before = self.store.history(self.box.task_id) + self.assertEqual(self.box.status('resume')['outcome'], 'pending') + retried = await self.runtime.submit_control('resume', 'resume') + self.assertEqual(retried['outcome'], 'pending') + self.assertEqual(before, self.store.history(self.box.task_id)) + self.assertIs(await self.runtime.resume_authority('resume'), False) + stopped = await self.runtime.cooperative_stop() + self.assertTrue(stopped['worker_joined'] and stopped['host_closed']) + self.assertEqual(stopped['fault'], 'runtime_operation_failed') + + async def test_malformed_real_ack_prefix_never_becomes_resume_authority(self): + self.submit('resume', 'resume'); self.consume() + with self.claim() as owned: + state = deepcopy(owned.checkpoint.state) + state[ACKS]['receipts'][0]['request_id'] = 'different' + owned.save_checkpoint(state, expected_revision=owned.checkpoint.revision) + with self.assertRaises(ControlBlocked): + await self.runtime.resume_authority('resume') + self.assertFalse(self.matches()) + + async def test_failed_host_fence_remains_visible_even_if_worker_later_joins(self): + with mock.patch.object(self.host, 'fence_admission', side_effect=RuntimeError('Fixture fence failure')): + with self.assertRaises(RuntimeError): + self.runtime.fence_admission() + receipt = await self.runtime.cooperative_stop() + self.assertTrue(receipt['worker_joined'] and receipt['host_closed']) + self.assertEqual(receipt['fault'], 'runtime_operation_failed') + + async def test_stop_waiter_cancellation_preserves_worker_join_and_host_fault(self): + self.host.join.clear() + self.host.result['fault'] = 'fixture_host_fault' + waiter = asyncio.create_task(self.runtime.cooperative_stop()) + await self.host.join_entered.wait() + waiter.cancel() + await asyncio.gather(waiter, return_exceptions=True) + self.assertFalse(self.runtime._stop_task.done()) + self.host.join.set() + result = await self.runtime.cooperative_stop() + self.assertEqual(result['fault'], 'fixture_host_fault') + self.assertEqual(self.host.stop_calls, 1) + + async def test_pause_during_real_authority_read_prevents_late_activation(self): + entered, release, blocked = self.block_observation() + authority_entered = asyncio.Event() + actual_authority = self.runtime.resume_authority + async def held_authority(request_id,**kwargs): + authority_entered.set() + with mock.patch.object(self.box, 'snapshot', side_effect=blocked): + return await actual_authority(request_id,**kwargs) + with mock.patch.object(self.runtime, 'resume_authority', side_effect=held_authority): + writer = Writer() + first = self.transport.admit(Reader(self.frame('resume', request_id='resume')), writer) + # As in the rotation test, first establish SQLite submission, then + # retain the unchanged two-second bound on the held thread entry. + try: + await asyncio.wait_for(authority_entered.wait(), 30) + except TimeoutError: + operation = self.transport._records.get('resume', {}).get('task') + error = (type(operation.exception()).__name__ if operation is not None + and operation.done() and not operation.cancelled() else None) + reply = json.loads(writer.data) if writer.data else None + self.fail(f'Authority setup did not complete: reply={reply!r}, error={error}') + await self.signal(entered) + pause = await self.request('pause', request_id='pause') + self.assertTrue(pause['ok']) + self.assertFalse(self.matches()) + release.set() + await first + await self.transport._records['pause']['task'] + self.assertFalse(self.matches()) + + async def test_rotation_during_real_authority_read_prevents_late_activation(self): + entered, release, blocked = self.block_observation() + authority_entered = asyncio.Event() + actual_authority = self.runtime.resume_authority + async def held_authority(request_id,**kwargs): + authority_entered.set() + with mock.patch.object(self.box, 'snapshot', side_effect=blocked): + return await actual_authority(request_id,**kwargs) + with mock.patch.object(self.runtime, 'resume_authority', side_effect=held_authority): + writer = Writer() + first = self.transport.admit(Reader(self.frame('resume', request_id='resume')), writer) + # SQLite submission precedes the held read. Establish that setup + # explicitly before the unchanged two-second thread-entry check. + try: + await asyncio.wait_for(authority_entered.wait(), 30) + except TimeoutError: + operation = self.transport._records.get('resume', {}).get('task') + error = (type(operation.exception()).__name__ if operation is not None + and operation.done() and not operation.cancelled() else None) + reply = json.loads(writer.data) if writer.data else None + self.fail(f'Authority setup did not complete: reply={reply!r}, error={error}') + await self.signal(entered) + self.transport.bind_session(self.transport_cap, 'new-session', 'c' * 64) + self.host.live.add('new-session') + self.host.bindings['new-session'] = UIBinding('host', 'new-session', 'task', 404, 505, 606) + self.assertTrue((await self.request('window_ready', session_id='new-session', token='c' * 64))['ok']) + release.set() + await first + await asyncio.wait_for(asyncio.shield(self.transport._records['resume']['task']), 3) + self.assertFalse(self.matches()) + + async def test_projection_keeps_only_bounded_fields_and_objective(self): + await self.request('pause', request_id='pause') + value = await self.request('status') + text = json.dumps(value) + self.assertTrue(value['ok']) + self.assertEqual(value['status']['objective'], OBJECTIVE) + for forbidden in ('owner_epoch', 'metadata', str(self.store.path), 'a' * 64, 'b' * 64): + self.assertNotIn(forbidden, text) + + +if __name__ == '__main__': + unittest.main(verbosity=2) From 411a3e0ed528a8b99c54f169574d0f7765c67c15 Mon Sep 17 00:00:00 2001 From: Midhat Nashar <35053211+midhatn@users.noreply.github.com> Date: Mon, 9 Nov 2026 06:59:15 +0300 Subject: [PATCH 4/5] Connect corrected fixtures to retained historical source checks --- CHANGELOG.md | 2 +- HANDOFF.md | 2 + docs/evidence.md | 12 +++++ provenance/ci-fixtures/2026-11-09/record.json | 32 +++++++++++++ tests/automatic_tool_recovery_lineage.py | 47 ++++++++++++++++++- tests/test_automatic_tool_recovery_lineage.py | 47 +++++++++++++++++++ 6 files changed, 139 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a861ffd..c83dc28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ These changes belong to the development source. The published `0.4.0a1` tag and - Add the **durable reboot-budget foundation**: a fixed expiry and saved remaining-time cap prevent a restart from granting a new execution allowance. Source, package, installation and scripted SDK checks passed. The first actual Windows restart preserved saved work but the original allowance expired before continuation; execution was correctly refused. Successful computer-reboot continuation remains open. See [budget behavior](docs/reboot-budget.md) and the [retained expired attempt](provenance/installed-reopen/reboot-budget/physical-restart-expired-2026-11-02.json). - Add browser-video observation and comparison tooling, retain unsuccessful measurement attempts and correct continuation-test cleanup. The attempted comparison did not establish desktop/video performance because its observation conditions differed; no broad coexistence or speed claim follows from it. - Make evidence-based agent engineering an explicit goal requirement: bounded changes, meaningful acceptance, independent review, reproducible handoffs and a protected main branch. See the [collaboration practices](docs/multi-agent-collaboration.md#engineering-practices). -- Correct two Windows CI fixture timing assumptions after retaining their original failures. Use a logical observation clock for an ordinary continuation test and a 30-second whole-fixture join guard for retained control work. Twelve targeted tests passed, including stale-observation refusals; production freshness and task deadlines are unchanged. See the [fixture evidence](provenance/ci-fixtures/2026-11-09/record.json). +- Correct two Windows CI fixture timing assumptions after retaining their original failures. Use a logical observation clock for an ordinary continuation test and a 30-second whole-fixture join guard for retained control work. Twelve targeted tests passed, including stale-observation refusals; production freshness and task deadlines are unchanged. Connect their exact retained predecessors to the existing historical-source reader after its separate hosted failure. The broader local reader sweep timed out after 118 passing checks and remains incomplete; fresh hosted validation is required. See the [fixture evidence](provenance/ci-fixtures/2026-11-09/record.json). The automatic-recovery source passed the complete **1,595-test local core suite** and all **16 additional workflow commands**, with the earlier failed test-double attempt retained. Source checks, installed SDK fixtures and physical IQ3_S evidence remain separately identified in the [evidence ledger](docs/evidence.md) and [local validation record](provenance/automatic-tool-recovery/local-validation-2026-11-08.json). These results do not complete M3, M4, M5, daily-use qualification or the full Adaptive-Hermes-Media-Autonomous Test. diff --git a/HANDOFF.md b/HANDOFF.md index 7d10171..d67f3d6 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -55,6 +55,8 @@ The active goal also requires the [agent engineering practices](docs/multi-agent The follow-up [Windows CI fixture correction](docs/evidence.md#follow-up-windows-ci-fixture-correction) retains two original hosted failures and twelve passing targeted checks, including negative expiry checks. Production deadlines and runtime source are unchanged. +Its historical-reader follow-up retains a separate failed hosted pair and an +incomplete local sweep (118 passes, 27 unfinished at the original outer guard). Use the exact current PR checks before integrating; historical or local passes do not override a failed required check. Main branch protection is enabled. diff --git a/docs/evidence.md b/docs/evidence.md index 88805be..b7164a9 100644 --- a/docs/evidence.md +++ b/docs/evidence.md @@ -51,6 +51,18 @@ allowances are unchanged. These source tests do not qualify installed agents, hardware, physical responsiveness or reboot recovery. Successor hosted CI is a separate gate; a local pass does not relabel an original failed run. +Both original workflows at `a83844b` then failed an unchanged historical source +assertion: the historical reader still selected the corrected live fixture. +The follow-up reuses the existing source-lineage chain to select only the exact +retained predecessor of either declared fixture. Unknown source bytes continue +to face the original assertions. Three new archive/identity regression tests +and the originally failing compiler assertion passed. A broader local sweep +logged 118 passes out of 145 planned tests before its original 600-second +outer guard expired; the retained child was terminated and joined, leaving +27 tests incomplete. This is **not a passing full-suite result**. Its partial +log and failed supervisor outcome remain recorded; required fresh hosted CI +must pass on the corrected successor before merge. + ## Agent-requested GPU video and foreground handoff The [second physical video task](../provenance/video-tool/physical-foreground-video-2026-11-02.json) diff --git a/provenance/ci-fixtures/2026-11-09/record.json b/provenance/ci-fixtures/2026-11-09/record.json index 8ccf737..37d9546 100644 --- a/provenance/ci-fixtures/2026-11-09/record.json +++ b/provenance/ci-fixtures/2026-11-09/record.json @@ -66,6 +66,38 @@ "cleanup_killed": false, "new_hosted_result": "Not claimed by this local record; observe the new commit original workflows separately" }, + "historical_reader_followup": { + "failed_head": "a83844b5fed20bf01dcfd61d4061b00961b787cc", + "original_failed_runs": [42557615924, 42557619469], + "original_attempt": 1, + "failure": "Historical compiler source assertion read the corrected live continuation fixture rather than its exact retained predecessor", + "representative_job": 128966748035, + "representative_log_sha256": "8a25fb23a4e5e98b531df85501b4167d5aa9433dd25fc4e43cd84a1e6fee5146", + "change": "Reuse the existing outer source-lineage reader to project only the two declared exact successor fixture hashes to their retained predecessors. Unknown bytes still reach unchanged historical assertions. Runtime projection and original manifests are unchanged.", + "source_sha256": { + "tests/automatic_tool_recovery_lineage.py": "49e2c487d8d5f3dad0840523d0fc8bbda2c642e30bd0c685220fe0a5cca18628", + "tests/test_automatic_tool_recovery_lineage.py": "dcf8700726072b98289c6c43ebfa66052767918fe97bb4004bb4f1546c8cf41a" + }, + "local_validation": { + "status": "incomplete_outer_guard_timeout", + "planned_tests": 145, + "completed_passes": 118, + "remaining_including_interrupted": 27, + "logged_failures_or_errors": 0, + "all_three_new_archive_regressions_completed": true, + "original_failed_compiler_assertion_completed": true, + "outer_guard_s": 600, + "retained_test_child_joined": true, + "retained_test_child_terminated_at_guard": true, + "supervisor_exit_code": 1, + "candidate_hashes_match_prelaunch_record": true, + "full_worker_source_inventory_receipt_written": false, + "log_sha256": "67c77aa183e43672656e908b7822c0e50f51e227d60ea8218e61ddc91c9d614f", + "supervisor_sha256": "da520007b3171be30b9a38dd9137c63c333e0f2a18c102cc52563e010bd7aa98", + "partial_audit_sha256": "9cd9533be307b1a684a1ed7f4cb05d2f89d12c62fb42d4e50a715a55f420b237", + "interpretation": "Partial log evidence, not a passing full lineage suite. No rerun or guard extension. Fresh required hosted CI must validate the full successor." + } + }, "research": [ { "url": "https://github.com/python/cpython/blob/v3.12.10/Lib/test/support/__init__.py", diff --git a/tests/automatic_tool_recovery_lineage.py b/tests/automatic_tool_recovery_lineage.py index 31474dd..d4cf4cf 100644 --- a/tests/automatic_tool_recovery_lineage.py +++ b/tests/automatic_tool_recovery_lineage.py @@ -13,6 +13,47 @@ ROOT = Path(__file__).resolve().parents[1] ARCHIVE = 'provenance/automatic-tool-recovery' +CI_FIXTURES = 'provenance/ci-fixtures/2026-11-09' +CI_FIXTURE_FILES = {'tests/test_continuation_host.py', 'tests/test_desktop_mailbox_runtime.py'} + + +def ci_fixture_record(root): + path = root / CI_FIXTURES / 'record.json' + if not path.exists(): + return None + rows = json.loads(path.read_bytes()) + if set(rows['files']) != CI_FIXTURE_FILES: + raise AssertionError('Changed CI fixture source set') + return rows + + +def ci_fixture_inputs(root): + rows = ci_fixture_record(root) + if rows is None: + return [] + return [root / CI_FIXTURES / 'record.json', + *(root / CI_FIXTURES / 'predecessor' / relative for relative in sorted(CI_FIXTURE_FILES))] + + +def ci_fixture_predecessor(relative, data, root): + """Reuse exact-file projection for later test-only timing corrections. + + Archived fixtures are data only. Their old qualification does not apply to + the corrected tests, and unknown bytes still face unchanged inner checks. + """ + if relative not in CI_FIXTURE_FILES: + return data + rows = ci_fixture_record(root) + if rows is None: + return data + row = rows['files'][relative] + expected = CI_FIXTURES + '/predecessor/' + relative + if row['predecessor_source'] != expected: + raise AssertionError('Changed CI fixture predecessor path') + before = (root / expected).read_bytes() + if hashlib.sha256(before).hexdigest() != row['predecessor_sha256']: + raise AssertionError('Changed CI fixture predecessor bytes') + return before if hashlib.sha256(data).hexdigest() == row['successor_sha256'] else data def record(root=None): @@ -25,9 +66,10 @@ def inputs(root=None): root = ROOT if root is None else root rows = record(root) if rows is None: - return [] + return ci_fixture_inputs(root) return [root / ARCHIVE / 'source-lineage.json', - *(root / row['predecessor_source'] for row in rows['files'].values())] + *(root / row['predecessor_source'] for row in rows['files'].values()), + *ci_fixture_inputs(root)] def retained(relative, row, root): @@ -46,6 +88,7 @@ def retained(relative, row, root): def predecessor(relative, data, root=None): """Project known complete successor bytes; unknown bytes remain unknown.""" root = ROOT if root is None else root + data = ci_fixture_predecessor(relative, data, root) rows = record(root) row = None if rows is None else rows['files'].get(relative) if row is None: diff --git a/tests/test_automatic_tool_recovery_lineage.py b/tests/test_automatic_tool_recovery_lineage.py index fd806f3..a55de0e 100644 --- a/tests/test_automatic_tool_recovery_lineage.py +++ b/tests/test_automatic_tool_recovery_lineage.py @@ -18,6 +18,53 @@ def current_runtime(self): return {path.relative_to(runtime).as_posix(): path.read_bytes() for path in runtime.rglob('*.py')} + def test_ci_fixture_delta_preserves_both_exact_archives_without_executing_them(self): + rows = lineage.ci_fixture_record(lineage.ROOT) + self.assertEqual(set(rows['files']), lineage.CI_FIXTURE_FILES) + self.assertTrue(set(lineage.ci_fixture_inputs(lineage.ROOT)) <= set(lineage.inputs())) + for relative, row in rows['files'].items(): + with self.subTest(relative=relative): + current = (lineage.ROOT / relative).read_bytes() + before = (lineage.ROOT / row['predecessor_source']).read_bytes() + self.assertEqual(hashlib.sha256(current).hexdigest(), row['successor_sha256']) + self.assertEqual(hashlib.sha256(before).hexdigest(), row['predecessor_sha256']) + self.assertNotEqual(current, before) + self.assertEqual(lineage.predecessor(relative, current), before) + self.assertEqual(lineage.predecessor(relative, before), before) + for extra in (b'\n# unreviewed fixture\n', b'\nunreviewed = True\n'): + changed = current + extra + self.assertEqual(lineage.predecessor(relative, changed), changed) + self.assertNotEqual(hashlib.sha256(changed).hexdigest(), row['predecessor_sha256']) + with tempfile.TemporaryDirectory() as directory: + self.assertEqual(lineage.predecessor(relative, current, Path(directory)), current) + + def test_ci_fixture_archive_path_and_hash_tampering_is_refused(self): + rows = lineage.ci_fixture_record(lineage.ROOT) + for relative in lineage.CI_FIXTURE_FILES: + data = (lineage.ROOT / relative).read_bytes() + for field in ('predecessor_source', 'predecessor_sha256'): + changed = deepcopy(rows); changed['files'][relative][field] = '0' * 64 + with self.subTest(relative=relative, field=field), mock.patch.object( + lineage, 'ci_fixture_record', return_value=changed): + with self.assertRaisesRegex(AssertionError, 'CI fixture predecessor'): + lineage.predecessor(relative, data) + changed = deepcopy(rows); changed['files'][relative]['successor_sha256'] = '0' * 64 + with mock.patch.object(lineage, 'ci_fixture_record', return_value=changed): + self.assertEqual(lineage.predecessor(relative, data), data) + + def test_modified_ci_fixture_archive_bytes_cannot_satisfy_historical_readers(self): + rows = lineage.ci_fixture_record(lineage.ROOT) + original_read = Path.read_bytes + for relative, row in rows['files'].items(): + current = (lineage.ROOT / relative).read_bytes() + target = lineage.ROOT / row['predecessor_source'] + def read(path): + raw = original_read(path) + return raw + b'\n# changed retained bytes\n' if path == target else raw + with self.subTest(relative=relative), mock.patch.object(Path, 'read_bytes', read): + with self.assertRaisesRegex(AssertionError, 'CI fixture predecessor bytes'): + lineage.predecessor(relative, current) + def test_exact_pending_tool_predecessors_and_evidence_remain_unchanged(self): rows = lineage.record() self.assertEqual(rows['predecessor_revision'], '9242434991f2c2c21461061d61d898649299b847') From 142a838de9cd8f41a1f027c13578301f11077bb7 Mon Sep 17 00:00:00 2001 From: Midhat Nashar <35053211+midhatn@users.noreply.github.com> Date: Mon, 9 Nov 2026 07:11:07 +0300 Subject: [PATCH 5/5] Share fixture join guard with aliased control tests --- HANDOFF.md | 3 ++ docs/evidence.md | 12 ++++++- provenance/ci-fixtures/2026-11-09/record.json | 33 +++++++++++++++++-- tests/test_desktop_mailbox_runtime.py | 13 ++++---- 4 files changed, 52 insertions(+), 9 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index d67f3d6..168d2df 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -57,6 +57,9 @@ retains two original hosted failures and twelve passing targeted checks, includi negative expiry checks. Production deadlines and runtime source are unchanged. Its historical-reader follow-up retains a separate failed hosted pair and an incomplete local sweep (118 passes, 27 unfinished at the original outer guard). +The next hosted run exposed two untested aliased helper callers; a shared +module-level guard fixes that dependency, with twelve focused successor checks +passing across direct/aliased callers and historical-source assertions. Use the exact current PR checks before integrating; historical or local passes do not override a failed required check. Main branch protection is enabled. diff --git a/docs/evidence.md b/docs/evidence.md index b7164a9..1f77a25 100644 --- a/docs/evidence.md +++ b/docs/evidence.md @@ -44,7 +44,8 @@ exact predecessor test files, failure identities and local validation. The first fixture now reuses the existing logical observation-clock pattern. The second uses the existing project/CPython 30-second whole-fixture guard while preserving shielded ownership and final-intent assertions. Twelve selected tests passed: -both affected paths, all six helper callers and five negative expiry checks. +both affected paths, six defining-module callers and five negative expiry checks. +The two callers that borrow the helper from another class were not included. Three owned inert children joined with their expected exits, without cleanup kills. Runtime files, production freshness, real execution clocks and task/tool allowances are unchanged. These source tests do not qualify installed agents, @@ -63,6 +64,15 @@ outer guard expired; the retained child was terminated and joined, leaving log and failed supervisor outcome remain recorded; required fresh hosted CI must pass on the corrected successor before merge. +The next original hosted pair at `411a3e0` exposed that missing alias coverage: +`ExpiryRuntimeTests` borrows the helper without inheriting its defining class, +so the new instance-setting lookup raised `AttributeError`. Move the unchanged +30-second guard to module scope. Twelve targeted successor checks passed: +all six direct and both aliased calling tests, three source-archive regressions, +and the original compiler source assertion. The retained test child joined +normally, with no timeout or source change during the check. Production behavior +and prior failed outcomes are unchanged; the new head still requires full CI. + ## Agent-requested GPU video and foreground handoff The [second physical video task](../provenance/video-tool/physical-foreground-video-2026-11-02.json) diff --git a/provenance/ci-fixtures/2026-11-09/record.json b/provenance/ci-fixtures/2026-11-09/record.json index 37d9546..fcba1ad 100644 --- a/provenance/ci-fixtures/2026-11-09/record.json +++ b/provenance/ci-fixtures/2026-11-09/record.json @@ -11,7 +11,8 @@ "tests/test_desktop_mailbox_runtime.py": { "predecessor_source": "provenance/ci-fixtures/2026-11-09/predecessor/tests/test_desktop_mailbox_runtime.py", "predecessor_sha256": "02cb14272823904f022307a5757f3a13daa9fe68bf734d6e6238554d57268697", - "successor_sha256": "12c11cf6cf1bc3007e18a5051e192d8dd43af34aa3b44d34eae4458091de9430" + "intermediate_successor_sha256": "12c11cf6cf1bc3007e18a5051e192d8dd43af34aa3b44d34eae4458091de9430", + "successor_sha256": "55ebc08fa3def908431556bd3e2089a034762296df235ae1df1dbe1647e730c4" } }, "original_failures": [ @@ -50,11 +51,13 @@ "Stale-observation refusals and duplicate-launch assertions" ], "validation": { + "tested_revision": "a83844b5fed20bf01dcfd61d4061b00961b787cc", + "mailbox_fixture_sha256": "12c11cf6cf1bc3007e18a5051e192d8dd43af34aa3b44d34eae4458091de9430", "selected_tests": 12, "failures": 0, "errors": 0, "skips": 0, - "scope": "Both previously failing cases, all six completed_control callers and five expiry/refusal cases", + "scope": "Both previously failing cases, six defining-module completed_control callers and five expiry/refusal cases; the two aliased callers were not included", "test_elapsed_s": 3.108, "receipt_sha256": "debe2466a111ceec2c3186c44fd6f8ca6b329ff443c4da80a67de6c65a7eaffa", "owned_child_exit_codes": [ @@ -98,6 +101,32 @@ "interpretation": "Partial log evidence, not a passing full lineage suite. No rerun or guard extension. Fresh required hosted CI must validate the full successor." } }, + "shared_helper_followup": { + "failed_head": "411a3e0ed528a8b99c54f169574d0f7765c67c15", + "original_runs": [42559679949, 42559684062], + "representative_job": 128973128415, + "representative_log_sha256": "d891ff6612f985f308ec2b254dfd3cdaf686ce3352477a1af45f42c9a54867fa", + "failure": "ExpiryRuntimeTests aliases completed_control without inheriting RuntimeTests, so the new self.control_completion_timeout_s attribute was absent", + "change": "Move the unchanged 30-second guard to the defining module scope so both direct and aliased callers use the same bound. Preserve shielding, assertions and production source.", + "validation": { + "tests": 12, + "failures": 0, + "errors": 0, + "skips": 0, + "scope": "Six direct and two aliased helper callers, three archive regressions and the original compiler source assertion", + "elapsed_s": 8.265, + "sources_unchanged": true, + "supervisor_exit_code": 0, + "retained_test_child_joined": true, + "timed_out": false, + "runtime_or_task_budgets_changed": false, + "result_sha256": "9099e90fd24e27928d59a2b26203dac55b1a8107e46d51f248de81d62e0d96e5", + "supervisor_sha256": "41fa51c1bd38a87a4ec741b1cb3738515544d784d97f83a33fbe46905f5a3567", + "log_sha256": "aa00a1903efb47e508dcf741aeeaf083c3f7645ca9bbf32f0065cf6281afc464", + "full_hosted_successor": "Required separately; the earlier failed runs and incomplete local sweep remain unchanged" + }, + "reference": "https://docs.python.org/3/tutorial/classes.html#python-scopes-and-namespaces" + }, "research": [ { "url": "https://github.com/python/cpython/blob/v3.12.10/Lib/test/support/__init__.py", diff --git a/tests/test_desktop_mailbox_runtime.py b/tests/test_desktop_mailbox_runtime.py index 28b9627..3e69229 100644 --- a/tests/test_desktop_mailbox_runtime.py +++ b/tests/test_desktop_mailbox_runtime.py @@ -21,6 +21,12 @@ NOW = '2026-10-24T18:00:00Z' OBJECTIVE = 'Keep the original task and finish the validated artifact.' +# Whole-fixture join guard for actual SQLite/thread work, not a product +# freshness or response-latency requirement. Keep it at module scope because +# other fixtures reuse completed_control without inheriting RuntimeTests. +# https://github.com/python/cpython/blob/v3.12.10/Lib/test/support/__init__.py +CONTROL_COMPLETION_TIMEOUT_S = 30 + class DataFixture: def data(self): @@ -86,11 +92,6 @@ async def cooperative_stop(self): class RuntimeTests(DataFixture, unittest.IsolatedAsyncioTestCase): - # Whole-fixture join guard for actual SQLite/thread work, not a product - # freshness or response-latency requirement. Reuse CPython SHORT_TIMEOUT: - # https://github.com/python/cpython/blob/v3.12.10/Lib/test/support/__init__.py - control_completion_timeout_s = 30 - async def asyncSetUp(self): self.data() self.demand() @@ -141,7 +142,7 @@ async def completed_control(self, method, *, request_id): reply = await self.request(method, request_id=request_id) self.assertTrue(reply['ok'], reply) await asyncio.wait_for(asyncio.shield(self.transport._records[request_id]['task']), - self.control_completion_timeout_s) + CONTROL_COMPLETION_TIMEOUT_S) return reply def matches(self):