WIPWIP: docs/remoting-poc-test.md
This commit is contained in:
parent
c2c6c1c187
commit
367173d30d
1 changed files with 235 additions and 0 deletions
235
docs/remoting-poc-test.md
Normal file
235
docs/remoting-poc-test.md
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
# Remoting POC Test - Child Process Sends a Message
|
||||
|
||||
## Goal
|
||||
|
||||
Validate the child process transport and wire framing end-to-end with the
|
||||
simplest possible test: a child process writes one framed CBOR message to
|
||||
stdout and the parent receives and decodes it correctly.
|
||||
|
||||
This test drives the first implementation increment of `src/remote/` without
|
||||
requiring the full endpoint or proxy infrastructure.
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
**In scope:**
|
||||
|
||||
- `framing.zig` - length-prefix frame writer (child side) and reader/accumulator (parent side)
|
||||
- `protocol.zig` - encoding and decoding of one wire message type (`send_named`)
|
||||
- A dedicated child binary built alongside the tests
|
||||
- Parent-side frame accumulation from `subprocess.zig` stdout chunks
|
||||
|
||||
**Out of scope:**
|
||||
|
||||
- Endpoint actor
|
||||
- Proxy actors
|
||||
- Link/exit propagation
|
||||
- Any message type other than `send_named`
|
||||
|
||||
---
|
||||
|
||||
## Child Binary
|
||||
|
||||
The child is a standalone executable built from a single source file:
|
||||
|
||||
`test/remote_child_send.zig`
|
||||
|
||||
It does not need a Thespian context. It writes one framed CBOR message to
|
||||
stdout and exits:
|
||||
|
||||
1. Encode a `send_named` wire message with `protocol.encode_send_named`.
|
||||
2. Write it to fd 1 (stdout) via `framing.write_frame`.
|
||||
3. Flush stdout.
|
||||
4. Exit with code 0.
|
||||
|
||||
The message:
|
||||
|
||||
```
|
||||
["send_named", 1, "test_actor", ["hello", "from_child"]]
|
||||
```
|
||||
|
||||
| Field | Value | Meaning |
|
||||
| --------- | ------------------------ | ------------------------------- |
|
||||
| `from_id` | `1` | Opaque ID for the child process |
|
||||
| `to_name` | `"test_actor"` | Well-known name on parent side |
|
||||
| `payload` | `["hello","from_child"]` | The actor message body |
|
||||
|
||||
Sketch:
|
||||
|
||||
```zig
|
||||
pub fn main() !void {
|
||||
var buf: [256]u8 = undefined;
|
||||
const payload = try protocol.encode_send_named(&buf, 1, "test_actor",
|
||||
try cbor.writeValue(&buf, .{ "hello", "from_child" }));
|
||||
try framing.write_frame(std.io.getStdOut().writer(), payload);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Parent Test Actor
|
||||
|
||||
The parent is a standard Thespian test actor defined in `test/remote_poc_test.zig`.
|
||||
It follows the `ip_tcp_client_server.zig` pattern.
|
||||
|
||||
Steps:
|
||||
|
||||
1. Spawn the child binary using `thespian.subprocess` with `stdin_behavior = .Closed`.
|
||||
2. Receive stdout chunks as `{tag, "stdout", bytes}` messages.
|
||||
3. Feed each chunk into a `framing.Accumulator` until a complete frame is ready.
|
||||
4. Decode the frame with `protocol.decode`.
|
||||
5. Assert the decoded message matches the expected `send_named` envelope.
|
||||
6. Wait for `{tag, "term", "exited", 0}`.
|
||||
7. Exit with `"success"`.
|
||||
|
||||
Message flow:
|
||||
|
||||
```
|
||||
subprocess.zig --> {tag, "stdout", bytes} --> Parent actor
|
||||
|
|
||||
accumulator.feed(bytes)
|
||||
|
|
||||
complete frame available?
|
||||
|
|
||||
protocol.decode(frame)
|
||||
|
|
||||
assert send_named fields match
|
||||
```
|
||||
|
||||
The child binary path is passed to the parent actor via its `Args` struct and
|
||||
comes from the build options (see below).
|
||||
|
||||
---
|
||||
|
||||
## New Source Files
|
||||
|
||||
```
|
||||
src/remote/framing.zig
|
||||
write_frame(writer, payload: []const u8) !void
|
||||
Accumulator
|
||||
feed(self, bytes: []const u8) ?[]u8 -- returns completed payload or null
|
||||
|
||||
src/remote/protocol.zig
|
||||
encode_send_named(buf, from_id: u64, to_name: []const u8, payload: []const u8) ![]u8
|
||||
decode(frame: []const u8) !WireMessage
|
||||
WireMessage (tagged union)
|
||||
.send_named: struct { from_id: u64, to_name: []const u8, payload: []const u8 }
|
||||
-- other variants stubbed for now
|
||||
```
|
||||
|
||||
`framing.Accumulator` keeps an internal buffer, accepts partial byte slices,
|
||||
and returns a slice to the completed payload once the length-prefixed frame
|
||||
is fully received.
|
||||
|
||||
---
|
||||
|
||||
## New Test Files
|
||||
|
||||
```
|
||||
test/remote_child_send.zig -- the child binary (no Thespian context)
|
||||
test/remote_poc_test.zig -- the Zig test (Parent actor + test case)
|
||||
```
|
||||
|
||||
Test case:
|
||||
|
||||
```zig
|
||||
test "remote: child process sends a message to parent" {
|
||||
const allocator = std.testing.allocator;
|
||||
var ctx = try thespian.context.init(allocator);
|
||||
defer ctx.deinit();
|
||||
|
||||
var success = false;
|
||||
var exit_handler = thespian.make_exit_handler(&success, struct {
|
||||
fn handle(ok: *bool, status: []const u8) void {
|
||||
ok.* = std.mem.eql(u8, status, "success");
|
||||
}
|
||||
}.handle);
|
||||
|
||||
_ = try ctx.spawn_link(
|
||||
Parent.Args{
|
||||
.allocator = allocator,
|
||||
.child_path = build_options.remote_child_path,
|
||||
},
|
||||
Parent.start,
|
||||
"remote_poc",
|
||||
&exit_handler,
|
||||
null,
|
||||
);
|
||||
|
||||
ctx.run();
|
||||
|
||||
if (!success) return error.TestFailed;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Build Integration
|
||||
|
||||
In `build.zig`:
|
||||
|
||||
1. Add the child as a standalone executable:
|
||||
|
||||
```zig
|
||||
const remote_child = b.addExecutable(.{
|
||||
.name = "remote_child_send",
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("test/remote_child_send.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.imports = &.{
|
||||
.{ .name = "cbor", .module = cbor_mod },
|
||||
},
|
||||
}),
|
||||
});
|
||||
// remote_child_send.zig uses src/remote/framing.zig and protocol.zig directly
|
||||
// via @import relative paths; no extra linkage needed for this binary.
|
||||
```
|
||||
|
||||
2. Pass the child binary path to the test suite via build options:
|
||||
|
||||
```zig
|
||||
options.addOptionPath("remote_child_path", remote_child.getEmittedBin());
|
||||
```
|
||||
|
||||
3. Make the test run depend on the child binary being built:
|
||||
|
||||
```zig
|
||||
test_run_cmd.step.dependOn(&remote_child.step);
|
||||
```
|
||||
|
||||
The test imports `build_options` (already wired in) and reads
|
||||
`build_options.remote_child_path` at compile time to get the path to the
|
||||
child executable.
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
The test passes when:
|
||||
|
||||
1. The child exits with code 0.
|
||||
2. The parent decodes a `send_named` frame with `from_id = 1`,
|
||||
`to_name = "test_actor"`, and `payload = ["hello", "from_child"]`.
|
||||
3. No Thespian actors exit with an unexpected error.
|
||||
|
||||
---
|
||||
|
||||
## What This Test Does NOT Validate
|
||||
|
||||
- The endpoint actor (not yet written).
|
||||
- Proxies (not yet written).
|
||||
- Bidirectional communication.
|
||||
- Link/exit propagation.
|
||||
- Partial-frame delivery across multiple stdout chunks (worth a dedicated unit
|
||||
test on `framing.Accumulator` separately).
|
||||
|
||||
---
|
||||
|
||||
## Next Step After This POC
|
||||
|
||||
Once this test passes, the next increment is a round-trip test: the parent
|
||||
sends a message to the child and the child replies. That test drives the
|
||||
parent-side `write_frame` path and the child-side `Accumulator`, completing
|
||||
the symmetric framing layer before the endpoint actor is introduced.
|
||||
Loading…
Add table
Add a link
Reference in a new issue