60c95173
fix: a --via wall records no layout, so one throwaway run cannot refuse the next
a73x 2026-09-03 05:20
Commit message
src/tui/wall_test_layout.zig
| Old | New | ||
|---|---|---|---|
| @@ -1077,6 +1077,34 @@ test "seedLayout: a leaf with no session, a bad name, or a repeat refuses; garba | |||
| 1077 | try std.testing.expectEqualStrings("not a layout", on_garbage.refused); | 1077 | try std.testing.expectEqualStrings("not a layout", on_garbage.refused); |
| 1078 | } | 1078 | } |
| 1079 | 1079 | ||
| 1080 | test "seedLayout: a --via leaf refuses the whole file, which is why a --via wall records nothing" { | ||
| 1081 | const alloc = std.testing.allocator; | ||
| 1082 | var shared = Shared{ .out_fd = -1, .size = .{ .cols = 120, .rows = 40 }, .is_tty = true }; | ||
| 1083 | defer shared.tree.deinit(); | ||
| 1084 | // The leaf a `--via` entry would be saved as. `tileLabel` is the one | ||
| 1085 | // speller of a leaf, so the file below is byte for byte the file the | ||
| 1086 | // start-up `persist` used to produce for `mux --via CMD`. | ||
| 1087 | const label = try wv.tileLabel(alloc, .{ .via = "ssh box mux d proxy" }, "0"); | ||
| 1088 | defer alloc.free(label); | ||
| 1089 | try std.testing.expectEqualStrings("--via ssh box mux d proxy#0", label); | ||
| 1090 | // `hosts.zig` refuses to write a `--via` line, so no hosts table can | ||
| 1091 | // ever have a row for one: this leaf names a host that cannot be there. | ||
| 1092 | var table = [_]Host{fixture.testHost(&shared, "--sock /a", "/a")}; | ||
| 1093 | var file: std.ArrayListUnmanaged(u8) = .{}; | ||
| 1094 | defer file.deinit(alloc); | ||
| 1095 | try file.appendSlice(alloc, "mux-layout 1\nbeside 0\n leaf 1 --sock /a#0\n leaf 1 "); | ||
| 1096 | try file.appendSlice(alloc, label); | ||
| 1097 | try file.append(alloc, '\n'); | ||
| 1098 | const res = wall_layout.seedLayout(alloc, &table, &shared, file.items, null); | ||
| 1099 | try std.testing.expect(res == .refused); | ||
| 1100 | try std.testing.expectEqualStrings(label, res.refused); | ||
| 1101 | // The refusal is the WHOLE file: the pane on `--sock /a`, and every | ||
| 1102 | // other pane the user authored, goes with it. That is the cost `run` | ||
| 1103 | // avoids by nulling `Shared.layout_path` for a `--via` entry — it | ||
| 1104 | // neither reads the file nor writes one. | ||
| 1105 | try std.testing.expect(shared.tree.root == null); | ||
| 1106 | } | ||
| 1107 | |||
| 1080 | test "seedLayout: the entry spelling takes leaf 0 when the file has it, and is inserted beside the focus when it does not" { | 1108 | test "seedLayout: the entry spelling takes leaf 0 when the file has it, and is inserted beside the focus when it does not" { |
| 1081 | var shared = Shared{ .out_fd = -1, .size = .{ .cols = 120, .rows = 40 }, .is_tty = true }; | 1109 | var shared = Shared{ .out_fd = -1, .size = .{ .cols = 120, .rows = 40 }, .is_tty = true }; |
| 1082 | defer shared.tree.deinit(); | 1110 | defer shared.tree.deinit(); |
src/tui/wallview.zig
| Old | New | ||
|---|---|---|---|
| @@ -1737,6 +1737,22 @@ pub fn run(alloc: std.mem.Allocator, host_specs: []const HostSpec, entry: Entry) | |||
| 1737 | entry_name = try alloc.dupe(u8, proto.resolveName(entry.entry_session)); | 1737 | entry_name = try alloc.dupe(u8, proto.resolveName(entry.entry_session)); |
| 1738 | entry_label = try tileLabel(alloc, host_specs[entry.entry_host.?].target, entry_name); | 1738 | entry_label = try tileLabel(alloc, host_specs[entry.entry_host.?].target, entry_name); |
| 1739 | } | 1739 | } |
| 1740 | // A `--via CMD` wall records nothing, and that includes its own pane. | ||
| 1741 | // `hosts.zig` refuses to write a `--via` line — an arbitrary command is | ||
| 1742 | // not an address — so no hosts table can ever hold a row for one, while | ||
| 1743 | // `tileLabel` still spells the tile `--via CMD#NAME` for its bar. Left | ||
| 1744 | // alone, the start-up `persist` writes that spelling as a leaf and the | ||
| 1745 | // NEXT start's `seedLayout` finds no host for it and refuses the WHOLE | ||
| 1746 | // file: one throwaway `--via` run costs the user every pane they | ||
| 1747 | // authored. Same rule as the trimmed seed below — a run that cannot | ||
| 1748 | // record what it is showing stops recording, before it reads the file | ||
| 1749 | // as well as before it writes one, and says so where the wall says | ||
| 1750 | // everything else. | ||
| 1751 | if (has_entry and host_specs[entry.entry_host.?].target == .via) { | ||
| 1752 | if (shared.layout_path) |p| alloc.free(p); | ||
| 1753 | shared.layout_path = null; | ||
| 1754 | setNotice(&shared, "[layout not saved: a --via wall is not recorded]"); | ||
| 1755 | } | ||
| 1740 | // The wall starts with the cut the sidecar remembers: its leaves are | 1756 | // The wall starts with the cut the sidecar remembers: its leaves are |
| 1741 | // pending panes the polls bind in place or the settle collapses, so | 1757 | // pending panes the polls bind in place or the settle collapses, so |
| 1742 | // the first paint is the saved shape, not a placeholder to re-cut. | 1758 | // the first paint is the saved shape, not a placeholder to re-cut. |
test/e2e_01_boot.sh
| Old | New | ||
|---|---|---|---|
| @@ -187,6 +187,43 @@ kill -0 "$D1PID" || { echo "e2e FAIL: daemon died in --via scenario"; exit 1; } | |||
| 187 | assert_converged "$OUT.via" "$SOCK" "via transport" | 187 | assert_converged "$OUT.via" "$SOCK" "via transport" |
| 188 | rm_swept "$OUT.via" | 188 | rm_swept "$OUT.via" |
| 189 | 189 | ||
| 190 | # Same transport, on a TERMINAL, for what the wall must NOT do with it: a | ||
| 191 | # `--via` run records no layout. `hosts.zig` refuses to write a `--via` | ||
| 192 | # line, so no hosts table can ever hold a row for one, while a tile of that | ||
| 193 | # transport still spells itself `--via CMD#NAME` — the only spelling it | ||
| 194 | # has. Saved, that leaf is one the next start's loader finds no host for, | ||
| 195 | # and it refuses the WHOLE file: one throwaway `--via` run would cost the | ||
| 196 | # user every pane they had authored. A pty and a state dir of its own are | ||
| 197 | # both required to see it — the layout path is opened only when stdin is a | ||
| 198 | # terminal, so the piped run above cannot show this either way. | ||
| 199 | VIASTATE="${TMPDIR:-/tmp}/mux-e2e-vias-$$" | ||
| 200 | defer_rm "$VIASTATE" | ||
| 201 | mkdir -p "$VIASTATE" | ||
| 202 | set +e | ||
| 203 | env XDG_STATE_HOME="$VIASTATE" XDG_RUNTIME_DIR=/nonexistent-mux-e2e SHELL=/bin/sh \ | ||
| 204 | timeout 60 "$PTYCLIENT" --cols 80 --rows 24 \ | ||
| 205 | --out "$OUT.viapty" --err "$OUT.viapty.err" -- \ | ||
| 206 | "$MUX" --via "$MUX d proxy --sock $SOCK" \ | ||
| 207 | > "$OUT.viapty.log" 2>&1 <<'VIAEOF' | ||
| 208 | settle 700 20000 | ||
| 209 | send printf 'm6-%s\n' via-tty\n | ||
| 210 | expect m6-via-tty 20000 | ||
| 211 | settle 500 15000 | ||
| 212 | send \x1cd | ||
| 213 | waitexit 10000 | ||
| 214 | VIAEOF | ||
| 215 | RC=$? | ||
| 216 | set -e | ||
| 217 | rc0 "--via on a terminal exited $RC:" "$OUT.viapty.log" "$OUT.viapty.err" | ||
| 218 | grep -q 'layout not saved: a --via wall is not recorded' "$OUT.viapty" || { | ||
| 219 | echo "e2e FAIL: a --via wall on a terminal did not say it records nothing:" | ||
| 220 | cat "$OUT.viapty"; exit 1; } | ||
| 221 | [ ! -e "$VIASTATE/mux/layout" ] || { | ||
| 222 | echo "e2e FAIL: a --via run wrote a layout; the next start's loader refuses" | ||
| 223 | echo " the whole file over its leaf, taking every other pane with it:" | ||
| 224 | cat "$VIASTATE/mux/layout"; exit 1; } | ||
| 225 | rm_swept "$OUT.viapty" "$OUT.viapty.err" "$OUT.viapty.log" | ||
| 226 | |||
| 190 | # --- M10: a --via command that dies before the first frame stops claiming | 227 | # --- M10: a --via command that dies before the first frame stops claiming |
| 191 | # a connection existed. ssh's own stderr still passes through untouched. | 228 | # a connection existed. ssh's own stderr still passes through untouched. |
| 192 | # No convergence: no session was ever established, so there is no grid to match. | 229 | # No convergence: no session was ever established, so there is no grid to match. |