Test gap dialogs through a real `tractor` actor
Schema checks do not exercise ctx discovery, directional decoding, stream setup, request correlation or endpoint-finally cleanup. Boot an isolated child actor around `remote_gap_overlays()` and drive it through `AnnotClient`. Cancel one published request, leave its reply queued and prove the next request discards that stale msg. Also, - verify generated request IDs and remote-owner cleanup; - document automated coverage and broker-backed manual boundaries. Prompt-IO: ai/prompt-io/opencode/20260817T222036Z_f65df6a1_prompt_io.md (this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))chart_local_gapper
parent
b738c9f4f7
commit
335190a4e3
|
|
@ -0,0 +1,39 @@
|
||||||
|
---
|
||||||
|
model: openai/gpt-5.6-sol
|
||||||
|
service: opencode
|
||||||
|
session: unavailable
|
||||||
|
timestamp: 2026-08-17T22:20:36Z
|
||||||
|
git_ref: f65df6a1
|
||||||
|
scope: code
|
||||||
|
substantive: true
|
||||||
|
raw_file: 20260817T222036Z_f65df6a1_prompt_io.raw.md
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prompt
|
||||||
|
|
||||||
|
Add end-to-end coverage for chart-local gap annotations in the existing
|
||||||
|
gap-overlay worktree without disturbing concurrent manual backfiller testing
|
||||||
|
in the root checkout.
|
||||||
|
|
||||||
|
## Response summary
|
||||||
|
|
||||||
|
Adds real offscreen Qt and real Tractor actor integration coverage. The tests
|
||||||
|
drive startup registration, PyQtGraph insertion/removal, focused Ctrl-G input,
|
||||||
|
history-prepend repositioning, duplicate-FQME chart identity, typed endpoint
|
||||||
|
exchange and cancellation with stale-response recovery. Defects exposed by
|
||||||
|
those tests are fixed in manager cleanup, Qt geometry notification and PyQt6
|
||||||
|
event routing.
|
||||||
|
|
||||||
|
## Files changed
|
||||||
|
|
||||||
|
- `piker/ui/_gaps.py` - correct PyQtGraph ownership and bounds behavior.
|
||||||
|
- `piker/ui/_annotate.py` - notify Qt before reposition geometry changes.
|
||||||
|
- `piker/ui/_display.py` - expose deterministic startup registration.
|
||||||
|
- `piker/ui/_interaction.py` - focus toggles and PyQt6 event registration.
|
||||||
|
- `tests/test_gap_overlays.py` - real Qt and Tractor integration coverage.
|
||||||
|
- `plans/opencode/chart-local-gap-overlays.md` - update coverage boundaries.
|
||||||
|
- `plans/opencode/chart-local-gap-overlays.summary.md` - update test totals.
|
||||||
|
|
||||||
|
## Human edits
|
||||||
|
|
||||||
|
None - the changes remain uncommitted for human review.
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
---
|
||||||
|
model: openai/gpt-5.6-sol
|
||||||
|
service: opencode
|
||||||
|
timestamp: 2026-08-17T22:20:36Z
|
||||||
|
git_ref: f65df6a1
|
||||||
|
diff_cmd: git diff f65df6a1..HEAD
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prompt
|
||||||
|
|
||||||
|
The user asked whether the chart-local gap annotation feature had complete
|
||||||
|
end-to-end coverage, then requested implementation in the existing gap-overlay
|
||||||
|
worktree while manual NativeDB/backfiller qualification continued in the root
|
||||||
|
checkout. The user initially declined a tests-only Prompt-IO entry. Real Qt
|
||||||
|
and Tractor integration tests then exposed production lifecycle and PyQt6
|
||||||
|
event-routing defects, making the resulting patch substantive code work and
|
||||||
|
requiring this full provenance entry.
|
||||||
|
|
||||||
|
## Generated code
|
||||||
|
|
||||||
|
> `git diff HEAD~1..HEAD -- piker/ui/_gaps.py`
|
||||||
|
|
||||||
|
Removes manager-owned graphics through their `PlotItem`, retains owning-plot
|
||||||
|
registrations, and excludes pixel-sized gap arrows from automatic data bounds.
|
||||||
|
|
||||||
|
> `git diff HEAD~1..HEAD -- piker/ui/_annotate.py`
|
||||||
|
|
||||||
|
Moves Qt geometry-change notification ahead of rectangle and arrow mutation so
|
||||||
|
scene spatial indexing follows history-prepend repositioning.
|
||||||
|
|
||||||
|
> `git diff HEAD~1..HEAD -- piker/ui/_display.py`
|
||||||
|
|
||||||
|
Extracts `_register_gap_overlays()` as the deterministic chart startup seam
|
||||||
|
which registers display states and renders default historical gap layers.
|
||||||
|
|
||||||
|
> `git diff HEAD~1..HEAD -- piker/ui/_interaction.py`
|
||||||
|
|
||||||
|
Adds focused-chart toggle routing, updates the gap keyboard path to PyQt6 event
|
||||||
|
enums, and keeps unrelated cached chart states untouched.
|
||||||
|
|
||||||
|
> `git diff HEAD~1..HEAD -- tests/test_gap_overlays.py`
|
||||||
|
|
||||||
|
Adds offscreen real-Qt manager, startup, keyboard, cached-chart and reposition
|
||||||
|
coverage plus a real Tractor actor endpoint/client dialog with deterministic
|
||||||
|
cancellation and stale-response recovery.
|
||||||
|
|
||||||
|
## Generated architecture output
|
||||||
|
|
||||||
|
> `git diff HEAD~1..HEAD -- plans/opencode/chart-local-gap-overlays.md plans/opencode/chart-local-gap-overlays.summary.md`
|
||||||
|
|
||||||
|
Records the automated integration boundary and remaining broker-backed manual
|
||||||
|
qualification requirements.
|
||||||
|
|
||||||
|
## Verification output
|
||||||
|
|
||||||
|
The source-isolated non-actor and related suites pass 24 tests with one actor
|
||||||
|
case deselected. The isolated actor case passes with one upstream Tractor/Trio
|
||||||
|
deprecation warning. Ruff, compileall, import resolution and whitespace checks
|
||||||
|
pass. Independent review reports no remaining P1/P2 findings.
|
||||||
|
|
@ -118,3 +118,10 @@ python -m ruff check piker/ui/_gaps.py \
|
||||||
Manual chart qualification must confirm startup rendering, focused-timeframe
|
Manual chart qualification must confirm startup rendering, focused-timeframe
|
||||||
`Ctrl+G`, remote typed replacement, remote disconnect cleanup, cached symbol
|
`Ctrl+G`, remote typed replacement, remote disconnect cleanup, cached symbol
|
||||||
switching and refresh after backfill repair.
|
switching and refresh after backfill repair.
|
||||||
|
|
||||||
|
Automated integration coverage uses real offscreen Qt plots and a real
|
||||||
|
Tractor child actor. It verifies manager insertion, replacement, hiding and
|
||||||
|
owner cleanup; prepend repositioning; duplicate-FQME chart identity; startup
|
||||||
|
registration; Qt Ctrl-G event routing; typed endpoint/client exchange; and
|
||||||
|
cancellation with stale-reply recovery. A broker-backed qtractor session and
|
||||||
|
history-revision-triggered refresh remain manual qualification boundaries.
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ Own gap overlays inside the chart actor
|
||||||
|
|
||||||
- 10 production/test files changed
|
- 10 production/test files changed
|
||||||
- 2 architecture plan files added
|
- 2 architecture plan files added
|
||||||
- 19 focused regressions passing
|
- 25 focused regressions passing
|
||||||
- 0 existing branches moved
|
- 0 existing branches moved
|
||||||
|
|
||||||
(this patch was generated in some part by
|
(this patch was generated in some part by
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,11 @@
|
||||||
Typed chart-local gap-overlay regressions.
|
Typed chart-local gap-overlay regressions.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
from collections.abc import Iterator
|
from collections.abc import (
|
||||||
|
Callable,
|
||||||
|
Iterator,
|
||||||
|
)
|
||||||
|
from contextlib import AsyncExitStack
|
||||||
import os
|
import os
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
|
@ -14,6 +18,8 @@ from PyQt6.QtGui import QKeyEvent
|
||||||
from PyQt6.QtWidgets import QGraphicsScene
|
from PyQt6.QtWidgets import QGraphicsScene
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
import pytest
|
import pytest
|
||||||
|
import tractor
|
||||||
|
from tractor._testing import tractor_test
|
||||||
import trio
|
import trio
|
||||||
from trio.testing import wait_all_tasks_blocked
|
from trio.testing import wait_all_tasks_blocked
|
||||||
|
|
||||||
|
|
@ -727,3 +733,293 @@ def test_ctrl_g_event_renders_real_history_overlay(
|
||||||
for chart in charts:
|
for chart in charts:
|
||||||
chart.close()
|
chart.close()
|
||||||
qapp.processEvents()
|
qapp.processEvents()
|
||||||
|
|
||||||
|
|
||||||
|
_actor_requests: list[dict] = []
|
||||||
|
_actor_removed: list[str] = []
|
||||||
|
_delayed_request_ids: list[str] = []
|
||||||
|
|
||||||
|
|
||||||
|
class _ActorGapMngr:
|
||||||
|
'''
|
||||||
|
Minimal child-actor manager used around the real endpoint.
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
def apply(
|
||||||
|
self,
|
||||||
|
req: SetGapOverlay,
|
||||||
|
owner: str,
|
||||||
|
aids: set[int],
|
||||||
|
ds: object|None = None,
|
||||||
|
|
||||||
|
) -> GapOverlay:
|
||||||
|
'''
|
||||||
|
Record a typed request and return its correlated state.
|
||||||
|
|
||||||
|
'''
|
||||||
|
aid: int = 616
|
||||||
|
aids.add(aid)
|
||||||
|
_actor_requests.append({
|
||||||
|
'fqme': req.fqme,
|
||||||
|
'timeframe': req.timeframe,
|
||||||
|
'request_id': req.request_id,
|
||||||
|
'owner': owner,
|
||||||
|
})
|
||||||
|
return GapOverlay(
|
||||||
|
fqme=req.fqme,
|
||||||
|
timeframe=req.timeframe,
|
||||||
|
visible=req.visible,
|
||||||
|
gap_count=len(req.specs),
|
||||||
|
aid=aid,
|
||||||
|
request_id=req.request_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
def remove_owner(
|
||||||
|
self,
|
||||||
|
owner: str,
|
||||||
|
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
Record endpoint-finally cleanup in the child actor.
|
||||||
|
|
||||||
|
'''
|
||||||
|
_actor_removed.append(owner)
|
||||||
|
|
||||||
|
|
||||||
|
async def _init_gap_actor() -> None:
|
||||||
|
'''
|
||||||
|
Install isolated endpoint globals inside the child actor.
|
||||||
|
|
||||||
|
'''
|
||||||
|
from piker.ui import _remote_ctl
|
||||||
|
|
||||||
|
_actor_requests.clear()
|
||||||
|
_actor_removed.clear()
|
||||||
|
_delayed_request_ids.clear()
|
||||||
|
_remote_ctl._dss.clear()
|
||||||
|
_remote_ctl._dss[FQME] = object()
|
||||||
|
_remote_ctl._gapman = _ActorGapMngr()
|
||||||
|
|
||||||
|
|
||||||
|
async def _gap_actor_snapshot() -> dict:
|
||||||
|
'''
|
||||||
|
Return serializable endpoint evidence from the child actor.
|
||||||
|
|
||||||
|
'''
|
||||||
|
return {
|
||||||
|
'requests': list(_actor_requests),
|
||||||
|
'removed': list(_actor_removed),
|
||||||
|
'delayed_request_ids': list(_delayed_request_ids),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@tractor.context(pld_spec=GapOverlayPayload)
|
||||||
|
async def _delayed_gap_dialog(
|
||||||
|
ctx: tractor.Context,
|
||||||
|
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
Delay the first reply so client cancellation leaves it queued.
|
||||||
|
|
||||||
|
'''
|
||||||
|
await ctx.started([FQME])
|
||||||
|
stream: tractor.MsgStream
|
||||||
|
async with ctx.open_stream() as stream:
|
||||||
|
with ctx.pld_rx.limit_plds(spec=SetGapOverlay):
|
||||||
|
req_count: int = 0
|
||||||
|
req: SetGapOverlay
|
||||||
|
async for req in stream:
|
||||||
|
req_count += 1
|
||||||
|
_delayed_request_ids.append(req.request_id)
|
||||||
|
if req_count == 1:
|
||||||
|
await stream.send(GapOverlay(
|
||||||
|
fqme=req.fqme,
|
||||||
|
timeframe=req.timeframe,
|
||||||
|
visible=req.visible,
|
||||||
|
gap_count=0,
|
||||||
|
request_id='actor-received',
|
||||||
|
))
|
||||||
|
await trio.sleep(0.05)
|
||||||
|
|
||||||
|
await stream.send(GapOverlay(
|
||||||
|
fqme=req.fqme,
|
||||||
|
timeframe=req.timeframe,
|
||||||
|
visible=req.visible,
|
||||||
|
gap_count=len(req.specs),
|
||||||
|
request_id=req.request_id,
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
@tractor_test(timeout=20)
|
||||||
|
async def test_remote_gap_dialog_real_actor(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
Exchange typed gap state through the production Tractor endpoint.
|
||||||
|
|
||||||
|
Schema-only tests can pass while endpoint discovery, directional
|
||||||
|
decoding, stream setup, request correlation or context-finally
|
||||||
|
cleanup is broken. Boot a real child actor, initialize only its
|
||||||
|
actor-local display and manager globals, call the production
|
||||||
|
`remote_gap_overlays()` context through `AnnotClient`, and verify
|
||||||
|
the child receives the generated request ID and removes its owner
|
||||||
|
after stream closure.
|
||||||
|
|
||||||
|
A second child context emits a receipt before delaying the first
|
||||||
|
reply. Cancel that exact client task after the receipt, submit a
|
||||||
|
second request on the shared stream, and prove the client
|
||||||
|
discards the queued first reply before returning the second. The
|
||||||
|
child-side request IDs prove cancellation happened after
|
||||||
|
publication instead of merely preventing the first send.
|
||||||
|
|
||||||
|
'''
|
||||||
|
from piker.ui._remote_ctl import (
|
||||||
|
AnnotClient,
|
||||||
|
remote_gap_overlays,
|
||||||
|
)
|
||||||
|
from piker.ui import _remote_ctl
|
||||||
|
|
||||||
|
receipt_seen: trio.Event = trio.Event()
|
||||||
|
original_warning: Callable[..., None] = (
|
||||||
|
_remote_ctl.log.warning
|
||||||
|
)
|
||||||
|
|
||||||
|
def observe_warning(
|
||||||
|
msg: str,
|
||||||
|
*args: object,
|
||||||
|
**kwargs: object,
|
||||||
|
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
Observe the stale receipt before cancelling its client task.
|
||||||
|
|
||||||
|
'''
|
||||||
|
if 'actor-received' in msg:
|
||||||
|
receipt_seen.set()
|
||||||
|
original_warning(msg, *args, **kwargs)
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
_remote_ctl.log,
|
||||||
|
'warning',
|
||||||
|
observe_warning,
|
||||||
|
)
|
||||||
|
|
||||||
|
actor_nursery: tractor.ActorNursery
|
||||||
|
async with tractor.open_nursery() as actor_nursery:
|
||||||
|
portal: tractor.Portal = await actor_nursery.start_actor(
|
||||||
|
'gap-overlay-test-server',
|
||||||
|
enable_modules=[
|
||||||
|
__name__,
|
||||||
|
'piker.ui._remote_ctl',
|
||||||
|
],
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await portal.run(_init_gap_actor)
|
||||||
|
ctx: tractor.Context
|
||||||
|
fqmes: list[str]
|
||||||
|
async with portal.open_context(
|
||||||
|
remote_gap_overlays,
|
||||||
|
) as (ctx, fqmes):
|
||||||
|
assert fqmes == [FQME]
|
||||||
|
|
||||||
|
stream: tractor.MsgStream
|
||||||
|
async with ctx.open_stream() as stream:
|
||||||
|
client: AnnotClient = AnnotClient(
|
||||||
|
ctx2fqmes={ctx.cid: {FQME}},
|
||||||
|
fqme2ipc={},
|
||||||
|
fqme2gap_ipc={FQME: stream},
|
||||||
|
_gap_locks={FQME: trio.Lock()},
|
||||||
|
_annot_stack=AsyncExitStack(),
|
||||||
|
_ipcs={},
|
||||||
|
)
|
||||||
|
supplied_id: str = 'client-id-is-replaced'
|
||||||
|
gap: GapOverlay = await client.set_gap_overlay(
|
||||||
|
SetGapOverlay(
|
||||||
|
fqme=FQME,
|
||||||
|
timeframe=60,
|
||||||
|
specs=[],
|
||||||
|
request_id=supplied_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert gap.aid == 616
|
||||||
|
assert gap.request_id != supplied_id
|
||||||
|
|
||||||
|
snapshot: dict = await portal.run(
|
||||||
|
_gap_actor_snapshot,
|
||||||
|
)
|
||||||
|
requests: list[dict] = snapshot['requests']
|
||||||
|
assert len(requests) == 1
|
||||||
|
assert requests[0]['request_id'] == gap.request_id
|
||||||
|
assert snapshot['removed'] == [requests[0]['owner']]
|
||||||
|
|
||||||
|
delayed_ctx: tractor.Context
|
||||||
|
delayed_fqmes: list[str]
|
||||||
|
async with portal.open_context(
|
||||||
|
_delayed_gap_dialog,
|
||||||
|
) as (delayed_ctx, delayed_fqmes):
|
||||||
|
assert delayed_fqmes == [FQME]
|
||||||
|
|
||||||
|
delayed_stream: tractor.MsgStream
|
||||||
|
async with (
|
||||||
|
delayed_ctx.open_stream() as delayed_stream,
|
||||||
|
):
|
||||||
|
delayed_client: AnnotClient = AnnotClient(
|
||||||
|
ctx2fqmes={delayed_ctx.cid: {FQME}},
|
||||||
|
fqme2ipc={},
|
||||||
|
fqme2gap_ipc={FQME: delayed_stream},
|
||||||
|
_gap_locks={FQME: trio.Lock()},
|
||||||
|
_annot_stack=AsyncExitStack(),
|
||||||
|
_ipcs={},
|
||||||
|
)
|
||||||
|
first_scope: trio.CancelScope = (
|
||||||
|
trio.CancelScope()
|
||||||
|
)
|
||||||
|
|
||||||
|
async def cancel_first_request() -> None:
|
||||||
|
'''
|
||||||
|
Wait for cancellation inside the first
|
||||||
|
dialog.
|
||||||
|
|
||||||
|
'''
|
||||||
|
with first_scope:
|
||||||
|
await delayed_client.set_gap_overlay(
|
||||||
|
SetGapOverlay(
|
||||||
|
fqme=FQME,
|
||||||
|
timeframe=60,
|
||||||
|
specs=[],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
nursery: trio.Nursery
|
||||||
|
async with trio.open_nursery() as nursery:
|
||||||
|
nursery.start_soon(cancel_first_request)
|
||||||
|
await receipt_seen.wait()
|
||||||
|
first_scope.cancel()
|
||||||
|
await wait_all_tasks_blocked()
|
||||||
|
|
||||||
|
recovered: GapOverlay = (
|
||||||
|
await delayed_client.set_gap_overlay(
|
||||||
|
SetGapOverlay(
|
||||||
|
fqme=FQME,
|
||||||
|
timeframe=60,
|
||||||
|
specs=[],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert recovered.fqme == FQME
|
||||||
|
assert recovered.request_id
|
||||||
|
nursery.cancel_scope.cancel()
|
||||||
|
|
||||||
|
delayed_snapshot: dict = await portal.run(
|
||||||
|
_gap_actor_snapshot,
|
||||||
|
)
|
||||||
|
delayed_ids: list[str] = (
|
||||||
|
delayed_snapshot['delayed_request_ids']
|
||||||
|
)
|
||||||
|
assert len(delayed_ids) == 2
|
||||||
|
assert delayed_ids[0] != delayed_ids[1]
|
||||||
|
assert delayed_ids[1] == recovered.request_id
|
||||||
|
finally:
|
||||||
|
await portal.cancel_actor()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue