test(gate): a Docker-free two-node fixture on all five legs (port of CIRISServer's mesh-repro chat scenario) - #130
Conversation
The two-node fixture edits CSD §5 lines ("Flow not complete") and drafts that
exist only on main (#128), so it stacks on #97 with main merged in.
Two conflicts: the vendoring digest (re-recorded), and a semantic one —
#97's test_the_real_csd_005_refuses_its_proposed_trust_chip named
`contacts_row_trust`, which main's CSD-005 no longer marks proposed. The test
now derives a real proposed tag from the shipped CSDs instead of naming one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155SkTGdbwnSnR6tWBqJvUM
…to feat/two-node-fixture
…e chat scenario, without Docker CIRISServer's harness/mesh-repro/scenarios/chat.sh stands two nodes up on a compose bridge, which only the Linux leg can host: macOS runners have no Docker, Windows runners run Windows containers, and neither the Android emulator nor the iOS simulator sees a compose network. testing/gate/two_node.py is the same sequence as a native process from the binary the leg already downloaded, in stdlib Python so all three runner images can run it: - `config set net.listen_addr` / `net.bootstrap_peers` offline before the first boot (node_boot.sh) — own ports 5242/5243, own --home, unique --key-id; - `identity create` + `claim --cohort-scope self` on the peer's console with the PIN from <home>/claim_pin; `POST /v1/federation/announce`; - the leg's node, claimed by the CLIENT's wizard, is signed in to with the same credentials and announced; its owner read off the announce bundle; - `POST /v1/federation/peering` both ways with the production self-key-record (the harness's test-blessed record and test-admit-peer are compiled only into test-anchor builds); - each owner adds the other (`POST /v1/contacts`; falls back to the contact code, then the peer NODE, and records which); both open the pair room; the peer speaks only once the room is keyed; arrival on the leg's node is waited for and recorded, and a message that did not cross is never handed to a flow. `down` kills the peer by its pidfile and deletes its home, for an always() step; TwoNodeFixture tears down on exit and on SIGTERM. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155SkTGdbwnSnR6tWBqJvUM
A flow could not name the thing a second node creates: the receipt's
hamburger is `btn_receipt_<keyId>`, the peer row `peer_pick_row_<keyId>`, the
message `chat_msg_<attestationId>`, and a `click:` takes one literal tag. Every
step that needed one was `optional_step` gated on a tag nothing opened, so it
always skipped (CSD-006/047/091 §5 "Flow not complete").
- flow_spec: a flow-level `fixture:` key (known: two_node) and `${NAME}` in any
tag, text, glob or input value. A name with no fixture to fill it is a LOAD
error — it would otherwise reach the app as the literal `${NAME}` and fail as
"element not found". At run time each step is resolved from the fixture's
values; a value the fixture did not produce FAILS the step, optional or not,
quoting the fixture's own note for why. `matches:` values are regex-escaped.
- run_flows.run_all: plain flows run first whatever the file order (a fixture
changes the leg's node, and people.yaml asserts the bare one); a fixture is
stood up once, just before the first runnable flow that asks for it — a run
with none, or whose fixture flows its floor refuses, never pays for it — and
torn down in a `finally`. A fixture that cannot stand up leaves its flows
cannot-start, naming why.
- run_flows / run_platform: --node-binary, --node-url, --peer-port, --peer-work.
- test_flows: a `${NAME}` tag is carried only if its literal head IS one of the
client's interpolated prefixes.
Shown red first: test_flow_fixtures.py against the previous flow_spec/run_flows
fails at collection (no UnresolvedVariable, no fixture support).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155SkTGdbwnSnR6tWBqJvUM
…own under always() - Every run_platform call (Linux, Android, macOS, iOS, Windows) passes --node-binary node/ciris-server and its own --peer-work under $RUNNER_TEMP. The peer starts only if a loaded, unrefused flow says `fixture: two_node`. - Reachability: the fixture runs on the leg's host and talks to both nodes on loopback. Desktop legs use 127.0.0.1:4243; the Android emulator reaches the same host node through the existing adb reverse and never dials the peer; the iOS app's embedded node shares the host loopback, so it is 127.0.0.1:4243 from the fixture too, and the peer's 5242/5243 do not collide with it. - Linux: the Android leg shares the desktop leg's node. When the desktop fixture seeded it (values.json exists), the node is stopped and the Android leg gets a fresh one on a fresh home, so its bare-node flows are not red for the desktop's reason. - Each job runs `two_node down` for its legs under always() (the runner's `finally` and SIGTERM handler do not survive a SIGKILL), and uploads each peer's log and values.json. - test_five_platform_workflow: the flags on every call, the always() teardown per leg, and the fresh node's position between the two Linux legs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155SkTGdbwnSnR6tWBqJvUM
…nstead of skipping
- csd-006-receipt: refreshes People, opens `btn_receipt_${PEER_KEY_ID}` and
asserts the five facts, the wire dimension and the wire rule, then closes.
- csd-005-people: the populated row, its trust chip and hamburger by key id,
and the receipt step now clicks the seeded contact's hamburger.
- csd-047-network-content: enters from the hub's `tile_federation_content`
(NetworkContent has no nav hop) and picks `peer_pick_row_${PEER_NODE_KEY_ID}`.
- csd-091-user-chat: enters the room from People by
`btn_contacts_chat_${PEER_KEY_ID}` and asserts the peer's message by its
attestation id — a value the leg's node cannot mint for itself.
Measured on the Linux desktop leg, locally, 2026-09-28 (candidate 0.5.224 run
as 0.5.225, node v0.5.217, leg node on 5142/5143, peer on 5242/5243):
people 3/3; csd_006 pass (5/6, grant-less step skipped as designed); csd_005
row/chip/hamburger/receipt pass, then fails at btn_scan_contact_code (the
desktop shows btn_scan_contact_code_status — unrelated to the fixture);
csd_091 entry/composer/refresh pass, history fails naming why; csd_047 cannot
start (nav_map's hop to LayerGlobalCommons stops on CircleTab).
Why 091 cannot pass on the released line is recorded in
evidence/blocked_upstream.tsv: two unconferred v0.5.217 nodes admit each other
ADVISORY, frames fail the SignedTransportDestination check, and the pair room
never keys in 480 s. CSD §5 lines and both flow READMEs say so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155SkTGdbwnSnR6tWBqJvUM
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155SkTGdbwnSnR6tWBqJvUM
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9af174cfa4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # `finally` unless it is turned into an exit. The peer runs in its own | ||
| # session, so nothing else would reap it. (The workflow also runs | ||
| # `two_node down` under `if: always()` for a SIGKILL.) | ||
| atexit.register(self.down) |
There was a problem hiding this comment.
Keep the standalone peer alive after
up
When the documented python3 -m testing.gate.two_node up ... command succeeds, main() immediately exits and this registered callback invokes self.down(), terminating the peer, removing its PID file, and deleting its home. Consequently the command advertised as “leave the peer running” cannot be used with the separate down command; limit this exit cleanup to the in-process flow-runner path or unregister it before the standalone up command returns.
Useful? React with 👍 / 👎.
…ode down The exit/SIGTERM cleanup that protects the in-process runner also fired when the standalone `up` command returned, killing the peer it was asked to leave running (Codex, PR #130). `up` now detaches after seeding; test red on the old code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155SkTGdbwnSnR6tWBqJvUM
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Stacked on #97. Ports CIRISServer
harness/mesh-repro/scenarios/chat.shso the five-platform matrix can test flows that need a second person.testing/gate/two_node.py: the leg's ownciris-serverbinary as a second native process on 5242/5243, own home and key id; claims the peer, announces both, peers both ways (productionself-key-record), each owner adds the other as a contact, both open the pair room, the peer sends a message once keyed.downkills it by pidfile and deletes its home.fixture: two_nodeon a flow;${PEER_KEY_ID},${ROOM_ID}etc. filled from the fixture; an unresolved${…}is a load error; a value the fixture couldn't produce fails the step with its reason. Fixture-free flows run first; the fixture starts only for flows that ask and pass their floor; torn down infinallyand on SIGTERM.always(), uploads the peer log.Local, Linux desktop (the workflow's form, scratch ports; the user's node untouched):
people3/3; CSD-006 receipt 5/6 (the grant-less step skips by design); CSD-005 through the five-fact receipt, then a wrong tag in the draft (btn_scan_contact_codevs_status); CSD-091 through the composer, then no message crosses.Upstream: two released v0.5.217 nodes never key a pair room (peers stay advisory; frames fail the transport-destination binding) — CIRISServer#698, recorded in
evidence/blocked_upstream.tsv. The server's own chat ladder only passes on atest-anchorbuild.Not run locally: macOS, Windows, Android emulator, iOS simulator (Windows teardown, the Android fresh-node step and iOS host→app reach are unverified until the matrix runs). The drafts are floored 0.5.225, so the matrix refuses them until that version.
🤖 Generated with Claude Code
https://claude.ai/code/session_0155SkTGdbwnSnR6tWBqJvUM