Commit 457416d
fix(core): restore asyncio.StreamReader inheritance on ACP stdio wrappers (#152)
* fix(core): restore asyncio.StreamReader inheritance on ACP stdio wrappers
ClientSideConnection.__init__ in the upstream ACP SDK enforces
isinstance(input_stream, asyncio.StreamWriter) and
isinstance(output_stream, asyncio.StreamReader)
before constructing the JSON-RPC connection. The earlier branch commit
1207f27 ("refactor(core): P1+P2-A/B dead code") rewrote
JsonRpcObjectStreamReader and _ByteCountingStreamReader as plain
composition classes on the assumption that the inheritance was
decorative. It wasn't — it was load-bearing for that isinstance gate.
Released as 0.19.0b34, the wrappers no longer match the gate and the
TUI fails on first ACP turn with:
Agent session failed: ClientSideConnection requires asyncio
StreamWriter/StreamReader
Restore the inheritance on both wrappers without calling super().__init__
(state is fully delegated to the wrapped reader). Document the contract
in the class docstring so a future cleanup pass keeps the inheritance.
Lock the contract with a unit regression test in tests/unit/test_acp_session.py
that constructs a real ClientSideConnection over each wrapper and asserts
the isinstance gate accepts it. The previous unit suite stubbed the
connection out with _FakeConnection / _FakeProcess, so the production
isinstance check was never exercised — by testing.md this is exactly the
"contract acceptance tests don't reach" scenario where a unit test earns
its place.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test: add real-stdio ACP integration harness with hermetic echo agent
Adopts patterns from the ACP reference suite (anthropics/agent-protocol):
- tests/helpers/echo_agent.py — vendored examples/echo_agent.py. A real ACP
agent in 80 lines that we spawn via sys.executable. No provider binary
needed.
- tests/helpers/acp_loopback.py — TCP-loopback fixture that yields real
asyncio.StreamReader/StreamWriter pairs (asyncio.start_server +
asyncio.open_connection). Lets tests construct the real
acp.client.connection.ClientSideConnection without a subprocess.
- tests/integration/acp_real/test_stream_wrappers.py — drives both wrappers
through the real SDK and the full handshake → session → prompt →
notification → teardown roundtrip via spawn_filtered_agent_process.
Fails with the production error message when inheritance is removed:
TypeError: ClientSideConnection requires asyncio StreamWriter/StreamReader
The previous always-on suite stubbed the SDK out entirely (_FakeConnection,
_FakeProcess) so no test ever exercised the production isinstance gate.
That's why 0.19.0b34 shipped a runtime regression with a green CI bar.
docs/internal/testing.md gains a "Real-stdio integration tests" section
explaining the new layer and when to add to it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(core): call super().__init__() on ACP stream wrappers
Greptile review on PR 152 flagged that both wrappers subclass
asyncio.StreamReader but skip super().__init__(), leaving base
attributes (notably _exception) uninitialised — wrapper.exception()
would AttributeError on any error path.
Today's SDK only calls readline() on the wrapped reader, so the gap
is unreachable, but it's a footgun for future SDK upgrades and the
parent constructor is cheap (allocates a small unused buffer).
Call super().__init__() in both wrappers and update docstrings to
reflect the new design. Extend the integration regression test to
assert wrapper.exception() returns None — verified to fail with
"AttributeError: object has no attribute '_exception'" if super()
is skipped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(acp): assert wrapper.exception() in unit regression test too
Mirrors the assertion added to the integration test in the previous
commit. Greptile's review thread on tests/unit/test_acp_session.py:238
explicitly requested this — the unit test stopped at construction so
the un-overridden exception() / set_exception() were never exercised.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(core): delegate exception()/set_exception() to wrapped reader
Greptile review on PR 152 (iter 2) flagged that calling super().__init__()
gives the wrapper its own _exception field that is never written to. The
asyncio transport calls set_exception() on the *wrapped* reader when the
process dies; the wrapper's own _exception stays None, so wrapper.exception()
returns None even after a transport failure.
Override exception() and set_exception() on both wrappers to delegate to
self._reader, so:
- A transport failure on the underlying reader is visible via the wrapper.
- An SDK-level set_exception on the wrapper propagates to the underlying
reader (and any blocked readline()/read() on it).
Add a unit regression test that calls underlying.set_exception(...) and
asserts wrapper.exception() returns the same exception, plus the inverse
direction. Verified the test fails (assert is boom) without the overrides.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 0bc787e commit 457416d
11 files changed
Lines changed: 506 additions & 7 deletions
File tree
- docs/internal
- src/kagan/core
- tests
- helpers
- integration
- acp_real
- unit
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
50 | 50 | | |
51 | 51 | | |
52 | 52 | | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
53 | 79 | | |
54 | 80 | | |
55 | 81 | | |
| |||
140 | 166 | | |
141 | 167 | | |
142 | 168 | | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
143 | 172 | | |
| 173 | + | |
| 174 | + | |
144 | 175 | | |
145 | 176 | | |
146 | 177 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
133 | 133 | | |
134 | 134 | | |
135 | 135 | | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
136 | 139 | | |
137 | 140 | | |
138 | 141 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| 5 | + | |
5 | 6 | | |
6 | | - | |
| 7 | + | |
7 | 8 | | |
8 | 9 | | |
9 | 10 | | |
10 | | - | |
11 | | - | |
12 | 11 | | |
13 | | - | |
14 | | - | |
| 12 | + | |
15 | 13 | | |
16 | 14 | | |
17 | 15 | | |
18 | 16 | | |
19 | 17 | | |
20 | 18 | | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
21 | 25 | | |
22 | 26 | | |
23 | 27 | | |
| 28 | + | |
24 | 29 | | |
25 | 30 | | |
26 | 31 | | |
| |||
59 | 64 | | |
60 | 65 | | |
61 | 66 | | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
62 | 73 | | |
63 | 74 | | |
64 | 75 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
866 | 866 | | |
867 | 867 | | |
868 | 868 | | |
869 | | - | |
| 869 | + | |
870 | 870 | | |
871 | 871 | | |
872 | 872 | | |
873 | 873 | | |
874 | 874 | | |
875 | 875 | | |
| 876 | + | |
| 877 | + | |
| 878 | + | |
| 879 | + | |
| 880 | + | |
| 881 | + | |
876 | 882 | | |
877 | 883 | | |
878 | 884 | | |
| |||
881 | 887 | | |
882 | 888 | | |
883 | 889 | | |
| 890 | + | |
884 | 891 | | |
885 | 892 | | |
886 | 893 | | |
| |||
924 | 931 | | |
925 | 932 | | |
926 | 933 | | |
| 934 | + | |
| 935 | + | |
| 936 | + | |
| 937 | + | |
| 938 | + | |
| 939 | + | |
927 | 940 | | |
928 | 941 | | |
929 | 942 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
Whitespace-only changes.
Whitespace-only changes.
0 commit comments