a73x

0df4068a

test: the tile's -A is what consents, and it is watched now

a73x   2026-08-21 08:33

Commit message
test: the tile's -A is what consents, and it is watched now

Both gates could be deleted under a green suite. The refusal leg's client
has no agent of its own, so the channel is refused at the dial whatever
the consent check says — the test that looked like it covered this was
answering a different question.

openAgentChan comes out of the pump to get a seam: everything else in that
loop needs a live daemon. The test binds a REAL listening agent socket, so
offered=false and offered=true differ by consent alone, and asserts the
attach leaves an empty pipe behind it rather than trusting a frame reader
to report the absence of a frame.

Both mutations verified failing.

build.zig
Old New
@@ -273,7 +273,7 @@ const mod_table = [_]ModSpec{
273 // typed-at session; the overlay machinery itself is interact's, shared — 273 // typed-at session; the overlay machinery itself is interact's, shared —
274 // and phase 3 promotes the tile into that core rather than growing a 274 // and phase 3 promotes the tile into that core rather than growing a
275 // second copy of it. 275 // second copy of it.
276 .{ .name = "wallview", .path = "src/wallview.zig", .layer = 4, .link_libc = true, .imports = &.{ "protocol", "client", "interact", "wall", "handoff", "xdg", "sockpath", "proxy", "engine", "paint" }, .quic_tests = true }, 276 .{ .name = "wallview", .path = "src/wallview.zig", .layer = 4, .link_libc = true, .imports = &.{ "protocol", "client", "interact", "wall", "handoff", "xdg", "sockpath", "proxy", "engine", "paint", "testtmp" }, .quic_tests = true },
277 // ---- layer 5 ---- 277 // ---- layer 5 ----
278 // wall owns the spelling grammar and the state file, so argv is parsed 278 // wall owns the spelling grammar and the state file, so argv is parsed
279 // by the SAME rules the page's POST /tiles and the restored file are — 279 // by the SAME rules the page's POST /tiles and the restored file are —
src/wallview.zig
Old New
@@ -105,6 +105,7 @@ const paint = @import("paint");
105 // The chord table and the prediction hooks a zoomed tile shares with the 105 // The chord table and the prediction hooks a zoomed tile shares with the
106 // client: one interaction core, not a second copy (interact.zig). 106 // client: one interaction core, not a second copy (interact.zig).
107 const interact = @import("interact"); 107 const interact = @import("interact");
108 const TmpDir = @import("testtmp").TmpDir;
108 109
109 pub const Resolved = struct { 110 pub const Resolved = struct {
110 target: client.Target, 111 target: client.Target,
@@ -910,6 +911,35 @@ fn closeLocal(locals: []?AgentLocal, slot: usize, transport: *client.Transport)
910 transport.writeFrame(.agent_close, &proto.encodeAgentId(ch.id)) catch {}; 911 transport.writeFrame(.agent_close, &proto.encodeAgentId(ch.id)) catch {};
911 } 912 }
912 913
914 /// Open one channel to this machine's agent, or refuse. False is the whole
915 /// refusal: the caller answers every reason with the same `agent_close`.
916 ///
917 /// Lifted out of the pump for the first line of it. `offered` is the tile's
918 /// own `-A`, and a gate that only ever ran inside a live pump is a gate no
919 /// test can watch: flip it open and the suite still passes, because a
920 /// client with no agent refuses at the dial for a different reason.
921 fn openAgentChan(
922 locals: []?AgentLocal,
923 id: u32,
924 offered: bool,
925 sock: []const u8,
926 ) bool {
927 // The OFFER is the consent, and it is per TILE: a wall where one tile
928 // was typed with `-A` must not hand another tile's host the keys,
929 // whoever asks.
930 if (!offered) return false;
931 // A live id reused. Refusing keeps the channel already on that id
932 // intact, which is the half of the collision that has real bytes moving
933 // through it.
934 if (findLocal(locals, id) != null) return false;
935 const fd = client.connectAgent(sock) orelse return false;
936 if (storeLocal(locals, id, fd) == null) {
937 std.posix.close(fd);
938 return false;
939 }
940 return true;
941 }
942
913 /// One `agent_data` payload onto the channel it names. Lifted out of the 943 /// One `agent_data` payload onto the channel it names. Lifted out of the
914 /// pump so the cap has a seam a test can reach: everything else in that 944 /// pump so the cap has a seam a test can reach: everything else in that
915 /// loop needs a live daemon, and the length rule is the one thing here that 945 /// loop needs a live daemon, and the length rule is the one thing here that
@@ -1390,26 +1420,12 @@ fn pumpTile(t: *Tile) void {
1390 // reasons is ordinary — no agent on this machine; 1420 // reasons is ordinary — no agent on this machine;
1391 // the rest are a daemon asking for something it 1421 // the rest are a daemon asking for something it
1392 // should not. 1422 // should not.
1393 const opened = open: { 1423 const opened = openAgentChan(
1394 // The OFFER is the consent, and it is per 1424 &agent_locals,
1395 // TILE: a wall where one tile was typed with 1425 id,
1396 // `-A` must not hand another tile's host the 1426 t.r.agent,
1397 // keys, whoever asks. 1427 std.posix.getenv(proto.agent_sock_env) orelse "",
1398 if (!t.r.agent) break :open false; 1428 );
1399 // A live id reused. Refusing keeps the
1400 // channel already on that id intact, which is
1401 // the half of the collision that has real
1402 // bytes moving through it.
1403 if (findLocal(&agent_locals, id) != null) break :open false;
1404 const fd = client.connectAgent(
1405 std.posix.getenv(proto.agent_sock_env) orelse "",
1406 ) orelse break :open false;
1407 if (storeLocal(&agent_locals, id, fd) == null) {
1408 std.posix.close(fd);
1409 break :open false;
1410 }
1411 break :open true;
1412 };
1413 if (!opened) 1429 if (!opened)
1414 transport.writeFrame( 1430 transport.writeFrame(
1415 .agent_close, 1431 .agent_close,
@@ -3363,3 +3379,65 @@ test "agent channels: an oversize frame hangs the channel up, a full one lands"
3363 try std.testing.expectEqual(proto.MsgType.agent_close, frame.type); 3379 try std.testing.expectEqual(proto.MsgType.agent_close, frame.type);
3364 try std.testing.expectEqual(@as(u32, 5), try proto.decodeAgentId(frame.payload)); 3380 try std.testing.expectEqual(@as(u32, 5), try proto.decodeAgentId(frame.payload));
3365 } 3381 }
3382
3383 test "agent forwarding is per tile: no -A offers nothing and opens nothing" {
3384 // A REAL agent socket on this machine, listening. Without one, a tile
3385 // that never asked for forwarding and a tile that did both refuse — for
3386 // different reasons — and the consent gate could be deleted under a
3387 // green suite.
3388 var tmp = try TmpDir.make();
3389 defer tmp.cleanup();
3390 var buf: [128]u8 = undefined;
3391 const sock = try std.fmt.bufPrintZ(&buf, "{s}/agent.sock", .{tmp.path()});
3392 const addr = try std.net.Address.initUnix(sock);
3393 var listener = try addr.listen(.{});
3394 defer listener.deinit();
3395
3396 var locals: [2]?AgentLocal = @splat(null);
3397 defer dropLocals(&locals);
3398
3399 // The gate, both ways round, against the same reachable agent.
3400 try std.testing.expect(!openAgentChan(&locals, 1, false, sock));
3401 try std.testing.expectEqual(@as(?usize, null), findLocal(&locals, 1));
3402 try std.testing.expect(openAgentChan(&locals, 1, true, sock));
3403 try std.testing.expect(findLocal(&locals, 1) != null);
3404
3405 // And the daemon does not get told about an agent the tile never
3406 // offered — the offer is what the daemon routes on, so a stray one
3407 // makes this client the answerer for a session it never armed.
3408 var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
3409 var t = Tile{
3410 .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "", .agent = false },
3411 .stripe = .{ .top = 0, .rows = 4 },
3412 .shared = &shared,
3413 .idx = 0,
3414 .wake_r = -1,
3415 .wake_w = -1,
3416 };
3417 const link = try std.posix.pipe2(.{ .NONBLOCK = true });
3418 defer std.posix.close(link[0]);
3419 defer std.posix.close(link[1]);
3420 var transport: client.Transport = .{ .conn = .{ .r = link[0], .w = link[1] }, .link = .fd };
3421
3422 try sendAttach(&t, &transport, 0, 0);
3423 const first = (try proto.readFrame(std.testing.allocator, link[0])) orelse
3424 return error.NoAttach;
3425 defer first.deinit(std.testing.allocator);
3426 try std.testing.expectEqual(proto.MsgType.attach, first.type);
3427 // Nothing else on the pipe at all: an empty non-blocking read is the
3428 // only proof that no offer followed, since a frame reader would just
3429 // block waiting for one.
3430 var spare: [1]u8 = undefined;
3431 try std.testing.expectError(error.WouldBlock, std.posix.read(link[0], &spare));
3432
3433 // The positive control on the same pipe: with `-A` the offer follows the
3434 // attach, so the silence above is the gate and not an unwritten frame.
3435 t.r.agent = true;
3436 try sendAttach(&t, &transport, 0, 0);
3437 const attach = (try proto.readFrame(std.testing.allocator, link[0])).?;
3438 defer attach.deinit(std.testing.allocator);
3439 const offer = (try proto.readFrame(std.testing.allocator, link[0])) orelse
3440 return error.NoAgentOffer;
3441 defer offer.deinit(std.testing.allocator);
3442 try std.testing.expectEqual(proto.MsgType.agent_offer, offer.type);
3443 }