临时连接:标题栏「临时连接…」打开对话框,连一台主机而不保存
标题栏右上角的「新建主机…」换成「临时连接…」(新建主机还在主机列表的「+」、 右键菜单和 ⌘N)。对话框沿用主机对话框,去掉分组、连接方式和备注,名称可不填; 密码只留在内存里,不进钥匙串。连上后功能齐全(不像外部连接只开一个通道), 不进主机列表和最近连接,标签都关掉就没了;可以使用凭据,凭据改动和删除也 照顾到它。外部 CLI 不区分临时连接和外部连接,表格写「(未保存)」。 测试连接选「密码」却没填时,按「无密码」那样试(免认证、SSH Agent、默认私钥), 服务器要密码时报「未填写密码」。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
835894625f
commit
9474c9e789
20 files changed
+596
-126
No files matched your search
@@ -768,6 +768,129 @@ async fn an_sftp_link_opens_an_sftp_tab_across_the_window(cx: &mut TestAppContex
|
||||
store.read_with(cx, |store, _| assert!(store.host(LINK_HOST).is_none()));
|
||||
}
|
||||
|
||||
/// The title bar's 临时连接: a host's login without its group, route or
|
||||
/// notes, connected to with every feature and saved nowhere.
|
||||
#[gpui_kit::test]
|
||||
async fn a_temporary_connection_has_every_feature_and_saves_nothing(cx: &mut TestAppContext) {
|
||||
use shellrs::app::CloseTerminal;
|
||||
|
||||
let data = tempfile::tempdir().unwrap();
|
||||
let path = data.path().join("shellrs.db");
|
||||
let keychain = Arc::new(InMemorySecretStore::default());
|
||||
let store = HostStore::load(HostDatabase::open(&path).unwrap())
|
||||
.unwrap()
|
||||
.with_secrets(keychain.clone());
|
||||
let remote = Arc::new(RecordingRemoteProvider::default());
|
||||
let tester = Arc::new(FakeConnectionTester::default());
|
||||
let (handle, workspace) =
|
||||
open_workspace_with_credentials(cx, store, remote.clone(), tester.clone());
|
||||
|
||||
in_frame(cx, handle, |window, cx| {
|
||||
window.click("temporary-connection", cx)
|
||||
});
|
||||
in_frame(cx, handle, |window, cx| {
|
||||
// Only what a login needs.
|
||||
assert!(window.try_find("host-route").is_none());
|
||||
assert!(window.try_find("host-notes").is_none());
|
||||
assert!(window.try_find("test-connection").is_some());
|
||||
assert_eq!(
|
||||
window.find("temporary-note").label(),
|
||||
Some("仅当前使用的临时会话,不会保存到主机列表。")
|
||||
);
|
||||
// The name may be left out.
|
||||
window.click("host-address", cx);
|
||||
window.input("10.0.0.9", cx);
|
||||
window.click("host-port", cx);
|
||||
window.press("cmd-a", cx);
|
||||
window.input("2222", cx);
|
||||
window.click("host-user", cx);
|
||||
window.press("cmd-a", cx);
|
||||
window.input("deploy", cx);
|
||||
window.click("host-password", cx);
|
||||
window.input("p@ss", cx);
|
||||
window.click("test-connection", cx);
|
||||
});
|
||||
// 测试连接 tries it as it would connect: directly.
|
||||
cx.wait_for(handle.into(), Duration::from_secs(3), |window, cx| {
|
||||
window.render_frame(cx);
|
||||
window.notifications(cx).len() == 1
|
||||
})
|
||||
.await;
|
||||
assert_eq!(tester.routes(), [(LoginRoute::Direct, None)]);
|
||||
in_frame(cx, handle, |window, cx| window.click("commit", cx));
|
||||
cx.wait_for(handle.into(), Duration::from_secs(2), |window, cx| {
|
||||
window.render_frame(cx);
|
||||
window.find("status-connection").label() == Some("已连接 10.0.0.9")
|
||||
})
|
||||
.await;
|
||||
|
||||
let login = remote.logins().pop().expect("connected");
|
||||
assert_eq!(
|
||||
(login.host.as_str(), login.port, login.user.as_str()),
|
||||
("10.0.0.9", 2222, "deploy")
|
||||
);
|
||||
assert_eq!(login.method, LoginMethod::Password);
|
||||
assert_eq!(login.route, LoginRoute::Direct);
|
||||
assert_eq!(login.password, SecretRef::temporary(LINK_HOST.0));
|
||||
// Every channel it likes, unlike a bastion host's link.
|
||||
assert!(!login.shell_only);
|
||||
cx.update_window(handle.into(), |_, window, cx| {
|
||||
window.render_frame(cx);
|
||||
assert_eq!(
|
||||
window.find(("terminal-tab", LINK_TERMINAL)).label(),
|
||||
Some("10.0.0.9")
|
||||
);
|
||||
assert!(window.try_find(("host-row", LINK_HOST.0)).is_none());
|
||||
// The hosts stay where they were.
|
||||
assert_eq!(window.find("show-hosts").checked(), Some(true));
|
||||
for tool in [
|
||||
"tool-snippets",
|
||||
"tool-history",
|
||||
"tool-docker",
|
||||
"tool-monitor",
|
||||
] {
|
||||
assert!(window.try_find(tool).is_some(), "{tool}");
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
let store = workspace.read_with(cx, |workspace, _| workspace.store().clone());
|
||||
store.read_with(cx, |store, _| {
|
||||
assert!(store.is_temporary(LINK_HOST));
|
||||
assert!(!store.is_external(LINK_HOST));
|
||||
assert!(store.hosts().is_empty());
|
||||
assert!(store.recent_hosts().next().is_none());
|
||||
assert_eq!(
|
||||
store
|
||||
.secrets()
|
||||
.get(&SecretRef::temporary(LINK_HOST.0))
|
||||
.unwrap()
|
||||
.as_deref()
|
||||
.map(String::as_str),
|
||||
Some("p@ss")
|
||||
);
|
||||
});
|
||||
assert!(keychain.is_empty());
|
||||
|
||||
// It goes with its tab, password and all.
|
||||
cx.update_window(handle.into(), |_, window, cx| {
|
||||
window.dispatch_action(Box::new(CloseTerminal(RemoteTerminalId(LINK_TERMINAL))), cx);
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
store.read_with(cx, |store, _| {
|
||||
assert!(store.host(LINK_HOST).is_none());
|
||||
assert!(
|
||||
store
|
||||
.secrets()
|
||||
.get(&SecretRef::temporary(LINK_HOST.0))
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
});
|
||||
let saved = HostStore::load(HostDatabase::open(&path).unwrap()).unwrap();
|
||||
assert!(saved.hosts().is_empty());
|
||||
}
|
||||
|
||||
#[gpui_kit::test]
|
||||
async fn a_link_that_cannot_be_read_says_why_and_opens_nothing(cx: &mut TestAppContext) {
|
||||
let (handle, workspace) = open_workspace_with_credentials(
|
||||
|
||||
@@ -232,7 +232,7 @@ async fn a_host_can_use_a_credential_instead_of_typing_a_login(cx: &mut TestAppC
|
||||
Arc::new(RecordingRemoteProvider::default()),
|
||||
Arc::new(FakeConnectionTester::default()),
|
||||
);
|
||||
in_frame(cx, handle, |window, cx| window.click("new-host", cx));
|
||||
in_frame(cx, handle, |window, cx| window.click("new-host-panel", cx));
|
||||
in_frame(cx, handle, |window, cx| {
|
||||
window.click("host-name", cx);
|
||||
window.input("web-01", cx);
|
||||
|
||||
+10
-10
@@ -8,7 +8,7 @@ async fn a_host_without_a_password_says_what_it_tries_and_is_saved_as_such(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
let (handle, workspace) = open_workspace_with_store(cx, HostStore::empty());
|
||||
in_frame(cx, handle, |window, cx| window.click("new-host", cx));
|
||||
in_frame(cx, handle, |window, cx| window.click("new-host-panel", cx));
|
||||
in_frame(cx, handle, |window, cx| {
|
||||
// A new host logs in with a password.
|
||||
let sources = window.within("host-auth-source");
|
||||
@@ -84,7 +84,7 @@ fn chain(window: &mut gpui_kit::Window) -> Option<String> {
|
||||
async fn a_host_goes_through_the_jump_hosts_it_lists_in_order(cx: &mut TestAppContext) {
|
||||
let (store, [aliyun, zentao, _]) = store_with_jump_hosts();
|
||||
let (handle, workspace) = open_workspace_with_store(cx, store);
|
||||
in_frame(cx, handle, |window, cx| window.click("new-host", cx));
|
||||
in_frame(cx, handle, |window, cx| window.click("new-host-panel", cx));
|
||||
in_frame(cx, handle, |window, cx| {
|
||||
// A new host connects directly, with nothing more to fill in.
|
||||
let routes = window.within("host-route");
|
||||
@@ -203,7 +203,7 @@ async fn testing_a_connection_through_a_jump_host_sends_its_login(cx: &mut TestA
|
||||
let (store, _) = store_with_jump_hosts();
|
||||
let tester = Arc::new(FakeConnectionTester::default());
|
||||
let (handle, _) = open_workspace_with_tester(cx, store, tester.clone());
|
||||
in_frame(cx, handle, |window, cx| window.click("new-host", cx));
|
||||
in_frame(cx, handle, |window, cx| window.click("new-host-panel", cx));
|
||||
in_frame(cx, handle, |window, cx| {
|
||||
window.click("host-name", cx);
|
||||
window.input("db", cx);
|
||||
@@ -238,7 +238,7 @@ async fn testing_a_connection_through_a_jump_host_sends_its_login(cx: &mut TestA
|
||||
#[gpui_kit::test]
|
||||
async fn a_hosts_notes_take_several_lines_and_come_back_when_edited(cx: &mut TestAppContext) {
|
||||
let (handle, workspace) = open_workspace_with_store(cx, HostStore::empty());
|
||||
in_frame(cx, handle, |window, cx| window.click("new-host", cx));
|
||||
in_frame(cx, handle, |window, cx| window.click("new-host-panel", cx));
|
||||
in_frame(cx, handle, |window, cx| {
|
||||
window.click("host-name", cx);
|
||||
window.input("db", cx);
|
||||
@@ -334,7 +334,7 @@ async fn a_host_behind_a_proxy_keeps_the_proxys_password_in_the_keychain(cx: &mu
|
||||
HostStore::empty().with_secrets(secrets.clone()),
|
||||
tester.clone(),
|
||||
);
|
||||
in_frame(cx, handle, |window, cx| window.click("new-host", cx));
|
||||
in_frame(cx, handle, |window, cx| window.click("new-host-panel", cx));
|
||||
in_frame(cx, handle, |window, cx| {
|
||||
window.click("host-name", cx);
|
||||
window.input("abroad", cx);
|
||||
@@ -457,7 +457,7 @@ async fn connection_edits_reconnect_once_but_display_edits_do_not(cx: &mut TestA
|
||||
#[gpui_kit::test]
|
||||
async fn a_form_error_sits_above_the_dialog_and_a_click_puts_it_away(cx: &mut TestAppContext) {
|
||||
let (handle, _) = open_workspace(cx);
|
||||
in_frame(cx, handle, |window, cx| window.click("new-host", cx));
|
||||
in_frame(cx, handle, |window, cx| window.click("new-host-panel", cx));
|
||||
in_frame(cx, handle, |window, cx| {
|
||||
window.click("host-name", cx);
|
||||
window.click("commit", cx);
|
||||
@@ -485,7 +485,7 @@ async fn new_host_dialog_validates_then_inserts(cx: &mut TestAppContext) {
|
||||
|
||||
cx.update_window(handle.into(), |_, window, cx| {
|
||||
window.render_frame(cx);
|
||||
window.click("new-host", cx);
|
||||
window.click("new-host-panel", cx);
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
@@ -640,7 +640,7 @@ async fn a_connection_test_needs_a_host_and_a_user_first(cx: &mut TestAppContext
|
||||
|
||||
cx.update_window(handle.into(), |_, window, cx| {
|
||||
window.render_frame(cx);
|
||||
window.click("new-host", cx);
|
||||
window.click("new-host-panel", cx);
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
@@ -770,7 +770,7 @@ async fn a_new_host_saves_its_password_to_the_keychain(cx: &mut TestAppContext)
|
||||
|
||||
cx.update_window(handle.into(), |_, window, cx| {
|
||||
window.render_frame(cx);
|
||||
window.click("new-host", cx);
|
||||
window.click("new-host-panel", cx);
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
@@ -944,7 +944,7 @@ async fn a_dialogs_choices_are_equal_segments_of_one_track(cx: &mut TestAppConte
|
||||
Arc::new(RecordingRemoteProvider::default()),
|
||||
Arc::new(FakeConnectionTester::default()),
|
||||
);
|
||||
in_frame(cx, handle, |window, cx| window.click("new-host", cx));
|
||||
in_frame(cx, handle, |window, cx| window.click("new-host-panel", cx));
|
||||
in_frame(cx, handle, |window, cx| {
|
||||
let name = window.find("host-name").bounds();
|
||||
let mut group = window.within("host-auth-source");
|
||||
|
||||
@@ -628,7 +628,8 @@ async fn a_click_beside_a_dialog_does_not_close_it(cx: &mut TestAppContext) {
|
||||
cx.run_until_parked();
|
||||
|
||||
for (open, field) in [
|
||||
("new-host", "host-name"),
|
||||
("new-host-panel", "host-name"),
|
||||
("temporary-connection", "host-address"),
|
||||
("new-group", "group-name"),
|
||||
("new-forward", "forward-name"),
|
||||
("new-credential", "credential-name"),
|
||||
@@ -681,7 +682,8 @@ async fn a_dialog_says_what_is_wrong_in_a_notification_that_goes_with_it(cx: &mu
|
||||
cx.run_until_parked();
|
||||
|
||||
for (open, field, error) in [
|
||||
("new-host", "host-name", "请输入名称"),
|
||||
("new-host-panel", "host-name", "请输入名称"),
|
||||
("temporary-connection", "host-address", "请输入地址"),
|
||||
("new-group", "group-name", "请输入分组名称"),
|
||||
("new-forward", "forward-name", "请选择端口转发经由的主机"),
|
||||
("new-credential", "credential-name", "请输入名称"),
|
||||
|
||||
Reference in new issue
Block a user