fix(remote): stop the unroutable-pane refusal spelling a profile UUID (#485)

The switcher's group labels were not the last surface a deleted profile
reached as a raw UUID. `spec_for` returns `Err` once the profile leaves
the config, so `pane_workspace_for` builds a `PaneWorkspace` with no
spec, `route_header` refuses to route it — and that refusal carried
`{target:?}`, which for a `Profile` target is its config UUID and
nothing else.

The string is not only logged. `land_pane` hands a failed spawn to the
pending pane, which prints the reason verbatim under "could not reach
{machine}", so opening (or restoring) a workspace whose profile was
deleted put the UUID on screen under a banner that had just gone to the
trouble of not saying it.

Name it the way everything else does: the workspace's own label, which
`pane_workspace_for` already fills from `route_label` when the entry has
no name of its own, and which survives the deletion via the route
snapshot. The new test is ungated — the route a workspace builds is
platform-neutral, and this file's main test module is unix-only.
This commit is contained in:
l0ng-ai
2026-09-07 22:35:46 +08:00
parent 9113057fde
commit e247ab9bdf
+74 -5
View File
@@ -136,11 +136,24 @@ impl PaneWorkspace {
RouteHeader::local_stdio(program.clone(), &argv)
}
(_, Some(spec)) => RouteHeader::ssh((**spec).clone()),
(target, None) => {
return Err(anyhow::anyhow!(
"this workspace has no SSH connection details ({target:?}), so its panes \
cannot be routed"
));
(_, None) => {
// Deliberately not the target: a `Profile` spells itself as
// its config UUID in `Display` and in `Debug` alike, and a
// deleted profile is exactly what empties `spec` here. This
// sentence is not only logged — `land_pane` hands it to the
// pending pane, which prints the reason verbatim under
// "could not reach {machine}", so the UUID reached the screen
// (#485). The workspace's own name is what every other
// surface calls this thing.
return Err(match self.label.as_deref() {
Some(label) => anyhow::anyhow!(
"{label} has no SSH connection details, so its panes cannot be routed"
),
None => anyhow::anyhow!(
"this workspace has no SSH connection details, so its panes \
cannot be routed"
),
});
}
};
Ok(header.for_pane())
@@ -3073,6 +3086,62 @@ mod windows_tests {
}
}
/// Ungated on purpose: what a workspace can build a route out of is the same
/// on every platform, and so is the name the refusal carries.
#[cfg(test)]
mod route_header_tests {
use super::*;
use crate::core::session::{RemoteTarget, WorkspaceId};
fn unroutable(target: RemoteTarget, label: Option<&str>) -> PaneWorkspace {
PaneWorkspace {
workspace: WorkspaceId::new(),
target,
spec: None,
label: label.map(str::to_string),
resize_echo: false,
}
}
/// A deleted profile is what empties `spec`, and the refusal built here is
/// what the pending pane prints verbatim under "could not reach
/// {machine}" — so this is one of the screens #485 is about. It used to
/// carry `{target:?}`, which for a `Profile` is its config UUID and
/// nothing else.
#[test]
fn an_unroutable_workspace_is_not_named_by_its_profile_uuid() {
let id = uuid::Uuid::new_v4();
let gone = RemoteTarget::Profile { id };
let named = unroutable(gone.clone(), Some("lager"));
let e = named
.route_header()
.expect_err("no spec, no route")
.to_string();
assert!(
!e.contains(&id.to_string()),
"a bare profile UUID reached the UI: {e}"
);
assert!(
e.contains("lager"),
"the entry's own name is what it is called: {e}"
);
assert!(e.contains("cannot be routed"), "{e}");
// Nothing to call it by is still no reason to print the UUID.
let bare = unroutable(gone, None);
let e = bare
.route_header()
.expect_err("no spec, no route")
.to_string();
assert!(
!e.contains(&id.to_string()),
"a bare profile UUID reached the UI: {e}"
);
assert!(e.contains("cannot be routed"), "{e}");
}
}
#[cfg(all(test, unix))]
mod tests {
use super::*;