diff --git a/ai/prompt-io/opencode/20260908T215045Z_1a2d4893_prompt_io.md b/ai/prompt-io/opencode/20260908T215045Z_1a2d4893_prompt_io.md new file mode 100644 index 00000000..011299d8 --- /dev/null +++ b/ai/prompt-io/opencode/20260908T215045Z_1a2d4893_prompt_io.md @@ -0,0 +1,179 @@ +--- +model: openai/gpt-6-astra +service: opencode +session: null +timestamp: 2026-09-08T21:50:45Z +git_ref: 1a2d4893 +scope: code +substantive: true +raw_file: 20260908T215045Z_1a2d4893_prompt_io.raw.md +--- + +## Prompt + +Continue implementation in SAME session/worktree +/home/goodboy/repos/piker/wkts/live_chart_qtractor. User explicitly +says proceed. No ownership takeover needed, no branch creation, no +staging/commits, no ai.skillz changes. Implement minimal prerequisite +fixes before live Kraken tests: inspect current code and fix +start_qtractor merge precedence so explicit caller registry/runtime +kwargs beat global defaults; fix PyQt6 blocking run_qtractor exec_ -> +exec; add focused offline headless lifecycle regressions proving +explicit registry selection despite stale globals and CLI adapter as +feasible. Load py-codestyle, piker-conc-expert, run-tests via tools; +read deployed test harness. Preserve existing comments and user-owned +state. Use apply_patch. Existing staged baseline is committed at +1a2d4893, .gitignore/.ai workflow state unrelated leave untouched. User +prefers typed opaque function returns, no redundant obvious +constructor types, vertical boolean operators, logging exceptions. +Run only targeted headless tests with existing py313 interpreter and +Nix wrapper (already user-authorized in this session); do not run full +suite or public network charts. Do not expand to paper mode or +readiness yet. Return changes/tests, exact remaining blockers, and any +provenance modifications. Persist prompt-io for code per skill with +correct active model identity if known, not invented old model. Keep +minimal. + +## Response Summary + +Fixed explicit caller kwarg precedence and the PyQt6 blocking API. +Extended real runtime lifecycle coverage and added subprocess-isolated +CLI adapter success/error regressions. Focused tests passed 3/3; +combined UI isolation and lifecycle tests passed 5/5, with only two +existing Tractor deprecation warnings per run. + +## Files Changed + +- `piker/ui/_exec.py`: merge order and `QApplication.exec()`. +- `tests/ui/test_qtractor.py`: stale-global and blocking regressions. +- This log and its raw companion: prompt and generated-output record. + +Existing provenance entries and the service README are unchanged. +Unrelated `.gitignore` and `.ai` state are unchanged. No staging, +commits, branch creation, or ai.skillz changes were performed. +The active harness did not expose a session UUID; it is not invented. + +## Human Edits + +The human identified both prerequisite defects, selected this existing +worktree, restricted implementation to minimal offline coverage, and +specified code style, test environment, provenance identity, and +untouched state. These are human-directed agent edits. No direct +manual edits during this turn were observed; acceptance remains with +the human. + +## Continuation Prompt: 2026-09-08T21:56:56Z + +Implement next minimal safety prerequisite in +/home/goodboy/repos/piker/wkts/live_chart_qtractor. Same session user +authorized continuation. Previous agent modified piker/ui/_exec.py, +tests/ui/test_qtractor.py and added +ai/prompt-io/opencode/20260908T215045Z_1a2d4893_prompt_io.md/.raw.md for +registry precedence and Qt exec fixes. Preserve all those and unrelated +.gitignore/.ai files. No staging, commits, branch/worktree operations +or ai.skillz edits. Task: inspect production chart startup +_app._async_main -> GodWidget/load_symbols/display -> open_order_mode +-> open_ems. Thread a minimal explicit execution mode option so a chart +integration caller can require paper EMS BEFORE any brokerd spawn. +Preserve shipped default live CLI behavior (concrete existing callers). +Prefer one clear existing convention/type and defaults, no +compatibility scaffolding. Also inspect readiness provider search +start_soon: implement minimal nursery.start/task_status handshake if +appropriate to ensure application readiness follows provider search +registration, without arbitrary waits. Load piker-clearing-expert, +piker-conc-expert, py-codestyle, run-tests skills and harness. Tests +must be real structured runtime tests or narrow direct unit tests as +existing conventions permit; do not add interlayer mocking without +asking. Do not run live providers, GUI compositor or full suite. Run +relevant headless tests using existing py313 and already-authorized +nix develop. If prerequisite changes grow too broad or unsafe stop +and report exact tradeoff. Update provenance for your changes +appropriately (same logical prerequisite patch may extend newest entry +without erasing original generation). Return code paths, exact test +evidence, risks and next live chart test requirements. + +## Continuation Summary + +Added explicit session-wide EMS mode plumbing across `_async_main`, +`GodWidget.load_symbols`, `display_symbol_data`, `open_order_mode`, +and `open_ems`. The existing string convention and live defaults are +preserved. Invalid startup modes fail before widget access. Added the +provider-search Trio startup handshake before application readiness. + +New direct unit coverage is in `tests/ui/test_chart_startup.py`. +Combined with existing Qtractor and UI-isolation tests, 9 tests passed +in 2.67s; the separate display/gap-overlay run passed 9 in 2.00s. +Warnings were limited to existing Tractor deprecations. Full forwarding +and provider registration ordering remain live-integration coverage +requirements, not claims of these narrow tests. + +This continuation changed four UI production files, added the startup +test file, and appended to this provenance pair. Original generation, +`_exec.py`, `test_qtractor.py`, `.gitignore`, `.ai`, and ai.skillz were +left untouched. No staging or Git history/worktree operations occurred. + +## Continuation Human Contributions + +The human selected paper execution and search readiness as the next +safety prerequisite, required shipped live defaults, prohibited +interlayer mocking without approval, limited verification to headless +offline tests, and authorized appending this existing provenance pair. +These requirements directed the generated edits. No direct manual +changes during this continuation were observed; acceptance remains +with the human. + +## Live Qualification Prompt: 2026-09-08 + +The human explicitly authorizes public Kraken market data for +xbtusd.spot.kraken, headless integration, isolated registry/config, +and explicit paper execution with no credentials or real orders. +Implement the minimal production live chart test in this same owned +worktree, preserving the uncommitted prerequisites and unrelated +.gitignore/.ai state. No takeover, staging/commit, or ai.skillz edits. +Use the authoritative live-backend roadmap, required concurrency, +clearing, Python style and test-harness skills, and existing py313/Nix +wrapper. Add explicit --live-provider=kraken opt-in; preserve original +selected-run errors. Use production GodWidget and _async_main via +start_qtractor, owned empty broker config, unique reg_addr, inherited +piker_test_dir and isolated settings. Record paper/no-brokerd and +exact owned resources where possible. Always cancel in finally and +pump Qt with a bound before widget destruction. No new interlayer +mocks, symbol switching, blanket retries, broad cleanup, or expanded +test excludes. Stop if production blockers require broader repairs. +Append accurate gpt-6-astra provenance and human network authorization +to the newest pair, preserving earlier generation. + +## Live Qualification Summary + +Active model remains gpt-6-astra (`openai/gpt-6-astra`). Added the +explicit live-provider option and production Kraken smoke test. +Corrected one demonstrated Kraken public-client constructor keyword +from `conf` to `config`. No other production code was changed during +this continuation. Exact new patch scope is: + +- `tests/conftest.py`: six-line opt-in option. +- `tests/ui/test_live_chart.py`: new production integration test. +- `piker/brokers/kraken/api.py`: one keyword correction. +- This existing Prompt-IO pair: append-only continuation records. + +Final offline targeted checks: 9 passed, 1 skipped in 2.65s. The live +run failed before the keyword fix (1 failed, 1 teardown error in +2.66s), then progressed to public history and paper EMS but failed +again (1 failed, 1 teardown error in 12.44s). Readiness preceded +rt_linked.mode assignment; Qt reported abstract paint errors and +widget teardown survivors. No successful live qualification is claimed. +Commands, original signatures, proposed minimal readiness repair, +and evidence limitations are retained in the raw companion. + +Guest completion preceded widget destruction, but Tractor escalated +its own child teardown to SIGKILL and a paperboi.kraken orphan remained: +PID 752893, UID e8317c39-ebbb-4526-85f0-3440a2e6e39c, PPID 1. +No agent-issued signal or SHM cleanup was performed. Further live +runs and production repairs were stopped. The final test improvements +to failure-path SHM/endpoint accounting were checked offline only. + +The human directed all safety boundaries, the public network opt-in, +the minimal scope and stop condition, and continuation of this pair. +These are human-directed agent edits; acceptance remains with the +human. Earlier provenance, prerequisite work, task-state markers, +.gitignore, .ai, and ai.skillz are unchanged. No staging or commits. diff --git a/ai/prompt-io/opencode/20260908T215045Z_1a2d4893_prompt_io.raw.md b/ai/prompt-io/opencode/20260908T215045Z_1a2d4893_prompt_io.raw.md new file mode 100644 index 00000000..efc821c4 --- /dev/null +++ b/ai/prompt-io/opencode/20260908T215045Z_1a2d4893_prompt_io.raw.md @@ -0,0 +1,226 @@ +--- +model: openai/gpt-6-astra +service: opencode +timestamp: 2026-09-08T21:50:45Z +git_ref: 1a2d4893 +diff_cmd: git diff HEAD +--- + +## Generated Changes + +> `git diff HEAD -- piker/ui/_exec.py` + +`start_qtractor()` copies global runtime defaults first and explicit +caller kwargs last. `run_qtractor()` uses PyQt6's `exec()` API. +Existing comments and exception logging are preserved. + +> `git diff HEAD -- tests/ui/test_qtractor.py` + +The existing repeated-session regression now seeds stale global +registry and runtime overrides before each startup. Its real guest +asserts the requested address is bound and the caller's runtime +overrides are active before publishing readiness. + +A parametrized child-process regression exercises the real blocking +adapter with normal guest completion and a sentinel exception. It +checks exception identity, window cleanup, and actor teardown. Each +child uses offscreen Qt and test-owned config and registry addresses. +No public provider, paper mode, or chart readiness work is included. + +## Verification + +The existing worktree-local interpreter resolves Piker from this +worktree: Python 3.13.14, pytest-qt 4.5.0. The authorized Nix wrapper +ran its normal uv check; no environment provisioning was requested. + +```xsh +nix develop --command timeout -k 5 300 py313/bin/python -m pytest --headless -q -x --tb=short --no-header tests/ui/test_qtractor.py +nix develop --command timeout -k 5 300 py313/bin/python -m pytest --headless -q -x --tb=short --no-header tests/ui/test_harness.py tests/ui/test_qtractor.py +``` + +The focused run passed 3 tests in 2.44s. The combined targeted run +passed 5 tests in 2.36s. Each reported two existing Tractor +`Registrar.uid` deprecation warnings. `git diff --check` passed. + +Live Kraken charts, the full suite, paper mode, and readiness changes +were not run or implemented. The deployed harness still contains a +stale SIGINT warning for `MainWindow`; current source uses structured +shutdown. The reference was left untouched. + +## Continuation: 2026-09-08T21:56:56Z + +Model: openai/gpt-6-astra. The original generation above is preserved. + +> `git diff HEAD -- piker/ui/_app.py` + +`_async_main()` accepts `exec_mode: str = 'live'`, rejects values other +than 'live' and 'paper' before touching the widget, and stores the +selection on `GodWidget`. `load_provider_search()` publishes Trio task +startup after entering provider search registration and symcache. +The application awaits `nursery.start()` before publishing readiness. + +> `git diff HEAD -- piker/ui/_widget.py` +> `git diff HEAD -- piker/ui/_display.py` +> `git diff HEAD -- piker/ui/order_mode.py` + +`GodWidget.load_symbols()` forwards its session execution mode to +`display_symbol_data()`, which passes it to `open_order_mode()` and +then `open_ems(mode=exec_mode)`. Defaults remain live, matching the +existing CLI call. Keeping selection on the widget also covers later +search-driven symbol loads. No EMS, broker, or CLI implementation was +changed. Existing EMS code checks paper before acquiring brokerd; +paper relay cache keys are already distinct from live keys. + +> `git diff HEAD -- tests/ui/test_chart_startup.py` + +Added narrow direct tests for invalid-mode rejection before widget +access, existing CLI argument binding, explicit paper binding, and +entrypoint defaults. There are no interlayer mocks. These assertions +do not prove full chart forwarding or provider readiness at runtime. + +### Verification + +The authorized Nix wrapper and existing py313 interpreter resolve +Piker inside this worktree, with Python 3.13.14 and pytest-qt 4.5.0. +VIRTUAL_ENV is the worktree's py313 directory. Nix ran its normal uv +check; no separate environment provisioning was performed. + +```xsh +nix develop --command timeout -k 5 300 py313/bin/python -m pytest --headless -q -x --tb=short --no-header tests/ui/test_chart_startup.py tests/ui/test_harness.py tests/ui/test_qtractor.py +nix develop --command timeout -k 5 300 py313/bin/python -m pytest --headless -q -x --tb=short --no-header tests/test_gap_overlays.py +git diff --check +``` + +First run: 9 passed in 2.67s, two existing `Registrar.uid` deprecation +warnings. Second run: 9 passed in 2.00s, one upstream `trio.Event` +boolean-use deprecation warning. Diff whitespace check passed. + +No live provider, compositor, or full-suite execution was performed. +Full chart readiness and zero-brokerd behavior need a separately +authorized live-feed test using explicit paper mode, isolated registry +and config, real search registration assertions, symbol switching, +and structured close/restart checks. Paper mode still accesses market +data and may write paper accounting state. It is not a process-wide +ban on other clients' live execution. + +## Live Qualification Continuation: 2026-09-08 + +Active model: gpt-6-astra (`openai/gpt-6-astra`). Earlier content +above is historical and is preserved. The human now explicitly +authorizes public Kraken market data, headless Qt, and paper EMS in +this same worktree. No credentials, real orders, ownership takeover, +staging, commits, ai.skillz edits, or unrelated state changes are +authorized. The human requires an opt-in production chart test, +isolated registry/config, bounded guest teardown before widget +destruction, original errors, and stopping rather than broad repairs. + +> `git diff HEAD -- tests/conftest.py` +> `git diff --no-index -- /dev/null tests/ui/test_live_chart.py` +> `git diff HEAD -- piker/brokers/kraken/api.py` + +Added `--live-provider=kraken` and a production GodWidget/_async_main +journey through start_qtractor. The test supplies explicit paper mode, +the Tractor fixture's unique registry, owned root config paths, an +empty brokers.toml, and inherited piker_test_dir. Existing process XDG +isolation protects Qt settings and is inherited by children. There +are no interlayer mocks, submitted orders, or symbol interactions. + +Qt polling races application readiness against guest completion. +Finally requests shutdown and pumps Qt with a 60-second bound before +normal pytest-qt widget destruction. Guest errors are unwrapped; +multiple body/teardown failures are grouped. Exact registry identities +and chart SHM names are recorded as pytest properties. Detached known +startup widgets are registered with QtBot after guest completion. +The test checks paper actors/no observed brokerd, title, markets, +nonempty history/realtime arrays, and a subsequent close-price change. + +The first selected run reproduced a public-client constructor typo: +`get_client()` passed `conf={}` to `Client(config, httpx_client, ...)`. +Changed only that keyword to `config={}`. The next selected run got +past this defect and consumed public Kraken history with paper EMS. + +### Exact Commands And Results + +The existing authorized Nix wrapper resolves Python 3.13.14, Piker in +this worktree, pytest-qt 4.5.0, and VIRTUAL_ENV pointing at py313. +Its normal uv check ran; no separate provisioning was requested. + +```xsh +nix develop --command timeout -k 5 300 py313/bin/python -m pytest --headless -q -x --tb=short --no-header tests/ui/test_live_chart.py tests/ui/test_chart_startup.py tests/ui/test_qtractor.py tests/ui/test_harness.py +nix develop --command timeout -k 5 300 py313/bin/python -m pytest --headless --live-provider=kraken -q -x --tb=short --no-header tests/ui/test_live_chart.py +git diff --check +``` + +Offline command: initially 9 passed, 1 skipped in 2.66s; final rerun +9 passed, 1 skipped in 2.65s, with two Tractor deprecation warnings. +Diff whitespace check passed. + +Live command was run twice with a targeted fix between runs, not as +a blanket provider retry. First: 1 failed, 1 teardown error, 4 warnings +in 2.66s. Original guest error was retained: + +```text +piker/brokers/kraken/api.py:764 in get_client + Client(conf={}, httpx_client=trio_client) +TypeError: Client.__init__() got an unexpected keyword argument +'conf'. Did you mean 'config'? +``` + +Second: 1 failed, 1 teardown error, 13 warnings in 12.44s. +Readiness and observed paper actor/config checks passed, then: + +```text +assert set(godw.rt_linked.mode.trackers) == {'paper'} +AttributeError: 'LinkedSplits' object has no attribute 'mode'. +Did you mean: 'move'? +``` + +`open_order_mode()` sets its started event at order_mode.py:1024, +before `_display.py:1693` assigns rt_linked.mode after entering chart +input handlers. `_async_main()` waits for the earlier event before +publishing session readiness. Minimal proposed follow-up: publish a +display-complete readiness event after these scopes and assignment, +rather than weakening this assertion or adding a timing sleep. + +Qt also reported twice, without a Python item-identifying traceback: + +```text +NotImplementedError: QGraphicsObject.paint() is abstract and must +be overridden +``` + +The UI isolation fixture reported remaining QFrame, QLabel, QMenu, +and ViewBoxMenu objects. Rendering and widget-ownership fixes were +not attempted. Identify the offending graphics item and give detached +chart resources explicit ownership in a separate bounded follow-up. + +Guest completion was observed before widgets were destroyed, but it +did NOT prove clean actor teardown. Tractor logs show its own forced +SIGKILL escalation for datad, samplerd, emsd and both FSP actors. +A non-mutating `ps` check found the exact paper child still alive: + +```text +PID 752893, PPID 1, paperboi.kraken +UID e8317c39-ebbb-4526-85f0-3440a2e6e39c +parent_addr ('127.0.0.1', 34915) +``` + +Checked known PIDs 738066, 738721, 749265, 749778, 750704, 751382, +751383, 752409 and 752893; only 752893 remained. No agent-issued +signals, manual SHM unlinking, broad cleanup, or subsequent live runs +were performed. The runtime's descendant teardown cause is not yet +established; it must be diagnosed before claiming clean shutdown. + +After that failure, the test was tightened to collect chart SHM during +startup polling and check exact SHM/endpoints even after a body error. +The final version was checked offline only, since the paper survivor +remains. The reached run did not execute market/title/array/live-price +assertions or the original success-only SHM/endpoint checks. Those +are implemented expectations, not passing qualification evidence. + +Further gaps: polling can miss transient actors; chart tokens omit +unpublished and non-chart child SHM; closed endpoints do not prove +PID exit; hard guest-timeout safety before widget destruction is not +qualified. No full suite, compositor, symbol switching, order journey, +or test-exclude changes were made. Existing prerequisite changes and +unrelated .gitignore/.ai state were preserved. diff --git a/piker/brokers/kraken/api.py b/piker/brokers/kraken/api.py index c49a948e..c9d8d8ff 100644 --- a/piker/brokers/kraken/api.py +++ b/piker/brokers/kraken/api.py @@ -762,7 +762,7 @@ async def get_client() -> Client: ) else: client = Client( - conf={}, + config={}, httpx_client=trio_client, ) diff --git a/piker/ui/_app.py b/piker/ui/_app.py index 61be4800..bff0350d 100644 --- a/piker/ui/_app.py +++ b/piker/ui/_app.py @@ -47,11 +47,17 @@ log = get_logger(__name__) async def load_provider_search( - brokermod: str, + brokermod: ModuleType, loglevel: str, -) -> None: + *, + task_status: trio.TaskStatus[None] = trio.TASK_STATUS_IGNORED, +) -> None: + ''' + Publish startup after provider search and symcache are installed. + + ''' name = brokermod.name log.info(f'loading datad for {name}..') @@ -73,6 +79,8 @@ async def load_provider_search( f'BACKEND DOES NOT (yet) support symcaching: `{brokermod.name}`' ) + task_status.started() + # keep search engine stream up until cancelled await trio.sleep_forever() @@ -85,14 +93,23 @@ async def _async_main( syms: list[str], brokers: dict[str, ModuleType], loglevel: str, + exec_mode: str = 'live', ) -> None: - """ + ''' Main Qt-trio routine invoked by the Qt loop with the widgets ``dict``. Provision the "main" widget with initial symbol data and root nursery. - """ + `exec_mode` selects 'live' or 'paper' EMS for this chart session, + including subsequent symbol loads through `GodWidget`. + + ''' + # Reject typos before chart tasks can request a live EMS dialog. + if exec_mode not in ('live', 'paper'): + raise ValueError(f'Invalid EMS execution mode: {exec_mode!r}') + main_widget.exec_mode = exec_mode + # enable chart's console logging if loglevel: get_console_log( @@ -168,7 +185,7 @@ async def _async_main( # load other providers into search **after** # the chart's select cache for brokername, mod in needed_brokermods.items(): - tn.start_soon( + await tn.start( load_provider_search, mod, loglevel, diff --git a/piker/ui/_display.py b/piker/ui/_display.py index 06afd142..bdc711d1 100644 --- a/piker/ui/_display.py +++ b/piker/ui/_display.py @@ -1298,6 +1298,7 @@ async def display_symbol_data( fqmes: list[str], loglevel: str, order_mode_started: trio.Event, + exec_mode: str = 'live', ) -> None: ''' @@ -1669,7 +1670,8 @@ async def display_symbol_data( godwidget, order_ctl_fqme, order_mode_started, - loglevel=loglevel + loglevel=loglevel, + exec_mode=exec_mode, ) as mode, # TODO: maybe have these startup sooner before diff --git a/piker/ui/_exec.py b/piker/ui/_exec.py index 2236c7cc..a5a05097 100644 --- a/piker/ui/_exec.py +++ b/piker/ui/_exec.py @@ -312,10 +312,10 @@ def start_qtractor( window.set_shutdown_callback(session.request_shutdown) # override tractor's defaults - tractor_kwargs = dict(tractor_kwargs or {}) - tractor_kwargs.update( - get_runtime_vars() - ) + tractor_kwargs = { + **get_runtime_vars(), + **(tractor_kwargs or {}), + } # define tractor entrypoint async def main() -> None: @@ -405,7 +405,7 @@ def run_qtractor( ) session.completed.connect(session.app.quit) try: - session.app.exec_() + session.app.exec() finally: try: session.completed.disconnect(session.app.quit) diff --git a/piker/ui/_widget.py b/piker/ui/_widget.py index 459a799b..27a50e34 100644 --- a/piker/ui/_widget.py +++ b/piker/ui/_widget.py @@ -80,6 +80,8 @@ class GodWidget(QWidget): ''' search: SearchWidget mode_name: str = 'god' + # `_async_main()` selects EMS execution before any symbol loads. + exec_mode: str = 'live' def __init__( @@ -207,6 +209,7 @@ class GodWidget(QWidget): fqmes, loglevel, order_mode_started, + self.exec_mode, ) # self.vbox.addWidget(hist_charts) diff --git a/piker/ui/order_mode.py b/piker/ui/order_mode.py index 0419a42f..31e07a8f 100644 --- a/piker/ui/order_mode.py +++ b/piker/ui/order_mode.py @@ -785,7 +785,8 @@ async def open_order_mode( godw: GodWidget, fqme: str, started: trio.Event, - loglevel: str = 'info' + loglevel: str = 'info', + exec_mode: str = 'live', ) -> None: ''' @@ -816,6 +817,7 @@ async def open_order_mode( async with ( open_ems( fqme, + mode=exec_mode, loglevel=loglevel, ) as ( client, diff --git a/tests/ui/test_chart_startup.py b/tests/ui/test_chart_startup.py new file mode 100644 index 00000000..929a1f64 --- /dev/null +++ b/tests/ui/test_chart_startup.py @@ -0,0 +1,76 @@ +from inspect import ( + BoundArguments, + signature, +) + +import pytest +import trio + +from piker.ui._app import _async_main +from piker.ui._display import display_symbol_data +from piker.ui._widget import GodWidget +from piker.ui.order_mode import open_order_mode + + +@pytest.mark.parametrize('exec_mode', ['', 'Paper', 'dark']) +def test_chart_rejects_invalid_ems_mode(exec_mode: str) -> None: + ''' + Reject misspelled execution modes before touching chart startup. + + EMS treats non-paper mode as live when a backend supports trades. + A chart caller must not accidentally request that path with a + typo or the order-trigger mode 'dark'. Invoke the actual startup + coroutine with no widget or backend: ValueError, rather than a + widget access or runtime error, proves rejection precedes chart + initialization and any tasks that could acquire brokerd. + + This is a direct input-boundary test, not an EMS integration test. + + ''' + with pytest.raises(ValueError, match='Invalid EMS execution mode'): + trio.run( + _async_main, + None, + [], + {}, + 'warning', + exec_mode, + ) + + +def test_chart_ems_mode_call_contract() -> None: + ''' + Preserve the shipped CLI argument shape while enabling paper EMS. + + `_main()` passes only widget, symbols, brokers and loglevel to + `_async_main()`. Bind that concrete call shape and apply defaults + to verify it remains live; bind an explicit paper argument to + verify integration callers need no CLI or runtime adapter changes. + Check the widget and lower entrypoint defaults as well, since + omitted arguments must not silently change shipped execution. + + These are direct API-contract assertions. They do not claim to + exercise forwarding through a running chart or broker selection. + + ''' + cli_call: BoundArguments = signature(_async_main).bind( + None, + ['xbtusd.kraken'], + {}, + 'warning', + ) + cli_call.apply_defaults() + assert cli_call.arguments['exec_mode'] == 'live' + + paper_call: BoundArguments = signature(_async_main).bind( + *cli_call.args[:-1], + exec_mode='paper', + ) + assert paper_call.arguments['exec_mode'] == 'paper' + assert GodWidget.exec_mode == 'live' + assert signature(display_symbol_data).parameters[ + 'exec_mode' + ].default == 'live' + assert signature(open_order_mode).parameters[ + 'exec_mode' + ].default == 'live' diff --git a/tests/ui/test_qtractor.py b/tests/ui/test_qtractor.py index 9ed9a68f..7b1e82de 100644 --- a/tests/ui/test_qtractor.py +++ b/tests/ui/test_qtractor.py @@ -1,3 +1,8 @@ +import os +from pathlib import Path +import subprocess +import sys +from textwrap import dedent from types import SimpleNamespace from typing import Any @@ -8,9 +13,11 @@ from PyQt6.QtWidgets import ( ) from pytestqt.qtbot import QtBot from pytestqt.wait_signal import SignalBlocker +import pytest import tractor import trio +from piker.service import get_runtime_vars from piker.ui import _window from piker.ui._exec import ( QtractorSession, @@ -36,6 +43,8 @@ class QtractorTestWidget(QWidget): async def run_until_window_close( main_widget: QtractorTestWidget, exited: list[QtractorTestWidget], + reg_addr: tuple[str, int], + runtime_overrides: dict[str, Any], ) -> None: ''' @@ -49,6 +58,11 @@ async def run_until_window_close( raise RuntimeError( 'No Qtractor session owns the test window' ) + actor: tractor.Actor = tractor.current_actor() + assert reg_addr in actor.accept_addrs + assert tractor.runtime._state._runtime_vars[ + 'piker_vars' + ] == runtime_overrides session.set_ready() try: await trio.sleep_forever() @@ -61,6 +75,7 @@ def test_qtractor_restarts_after_structured_window_close( qapp: QApplication, qtbot: QtBot, reg_addr: tuple[str, int], + monkeypatch: pytest.MonkeyPatch, ) -> None: ''' @@ -80,6 +95,12 @@ def test_qtractor_restarts_after_structured_window_close( startup prove structured teardown completed without quitting the shared Qt application. + Before each start, seed `get_runtime_vars()` with a stale + loopback registry and conflicting runtime overrides. Previously + these globals overwrote explicit caller kwargs. Assertions inside + the guest verify the actual bound address and runtime state before + readiness, rather than merely checking forwarded arguments. + ''' assert isolated_ui_state.qapp is qapp exited: list[QtractorTestWidget] = [] @@ -99,9 +120,23 @@ def test_qtractor_restarts_after_structured_window_close( _: int for _ in range(2): + monkeypatch.setitem( + get_runtime_vars(), + 'registry_addrs', + [('127.0.0.1', 0)], + ) + monkeypatch.setitem( + get_runtime_vars(), + 'tractor_runtime_overrides', + {'stale': True}, + ) session: QtractorSession = start_qtractor( func=run_until_window_close, - args=(exited,), + args=( + exited, + reg_addr, + tractor_kwargs['tractor_runtime_overrides'], + ), main_widget_type=QtractorTestWidget, tractor_kwargs=tractor_kwargs, app=qapp, @@ -146,3 +181,88 @@ def test_qtractor_restarts_after_structured_window_close( qapp.cursorFlashTime(), qapp.styleSheet(), ) == qt_state + + +@pytest.mark.parametrize('fail', [False, True]) +def test_run_qtractor_blocking_adapter( + reg_addr: tuple[str, int], + tmp_path: Path, + fail: bool, + +) -> None: + ''' + The blocking CLI adapter must drive PyQt6 and unwrap its guest. + + `run_qtractor()` called the removed `QApplication.exec_()` API, + failing before Trio could start. Run the real adapter in a fresh + process with an offscreen application and test-owned registry and + config. The guest either returns normally or raises a sentinel + error after registering its window for inspection. + + Guest completion must quit `QApplication.exec()` without a timer + or external shutdown. A bounded subprocess wait detects hangs; + exit status, propagated error identity, and the cleared actor and + window slots prove both outcome paths finish structured teardown. + Process isolation keeps the CLI's Qt ownership out of pytest-qt. + + ''' + script: str = dedent(f''' + from PyQt6.QtGui import QResizeEvent + from PyQt6.QtWidgets import QWidget + import tractor + from piker.ui import _window + from piker.ui._exec import run_qtractor + + class Widget(QWidget): + def on_win_resize(self, event: QResizeEvent) -> None: + pass + + windows: list[QWidget] = [] + error = RuntimeError('qtractor guest sentinel') + + async def main(widget: Widget) -> None: + windows.append(widget.window) + assert {reg_addr!r} in ( + tractor.current_actor().accept_addrs + ) + if {fail!r}: + raise error + + try: + run_qtractor( + func=main, + args=(), + main_widget_type=Widget, + tractor_kwargs={{ + 'registry_addrs': [{reg_addr!r}], + 'tractor_runtime_overrides': {{ + 'piker_test_dir': {str(tmp_path)!r}, + }}, + }}, + ) + except RuntimeError as exc: + assert {fail!r} + assert exc is error + else: + assert not {fail!r} + + assert len(windows) == 1 + assert windows[0].qtractor_session is None + assert _window._qt_win is None + assert tractor.current_actor( + err_on_no_runtime=False, + ) is None + ''') + result: subprocess.CompletedProcess[str] = subprocess.run( + [sys.executable, '-c', script], + env={ + **os.environ, + 'QT_QPA_PLATFORM': 'offscreen', + 'XDG_CONFIG_HOME': str(tmp_path / 'config'), + 'XDG_CONFIG_DIRS': str(tmp_path / 'system-config'), + }, + capture_output=True, + text=True, + timeout=60, + ) + assert result.returncode == 0, result.stdout + result.stderr