Fix `Qtractor` startup and paper-chart prerequisites
Let explicit runtime kwargs override stale globals and drive the
blocking adapter through PyQt6's `QApplication.exec()`. Exercise
registry selection and normal/error completion with real runtimes.
Thread an explicit paper execution mode through chart startup and
subsequent symbol loads to EMS, retaining the live CLI default.
Reject invalid modes before initialization and await provider search
registration before publishing application readiness.
Also, correct the credential-free Kraken `Client(config={})` call.
The public-data run progressed past this constructor failure but
still exposed display readiness, Qt paint, and daemon teardown
defects. This is a prerequisite fix, not live-chart qualification;
the failing opt-in reproducer remains outside this boundary.
Retain the combined session provenance, including failed live-run
evidence. Its orphan report predates the later human-authorized
SIGINT cleanup, which stopped the exact remaining paper actor.
Prompt-IO: ai/prompt-io/opencode/20260908T215045Z_1a2d4893_prompt_io.md
(this patch was generated in some part by `opencode` using `gpt-6-astra` (`openai`))
wkt/live_chart_qtractor
parent
1a2d489397
commit
5c17419c93
|
|
@ -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.
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -762,7 +762,7 @@ async def get_client() -> Client:
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
client = Client(
|
client = Client(
|
||||||
conf={},
|
config={},
|
||||||
httpx_client=trio_client,
|
httpx_client=trio_client,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,11 +47,17 @@ log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def load_provider_search(
|
async def load_provider_search(
|
||||||
brokermod: str,
|
brokermod: ModuleType,
|
||||||
loglevel: str,
|
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
|
name = brokermod.name
|
||||||
log.info(f'loading datad for {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}`'
|
f'BACKEND DOES NOT (yet) support symcaching: `{brokermod.name}`'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
task_status.started()
|
||||||
|
|
||||||
# keep search engine stream up until cancelled
|
# keep search engine stream up until cancelled
|
||||||
await trio.sleep_forever()
|
await trio.sleep_forever()
|
||||||
|
|
||||||
|
|
@ -85,14 +93,23 @@ async def _async_main(
|
||||||
syms: list[str],
|
syms: list[str],
|
||||||
brokers: dict[str, ModuleType],
|
brokers: dict[str, ModuleType],
|
||||||
loglevel: str,
|
loglevel: str,
|
||||||
|
exec_mode: str = 'live',
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
'''
|
||||||
Main Qt-trio routine invoked by the Qt loop with the widgets ``dict``.
|
Main Qt-trio routine invoked by the Qt loop with the widgets ``dict``.
|
||||||
|
|
||||||
Provision the "main" widget with initial symbol data and root nursery.
|
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
|
# enable chart's console logging
|
||||||
if loglevel:
|
if loglevel:
|
||||||
get_console_log(
|
get_console_log(
|
||||||
|
|
@ -168,7 +185,7 @@ async def _async_main(
|
||||||
# load other providers into search **after**
|
# load other providers into search **after**
|
||||||
# the chart's select cache
|
# the chart's select cache
|
||||||
for brokername, mod in needed_brokermods.items():
|
for brokername, mod in needed_brokermods.items():
|
||||||
tn.start_soon(
|
await tn.start(
|
||||||
load_provider_search,
|
load_provider_search,
|
||||||
mod,
|
mod,
|
||||||
loglevel,
|
loglevel,
|
||||||
|
|
|
||||||
|
|
@ -1298,6 +1298,7 @@ async def display_symbol_data(
|
||||||
fqmes: list[str],
|
fqmes: list[str],
|
||||||
loglevel: str,
|
loglevel: str,
|
||||||
order_mode_started: trio.Event,
|
order_mode_started: trio.Event,
|
||||||
|
exec_mode: str = 'live',
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
|
|
@ -1669,7 +1670,8 @@ async def display_symbol_data(
|
||||||
godwidget,
|
godwidget,
|
||||||
order_ctl_fqme,
|
order_ctl_fqme,
|
||||||
order_mode_started,
|
order_mode_started,
|
||||||
loglevel=loglevel
|
loglevel=loglevel,
|
||||||
|
exec_mode=exec_mode,
|
||||||
) as mode,
|
) as mode,
|
||||||
|
|
||||||
# TODO: maybe have these startup sooner before
|
# TODO: maybe have these startup sooner before
|
||||||
|
|
|
||||||
|
|
@ -312,10 +312,10 @@ def start_qtractor(
|
||||||
window.set_shutdown_callback(session.request_shutdown)
|
window.set_shutdown_callback(session.request_shutdown)
|
||||||
|
|
||||||
# override tractor's defaults
|
# override tractor's defaults
|
||||||
tractor_kwargs = dict(tractor_kwargs or {})
|
tractor_kwargs = {
|
||||||
tractor_kwargs.update(
|
**get_runtime_vars(),
|
||||||
get_runtime_vars()
|
**(tractor_kwargs or {}),
|
||||||
)
|
}
|
||||||
|
|
||||||
# define tractor entrypoint
|
# define tractor entrypoint
|
||||||
async def main() -> None:
|
async def main() -> None:
|
||||||
|
|
@ -405,7 +405,7 @@ def run_qtractor(
|
||||||
)
|
)
|
||||||
session.completed.connect(session.app.quit)
|
session.completed.connect(session.app.quit)
|
||||||
try:
|
try:
|
||||||
session.app.exec_()
|
session.app.exec()
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
session.completed.disconnect(session.app.quit)
|
session.completed.disconnect(session.app.quit)
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,8 @@ class GodWidget(QWidget):
|
||||||
'''
|
'''
|
||||||
search: SearchWidget
|
search: SearchWidget
|
||||||
mode_name: str = 'god'
|
mode_name: str = 'god'
|
||||||
|
# `_async_main()` selects EMS execution before any symbol loads.
|
||||||
|
exec_mode: str = 'live'
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
||||||
|
|
@ -207,6 +209,7 @@ class GodWidget(QWidget):
|
||||||
fqmes,
|
fqmes,
|
||||||
loglevel,
|
loglevel,
|
||||||
order_mode_started,
|
order_mode_started,
|
||||||
|
self.exec_mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
# self.vbox.addWidget(hist_charts)
|
# self.vbox.addWidget(hist_charts)
|
||||||
|
|
|
||||||
|
|
@ -785,7 +785,8 @@ async def open_order_mode(
|
||||||
godw: GodWidget,
|
godw: GodWidget,
|
||||||
fqme: str,
|
fqme: str,
|
||||||
started: trio.Event,
|
started: trio.Event,
|
||||||
loglevel: str = 'info'
|
loglevel: str = 'info',
|
||||||
|
exec_mode: str = 'live',
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
|
|
@ -816,6 +817,7 @@ async def open_order_mode(
|
||||||
async with (
|
async with (
|
||||||
open_ems(
|
open_ems(
|
||||||
fqme,
|
fqme,
|
||||||
|
mode=exec_mode,
|
||||||
loglevel=loglevel,
|
loglevel=loglevel,
|
||||||
) as (
|
) as (
|
||||||
client,
|
client,
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
@ -1,3 +1,8 @@
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from textwrap import dedent
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
@ -8,9 +13,11 @@ from PyQt6.QtWidgets import (
|
||||||
)
|
)
|
||||||
from pytestqt.qtbot import QtBot
|
from pytestqt.qtbot import QtBot
|
||||||
from pytestqt.wait_signal import SignalBlocker
|
from pytestqt.wait_signal import SignalBlocker
|
||||||
|
import pytest
|
||||||
import tractor
|
import tractor
|
||||||
import trio
|
import trio
|
||||||
|
|
||||||
|
from piker.service import get_runtime_vars
|
||||||
from piker.ui import _window
|
from piker.ui import _window
|
||||||
from piker.ui._exec import (
|
from piker.ui._exec import (
|
||||||
QtractorSession,
|
QtractorSession,
|
||||||
|
|
@ -36,6 +43,8 @@ class QtractorTestWidget(QWidget):
|
||||||
async def run_until_window_close(
|
async def run_until_window_close(
|
||||||
main_widget: QtractorTestWidget,
|
main_widget: QtractorTestWidget,
|
||||||
exited: list[QtractorTestWidget],
|
exited: list[QtractorTestWidget],
|
||||||
|
reg_addr: tuple[str, int],
|
||||||
|
runtime_overrides: dict[str, Any],
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
|
|
@ -49,6 +58,11 @@ async def run_until_window_close(
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
'No Qtractor session owns the test window'
|
'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()
|
session.set_ready()
|
||||||
try:
|
try:
|
||||||
await trio.sleep_forever()
|
await trio.sleep_forever()
|
||||||
|
|
@ -61,6 +75,7 @@ def test_qtractor_restarts_after_structured_window_close(
|
||||||
qapp: QApplication,
|
qapp: QApplication,
|
||||||
qtbot: QtBot,
|
qtbot: QtBot,
|
||||||
reg_addr: tuple[str, int],
|
reg_addr: tuple[str, int],
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
|
|
@ -80,6 +95,12 @@ def test_qtractor_restarts_after_structured_window_close(
|
||||||
startup prove structured teardown completed without quitting the
|
startup prove structured teardown completed without quitting the
|
||||||
shared Qt application.
|
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
|
assert isolated_ui_state.qapp is qapp
|
||||||
exited: list[QtractorTestWidget] = []
|
exited: list[QtractorTestWidget] = []
|
||||||
|
|
@ -99,9 +120,23 @@ def test_qtractor_restarts_after_structured_window_close(
|
||||||
|
|
||||||
_: int
|
_: int
|
||||||
for _ in range(2):
|
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(
|
session: QtractorSession = start_qtractor(
|
||||||
func=run_until_window_close,
|
func=run_until_window_close,
|
||||||
args=(exited,),
|
args=(
|
||||||
|
exited,
|
||||||
|
reg_addr,
|
||||||
|
tractor_kwargs['tractor_runtime_overrides'],
|
||||||
|
),
|
||||||
main_widget_type=QtractorTestWidget,
|
main_widget_type=QtractorTestWidget,
|
||||||
tractor_kwargs=tractor_kwargs,
|
tractor_kwargs=tractor_kwargs,
|
||||||
app=qapp,
|
app=qapp,
|
||||||
|
|
@ -146,3 +181,88 @@ def test_qtractor_restarts_after_structured_window_close(
|
||||||
qapp.cursorFlashTime(),
|
qapp.cursorFlashTime(),
|
||||||
qapp.styleSheet(),
|
qapp.styleSheet(),
|
||||||
) == qt_state
|
) == 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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue