Skip to main content

max / alloy

Inject the route check, so the enrollment tests state their own network Both views now hold `route: fn() -> bool` rather than calling `machine_has_route` directly. The gate arrived reading /proc/net/route at the call site, which quietly made every enrollment test depend on the machine running it: the tests asserting that signing in hands over the terminal passed on a laptop and would have failed in a build container with no default route, for a reason with nothing to do with what they assert. Three tests the seam buys, none of which could be written before: the refusal and its message on a machine with no route, the handover on a machine with one, and the same refusal through the first-boot screen's key handling. The existing suspend test now says `route: || true` instead of inheriting whatever the host has.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01WFBzMprSmNCfvdj2cGZyka
Author: Max Johnson <me@maxj.phd> · 2026-09-07 20:07 UTC
Signed with PGP, not checked
Commit: 15450564c9f859c0df75005e0b5a9cf06b1a79b5
Parent: 8ad44a2
3 files changed, +92 insertions, -2 deletions
@@ -629,6 +629,15 @@
629 629 /// Headscale choice visible at the moment it is made, and the vendor's
630 630 /// plane is the empty answer.
631 631 server: Option<TextField>,
632 + /// Whether this machine can reach anything, as a seam.
633 + ///
634 + /// Injected rather than called directly so the enrollment tests state the
635 + /// network they are testing against. Reading `/proc/net/route` from inside
636 + /// a test makes the result depend on the machine running it: the same test
637 + /// would pass on a laptop and fail in a build container with no default
638 + /// route, and it would fail for a reason that has nothing to do with what
639 + /// it asserts.
640 + route: fn() -> bool,
632 641 }
633 642
634 643 impl MeshView {
@@ -645,6 +654,7 @@
645 654 error: None,
646 655 ticks: 0,
647 656 server: None,
657 + route: machine_has_route,
648 658 };
649 659 view.refresh(log);
650 660 view
@@ -756,7 +766,7 @@
756 766 return Flow::Continue;
757 767 }
758 768 };
759 - if !machine_has_route() {
769 + if !(self.route)() {
760 770 self.error = Some(NO_ROUTE.into());
761 771 self.close_enrollment();
762 772 return Flow::Continue;
@@ -179,6 +179,9 @@
179 179 server: Option<TextField>,
180 180 error: Option<String>,
181 181 ticks: u64,
182 + /// Whether this machine can reach anything. See [`mesh::MeshView`]'s field
183 + /// of the same name for why it is a seam rather than a call.
184 + route: fn() -> bool,
182 185 /// Refresh on the next tick regardless of the poll counter.
183 186 ///
184 187 /// Set before handing the terminal away. See the module docs: the shell's
@@ -208,6 +211,7 @@
208 211 server: None,
209 212 error: None,
210 213 ticks: 0,
214 + route: mesh::machine_has_route,
211 215 stale: false,
212 216 };
213 217 view.refresh(log);
@@ -299,7 +303,7 @@
299 303 return Flow::Continue;
300 304 }
301 305 };
302 - if !mesh::machine_has_route() {
306 + if !(self.route)() {
303 307 self.error = Some(mesh::NO_ROUTE.into());
304 308 self.server = None;
305 309 return Flow::Continue;
@@ -694,6 +698,7 @@
694 698 fn enrolling_the_mesh_asks_for_a_server_then_suspends() {
695 699 let mut log = CommandLog::new();
696 700 let mut view = view(false, true, &mut log);
701 + view.route = || true;
697 702 assert_eq!(view.selected(), Some(Row::Mesh));
698 703
699 704 let flow = view.handle(press(KeyCode::Char('e')), &mut log);
@@ -709,6 +714,39 @@
709 714 assert!(view.server.is_none(), "the overlay closes on the way out");
710 715 }
711 716
717 + /// The same flow on a machine with no route stops before the handover.
718 + ///
719 + /// Measured on fw12 2026-09-07: a reinstall took the Wi-Fi with it, and
720 + /// enrolling escalated, took the password, and left the user on a blank
721 + /// terminal while `tailscale up` retried a control server it could not
722 + /// reach. The route is injected so this states its own network instead of
723 + /// inheriting whatever the machine running the suite happens to have.
724 + #[test]
725 + fn enrolling_the_mesh_without_a_route_refuses_and_names_the_screen() {
726 + let mut log = CommandLog::new();
727 + let mut view = view(false, true, &mut log);
728 + view.route = || false;
729 +
730 + let flow = view.handle(press(KeyCode::Char('e')), &mut log);
731 + assert!(matches!(flow, Flow::Continue), "the overlay opens first");
732 +
733 + let flow = view.handle(press(KeyCode::Enter), &mut log);
734 + assert!(
735 + matches!(flow, Flow::Continue),
736 + "a sign-in that cannot reach a control server must not take the terminal",
737 + );
738 + let error = view.error.as_deref().expect("the refusal is reported");
739 + assert!(
740 + error.contains("no network"),
741 + "it says what is wrong: {error}"
742 + );
743 + assert!(
744 + error.contains("alloy net"),
745 + "it names the screen that fixes it: {error}",
746 + );
747 + assert!(view.server.is_none(), "the overlay closes either way");
748 + }
749 +
712 750 // The reason this screen has a `stale` flag at all. The user comes back
713 751 // from the mesh sign-in to take the other row, and the shell refreshes a
714 752 // rebuilt view by calling `tick` once — which the poll counter would eat.
@@ -225,6 +225,7 @@
225 225 error: None,
226 226 ticks: 0,
227 227 server: None,
228 + route: || true,
228 229 },
229 230 CommandLog::new(),
230 231 )
@@ -570,3 +571,44 @@
570 571 ));
571 572 assert!(!has_route(""));
572 573 }
574 +
575 + /// With no route, enrolling says so and keeps the terminal.
576 + ///
577 + /// The seam is the point: this asserts the refusal on a machine that certainly
578 + /// has a route, which is every machine anyone runs the suite on.
579 + #[test]
580 + fn enrolling_without_a_route_refuses_and_names_the_screen() {
581 + let (mut view, mut log) = logged_out_view();
582 + view.route = || false;
583 + view.open_enrollment();
584 +
585 + let flow = view.submit_enrollment(&mut log);
586 + assert!(
587 + matches!(flow, Flow::Continue),
588 + "a sign-in that cannot reach a control server must not take the terminal",
589 + );
590 + let error = view.error.as_deref().expect("the refusal is reported");
591 + assert!(
592 + error.contains("no network"),
593 + "it says what is wrong: {error}"
594 + );
595 + assert!(
596 + error.contains("alloy net"),
597 + "it names the screen that fixes it: {error}",
598 + );
599 + assert!(view.server.is_none(), "the overlay closes either way");
600 + }
601 +
602 + /// With a route, the same submission hands over as before.
603 + #[test]
604 + fn enrolling_with_a_route_still_suspends() {
605 + let (mut view, mut log) = logged_out_view();
606 + view.route = || true;
607 + view.open_enrollment();
608 +
609 + let flow = view.submit_enrollment(&mut log);
610 + assert!(
611 + matches!(flow, Flow::Suspend(_)),
612 + "signing in hands over the terminal",
613 + );
614 + }