aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJJ Liebig <jonathan.liebig@gmail.com>2026-09-05 20:39:32 +0200
committerGitHub <noreply@github.com>2026-09-05 21:39:32 +0300
commit50ce4ec2ddb8ec621a77d8773aa6df6d9e7441ff (patch)
treeb61a6e636e31394334ba2ba8e04a34c6e46d4d1b
parent6c52aad511b0fb601e6223bd6ad944f48cbdba6d (diff)
fix: run the full applicable test suite on windows (#3660)
* fix: run the full applicable test suite on windows * test: remove redundant test shell assignment * test: make endpoint and metadata fixtures deterministic * test: tolerate exited servers during windows probe cleanup
-rw-r--r--.gitattributes1
-rw-r--r--.github/workflows/ci.yml6
-rw-r--r--justfile22
-rw-r--r--scripts/test_unix_installer.py1
-rw-r--r--scripts/test_vendor_libghostty_vt.py4
-rw-r--r--scripts/test_vendor_portable_pty.py4
-rw-r--r--scripts/windows_check.ps158
-rw-r--r--scripts/windows_conpty_enhanced_input_probe.ps12
-rw-r--r--scripts/windows_smoke_conpty_path.ps12
-rw-r--r--src/api/server/pane_graphics_stream.rs21
-rw-r--r--src/app/api.rs1
-rw-r--r--src/app/api/layouts.rs4
-rw-r--r--src/app/api/worktrees.rs7
-rw-r--r--src/app/mod.rs22
-rw-r--r--src/client/shell/preferences.rs2
-rw-r--r--src/client/shell/tests/agents_worktrees_notifications.rs4
-rw-r--r--src/detect/manifest_update.rs27
-rw-r--r--src/integration/assets/herdr-agent-state.test.ts6
-rw-r--r--src/integration/tests.rs136
-rw-r--r--src/pane/terminal.rs10
-rw-r--r--src/platform/mod.rs8
-rw-r--r--src/platform/windows.rs33
-rw-r--r--src/server/headless/tests/mod.rs6
-rw-r--r--src/terminal/metadata.rs9
-rw-r--r--tests/api_ping.rs2
-rw-r--r--tests/auto_detect.rs2
-rw-r--r--tests/cli.rs2
-rw-r--r--tests/cross_area.rs2
-rw-r--r--tests/detach_reattach.rs2
-rw-r--r--tests/live_handoff.rs2
-rw-r--r--tests/multi_client.rs2
-rw-r--r--tests/server_headless.rs2
32 files changed, 121 insertions, 291 deletions
diff --git a/.gitattributes b/.gitattributes
index 64835a8d..4158df58 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1 +1,2 @@
+* text=auto eol=lf
packaging/windows/licenses/* binary
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index fee229e5..8723a8e4 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -76,13 +76,11 @@ jobs:
targets: x86_64-pc-windows-msvc
- name: Install Rust tools
- if: matrix.kind == 'unix'
uses: taiki-e/install-action@fd2f5e3d644b484055ebf4268f474c565f148f25 # v2.81.9
with:
tool: just,cargo-nextest
- name: Install Bun
- if: matrix.kind == 'unix'
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.3.14
@@ -138,14 +136,14 @@ jobs:
- name: Run Windows checks
if: matrix.kind == 'windows'
shell: pwsh
- run: .\scripts\windows_check.ps1 -Mode check
+ run: just check
- name: Smoke ConPTY pane
if: matrix.kind == 'windows'
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
- $exe = Join-Path $PWD "target\x86_64-pc-windows-msvc\debug\herdr.exe"
+ $exe = Join-Path $PWD "target\debug\herdr.exe"
.\scripts\windows_smoke_conpty_path.ps1 -ExePath $exe -Session "ci-windows-$env:GITHUB_RUN_ID-$env:GITHUB_RUN_ATTEMPT"
windows-conpty-package:
diff --git a/justfile b/justfile
index 3cc6c03b..f69bb38c 100644
--- a/justfile
+++ b/justfile
@@ -1,4 +1,7 @@
# herdr task runner
+set windows-shell := ["cmd.exe", "/d", "/s", "/c"]
+
+python := if os() == "windows" { "python" } else { "python3" }
# Run tests
test:
@@ -11,21 +14,15 @@ test:
# Run repository maintenance contract tests
maintenance-test:
- python3 -m unittest scripts.test_agent_detection_manifest_check scripts.test_changelog scripts.test_config_reference_check scripts.test_docs_translation_parity scripts.test_hermes_integration_asset scripts.test_package_windows_conpty scripts.test_preview scripts.test_unix_installer scripts.test_vendor_libghostty_vt scripts.test_vendor_portable_pty
+ {{python}} -m unittest scripts.test_agent_detection_manifest_check scripts.test_changelog scripts.test_config_reference_check scripts.test_docs_translation_parity scripts.test_hermes_integration_asset scripts.test_package_windows_conpty scripts.test_preview scripts.test_unix_installer scripts.test_vendor_libghostty_vt scripts.test_vendor_portable_pty
# Run one nextest filter, e.g. `just test-one codex_stale_working`
-[unix]
test-one filter:
cargo nextest run --locked "{{filter}}" --status-level fail --final-status-level fail --failure-output final --success-output never
-[script("powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File")]
-[windows]
-test-one filter:
- cargo nextest run --locked --bin herdr "{{filter}}" --status-level fail --final-status-level fail --failure-output final --success-output never
-
# Enforce deterministic UI hot-path architecture boundaries
ui-hot-path-architecture-test:
- python3 -m unittest scripts.test_ui_hot_path_architecture
+ {{python}} -m unittest scripts.test_ui_hot_path_architecture
# Run fast local lint checks
[unix]
@@ -39,7 +36,6 @@ lint:
& .\scripts\windows_check.ps1 -Mode lint
# Run PR CI checks
-[unix]
ci filter='all()': lint
cargo nextest run --locked -E "{{filter}}" --status-level fail --final-status-level slow --failure-output final --success-output never
just maintenance-test
@@ -72,12 +68,6 @@ install-hooks:
@echo "installed git hooks from .githooks"
# Build release binary
-[unix]
-build:
- cargo build --release --locked
-
-[script("powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File")]
-[windows]
build:
cargo build --release --locked
@@ -92,7 +82,7 @@ bench-release-smoke:
# Test public documentation snapshot and release lifecycle tooling
docs-contract-test:
- bun test scripts/docs/*.test.ts
+ bun test ./scripts/docs
# Test bundled agent integration assets
integration-assets-test:
diff --git a/scripts/test_unix_installer.py b/scripts/test_unix_installer.py
index 4e82f5bd..493a85b3 100644
--- a/scripts/test_unix_installer.py
+++ b/scripts/test_unix_installer.py
@@ -15,6 +15,7 @@ INSTALLER = REPO_ROOT / "distribution" / "install.sh"
REQUIRED_COMMANDS = ("awk", "cat", "chmod", "cp", "mkdir", "mktemp", "mv", "rm")
+@unittest.skipUnless(os.name == "posix", "Unix installer requires a POSIX host")
class UnixInstallerTests(unittest.TestCase):
def setUp(self) -> None:
self.temp_dir = tempfile.TemporaryDirectory(prefix="herdr-installer-test-")
diff --git a/scripts/test_vendor_libghostty_vt.py b/scripts/test_vendor_libghostty_vt.py
index ad5c8d5e..2986af8f 100644
--- a/scripts/test_vendor_libghostty_vt.py
+++ b/scripts/test_vendor_libghostty_vt.py
@@ -108,9 +108,9 @@ class VendorLibghosttyVtTests(unittest.TestCase):
self.assertTrue(index.exists())
text = index.read_text()
missing = [
- str(path.relative_to(project_root))
+ path.relative_to(project_root).as_posix()
for path in patches
- if str(path.relative_to(project_root)) not in text
+ if path.relative_to(project_root).as_posix() not in text
]
self.assertEqual(missing, [])
diff --git a/scripts/test_vendor_portable_pty.py b/scripts/test_vendor_portable_pty.py
index cf3b791a..899b33f8 100644
--- a/scripts/test_vendor_portable_pty.py
+++ b/scripts/test_vendor_portable_pty.py
@@ -65,9 +65,9 @@ class VendorPortablePtyTests(unittest.TestCase):
self.assertTrue(index.exists())
text = index.read_text()
missing = [
- str(path.relative_to(project_root))
+ path.relative_to(project_root).as_posix()
for path in patches
- if str(path.relative_to(project_root)) not in text
+ if path.relative_to(project_root).as_posix() not in text
]
self.assertEqual(missing, [])
diff --git a/scripts/windows_check.ps1 b/scripts/windows_check.ps1
index 2811c6a2..b61da6c2 100644
--- a/scripts/windows_check.ps1
+++ b/scripts/windows_check.ps1
@@ -29,62 +29,12 @@ function Invoke-CargoWithZigCacheRecovery {
Invoke-Checked cargo $Arguments
}
-function Invoke-CargoTestFilter {
- param(
- [Parameter(Mandatory)]
- [string]$Filter,
- [switch]$Exact
- )
-
- $commonArguments = @(
- "test",
- "--locked",
- "--target",
- "x86_64-pc-windows-msvc",
- "--bin",
- "herdr",
- $Filter
- )
- $harnessArguments = @("--list")
- if ($Exact) {
- $harnessArguments += "--exact"
- }
-
- $listArguments = $commonArguments + @("--") + $harnessArguments
- $listOutput = @(& cargo @listArguments)
- if ($LASTEXITCODE -ne 0) {
- throw "could not enumerate tests for filter '$Filter': $($listOutput -join [Environment]::NewLine)"
- }
-
- $testNames = @(
- foreach ($line in $listOutput) {
- $match = [regex]::Match([string]$line, '^\s*(\S+): test\s*$')
- if ($match.Success) {
- $match.Groups[1].Value
- }
- }
- )
- if ($testNames.Count -eq 0) {
- throw "test filter '$Filter' selected zero tests"
- }
-
- Write-Host "Running $($testNames.Count) test(s) for '$Filter'"
- $runArguments = $commonArguments
- if ($Exact) {
- $runArguments += @("--", "--exact")
- }
- Invoke-Checked cargo $runArguments
-}
-
-Invoke-Checked rustup @("target", "add", "x86_64-pc-windows-msvc")
Invoke-Checked cargo @("fmt", "--check")
Invoke-CargoWithZigCacheRecovery @(
"clippy",
"--bin",
"herdr",
"--locked",
- "--target",
- "x86_64-pc-windows-msvc",
"--",
"-D",
"warnings"
@@ -94,9 +44,5 @@ if ($Mode -eq "lint") {
return
}
-Invoke-CargoTestFilter "windows_"
-Invoke-CargoTestFilter "server::client_transport::tests"
-Invoke-CargoTestFilter "input::lease::tests::duplicate_physical_press_normalizes_for_forwarded_and_consumed_leases" -Exact
-Invoke-CargoTestFilter "client::shell::tests::input_domain::physical_release_uses_the_leased_press_code_with_current_modifiers" -Exact
-Invoke-CargoTestFilter "client::shell::tests::input_domain::pane_key_release_keeps_the_press_target" -Exact
-Invoke-Checked cargo @("build", "--locked", "--target", "x86_64-pc-windows-msvc")
+Invoke-Checked just @("test")
+Invoke-Checked cargo @("build", "--locked")
diff --git a/scripts/windows_conpty_enhanced_input_probe.ps1 b/scripts/windows_conpty_enhanced_input_probe.ps1
index 1e452521..559396c4 100644
--- a/scripts/windows_conpty_enhanced_input_probe.ps1
+++ b/scripts/windows_conpty_enhanced_input_probe.ps1
@@ -478,7 +478,7 @@ fn main() {
} catch {
Write-Host "server stop during cleanup failed: $($_.Exception.Message)"
}
- Wait-Process -Id $server.Id -Timeout 10 -ErrorAction SilentlyContinue
+ $null = $server.WaitForExit(10000)
$server.Refresh()
if (-not $server.HasExited) {
& taskkill.exe /PID $server.Id /T /F 2>&1 | Out-Null
diff --git a/scripts/windows_smoke_conpty_path.ps1 b/scripts/windows_smoke_conpty_path.ps1
index bec93b66..484f2a1f 100644
--- a/scripts/windows_smoke_conpty_path.ps1
+++ b/scripts/windows_smoke_conpty_path.ps1
@@ -135,7 +135,7 @@ try {
} catch {
Write-Host "server stop during cleanup failed: $($_.Exception.Message)"
}
- Wait-Process -Id $server.Id -Timeout 10 -ErrorAction SilentlyContinue
+ $null = $server.WaitForExit(10000)
$server.Refresh()
if (-not $server.HasExited) {
& taskkill.exe /PID $server.Id /T /F 2>&1 | Out-Null
diff --git a/src/api/server/pane_graphics_stream.rs b/src/api/server/pane_graphics_stream.rs
index e4d3315d..0df857fc 100644
--- a/src/api/server/pane_graphics_stream.rs
+++ b/src/api/server/pane_graphics_stream.rs
@@ -393,7 +393,7 @@ fn read_line(
total_deadline,
"timed out reading stream frame header",
)?;
- match stream.read(&mut byte) {
+ match wait.read(stream, &mut byte) {
Ok(0) => return Ok(None),
Ok(_) => {
wait.on_progress();
@@ -455,7 +455,7 @@ fn read_exact(
)?;
let remaining = len - data.len();
let read_len = remaining.min(chunk.len());
- match stream.read(&mut chunk[..read_len]) {
+ match wait.read(stream, &mut chunk[..read_len]) {
Ok(0) if data.is_empty() => return Ok(None),
Ok(0) => {
return Err(io::Error::new(
@@ -494,6 +494,17 @@ enum ReadWait {
}
impl ReadWait {
+ fn read(&self, stream: &mut LocalStream, buffer: &mut [u8]) -> io::Result<usize> {
+ if matches!(self, Self::SocketTimeout) {
+ return stream.read(buffer);
+ }
+ match crate::ipc::poll_local_stream_read_count(stream, buffer)? {
+ crate::ipc::LocalStreamReadCount::Data(count) => Ok(count),
+ crate::ipc::LocalStreamReadCount::Pending => Err(io::ErrorKind::WouldBlock.into()),
+ crate::ipc::LocalStreamReadCount::Closed => Ok(0),
+ }
+ }
+
fn after_retry(&mut self, idle_deadline: Option<Instant>, total_deadline: Option<Instant>) {
if let Self::Poll(backoff) = self {
sleep_until_poll(idle_deadline, total_deadline, backoff.interval);
@@ -545,9 +556,11 @@ fn with_timed_reads<T>(
finish_timed_read(result, || stream.set_recv_timeout(None))
}
Err(err) if err.kind() == io::ErrorKind::Unsupported => {
- stream.set_nonblocking(true)?;
+ crate::ipc::set_local_stream_polling(stream, true)?;
let result = read(stream, ReadWait::Poll(PollBackoff::new()));
- finish_timed_read(result, || stream.set_nonblocking(false))
+ finish_timed_read(result, || {
+ crate::ipc::set_local_stream_polling(stream, false)
+ })
}
// A peer can disconnect after the caller's running check but before
// setsockopt. macOS reports that closed-socket race as EINVAL.
diff --git a/src/app/api.rs b/src/app/api.rs
index 598356b0..c504b4c0 100644
--- a/src/app/api.rs
+++ b/src/app/api.rs
@@ -2207,6 +2207,7 @@ mod tests {
crate::api::EventHub::default(),
);
let workspace = crate::workspace::Workspace::test_new("restored");
+ app.state.default_shell = test_support::exiting_test_command().into();
let pane_id = workspace.tabs[0].root_pane;
let terminal_id = workspace.terminal_id(pane_id).cloned().unwrap();
app.state.workspaces = vec![workspace];
diff --git a/src/app/api/layouts.rs b/src/app/api/layouts.rs
index 943ed835..74190434 100644
--- a/src/app/api/layouts.rs
+++ b/src/app/api/layouts.rs
@@ -747,7 +747,7 @@ mod tests {
second: Box::new(LayoutNode::Pane {
pane: LayoutPane {
label: Some("tests".into()),
- command: Some(vec!["sh".into(), "-c".into(), "true".into()]),
+ command: Some(vec![exiting_test_command().into()]),
env: std::collections::HashMap::from([(
"HERDR_ROLE".into(),
"tests".into(),
@@ -789,7 +789,7 @@ mod tests {
assert_eq!(second_pane.label.as_deref(), Some("tests"));
assert_eq!(
second_pane.command,
- Some(vec!["sh".into(), "-c".into(), "true".into()])
+ Some(vec![exiting_test_command().into()])
);
assert!(matches!(
&app.event_hub.events_after(0).last().expect("layout event").1.data,
diff --git a/src/app/api/worktrees.rs b/src/app/api/worktrees.rs
index fd7bb0fb..3c8407b9 100644
--- a/src/app/api/worktrees.rs
+++ b/src/app/api/worktrees.rs
@@ -794,13 +794,15 @@ mod tests {
fn test_app_with_event_hub(event_hub: crate::api::EventHub) -> App {
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
- App::new(
+ let mut app = App::new(
&Config::default(),
crate::app::AppPolicy::TEST,
None,
api_rx,
event_hub,
- )
+ );
+ app.state.default_shell = test_shell().into();
+ app
}
#[cfg(windows)]
@@ -815,7 +817,6 @@ mod tests {
fn app_with_parent(repo: &Path) -> App {
let mut app = test_app();
- app.state.default_shell = test_shell().into();
let mut parent = Workspace::test_new("main");
parent.identity_cwd = repo.to_path_buf();
app.state.workspaces = vec![parent];
diff --git a/src/app/mod.rs b/src/app/mod.rs
index ed2c5fd9..0f78cf8b 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -9,6 +9,8 @@ pub(crate) mod agent_view;
mod agents;
pub(crate) use agents::{AGENT_START_SETTLE_DELAY, MAX_AGENT_START_TIMEOUT};
mod api;
+#[cfg(test)]
+pub(crate) use api::test_support::exiting_test_command;
mod api_helpers;
pub(crate) use api_helpers::limit_snapshot_lines;
mod creation;
@@ -975,13 +977,15 @@ mod tests {
fn test_app() -> App {
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
- App::new(
+ let mut app = App::new(
&Config::default(),
crate::app::AppPolicy::TEST,
None,
api_rx,
crate::api::EventHub::default(),
- )
+ );
+ app.state.default_shell = exiting_test_command().into();
+ app
}
fn unique_temp_path(name: &str) -> std::path::PathBuf {
@@ -992,16 +996,6 @@ mod tests {
std::env::temp_dir().join(format!("herdr-{name}-{}-{stamp}", std::process::id()))
}
- #[cfg(windows)]
- fn exiting_test_command() -> &'static str {
- "C:\\Windows\\System32\\whoami.exe"
- }
-
- #[cfg(not(windows))]
- fn exiting_test_command() -> &'static str {
- "/usr/bin/true"
- }
-
fn config_env_lock() -> &'static Mutex<()> {
crate::config::test_config_env_lock()
}
@@ -2657,7 +2651,7 @@ mod tests {
async fn pane_split_request_applies_ratio() {
let _guard = config_env_lock().lock().unwrap();
let original_shell = std::env::var_os("SHELL");
- std::env::set_var("SHELL", "/usr/bin/true");
+ std::env::set_var("SHELL", exiting_test_command());
let mut app = test_app();
let workspace = Workspace::test_new("api-pane-split-ratio");
@@ -2713,7 +2707,7 @@ mod tests {
async fn pane_split_request_uses_active_focused_pane_when_target_is_omitted() {
let _guard = config_env_lock().lock().unwrap();
let original_shell = std::env::var_os("SHELL");
- std::env::set_var("SHELL", "/usr/bin/true");
+ std::env::set_var("SHELL", exiting_test_command());
let mut app = test_app();
let workspace = Workspace::test_new("api-pane-split-current");
diff --git a/src/client/shell/preferences.rs b/src/client/shell/preferences.rs
index dc6cc1ac..f657dfbf 100644
--- a/src/client/shell/preferences.rs
+++ b/src/client/shell/preferences.rs
@@ -54,7 +54,7 @@ pub(super) fn store(path: &Path, preferences: ClientChromePreferences) -> Result
let temp_path = parent.join(temp_name);
std::fs::write(&temp_path, content)
.map_err(|error| format!("failed to write client shell state: {error}"))?;
- crate::platform::replace_file(&temp_path, path).map_err(|error| {
+ std::fs::rename(&temp_path, path).map_err(|error| {
let _ = std::fs::remove_file(&temp_path);
format!("failed to replace client shell state: {error}")
})
diff --git a/src/client/shell/tests/agents_worktrees_notifications.rs b/src/client/shell/tests/agents_worktrees_notifications.rs
index 055b594d..2ccfe77b 100644
--- a/src/client/shell/tests/agents_worktrees_notifications.rs
+++ b/src/client/shell/tests/agents_worktrees_notifications.rs
@@ -966,7 +966,9 @@ fn worktree_create_previews_the_endpoint_owned_checkout_path() {
&state.overlay,
Some(ClientShellOverlay::WorktreeCreate(create))
if create.checkout_path
- == "/tmp/herdr-worktrees/repo/feature-client-shell"
+ == std::path::Path::new("/tmp/herdr-worktrees")
+ .join("repo")
+ .join("feature-client-shell").to_string_lossy()
));
let submit = state.handle_input_bytes(b"\r");
let [ClientShellAction::Endpoint { request, .. }] = &submit.actions[..] else {
diff --git a/src/detect/manifest_update.rs b/src/detect/manifest_update.rs
index 4e165fe9..33493059 100644
--- a/src/detect/manifest_update.rs
+++ b/src/detect/manifest_update.rs
@@ -699,7 +699,14 @@ path = "codex.toml"
.unwrap();
std::env::set_var(
CATALOG_URL_ENV,
- format!("file://{}", web_dir.join("index.toml").display()),
+ format!(
+ "file:///{}",
+ web_dir
+ .join("index.toml")
+ .to_string_lossy()
+ .replace('\\', "/")
+ .trim_start_matches('/')
+ ),
);
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
@@ -762,7 +769,14 @@ path = "codex.toml"
fs::write(remote_manifest_path(Agent::Codex), current).unwrap();
std::env::set_var(
CATALOG_URL_ENV,
- format!("file://{}", web_dir.join("index.toml").display()),
+ format!(
+ "file:///{}",
+ web_dir
+ .join("index.toml")
+ .to_string_lossy()
+ .replace('\\', "/")
+ .trim_start_matches('/')
+ ),
);
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
@@ -833,7 +847,14 @@ path = "missing-cursor.toml"
.unwrap();
std::env::set_var(
CATALOG_URL_ENV,
- format!("file://{}", web_dir.join("index.toml").display()),
+ format!(
+ "file:///{}",
+ web_dir
+ .join("index.toml")
+ .to_string_lossy()
+ .replace('\\', "/")
+ .trim_start_matches('/')
+ ),
);
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
diff --git a/src/integration/assets/herdr-agent-state.test.ts b/src/integration/assets/herdr-agent-state.test.ts
index 2a86f0ca..7720e088 100644
--- a/src/integration/assets/herdr-agent-state.test.ts
+++ b/src/integration/assets/herdr-agent-state.test.ts
@@ -121,7 +121,7 @@ async function startRecordingServer(name: string): Promise<unknown[]> {
server = recordingServer;
await new Promise<void>((resolve, reject) => {
recordingServer.once("error", reject);
- recordingServer.listen(recordingSocketPath, resolve);
+ recordingServer.listen(originalPlatform === "win32" ? `\\\\.\\pipe\\${recordingSocketPath}` : recordingSocketPath, resolve);
});
configureIntegrationEnvironment(recordingSocketPath);
return requests;
@@ -378,7 +378,7 @@ test("Pi waits for a replacement session report before publishing state", async
server = recordingServer;
await new Promise<void>((resolve, reject) => {
recordingServer.once("error", reject);
- recordingServer.listen(recordingSocketPath, resolve);
+ recordingServer.listen(originalPlatform === "win32" ? `\\\\.\\pipe\\${recordingSocketPath}` : recordingSocketPath, resolve);
});
configureIntegrationEnvironment(recordingSocketPath);
@@ -457,7 +457,7 @@ async function startDroppedFirstResponseServer(name: string) {
server = recordingServer;
await new Promise<void>((resolve, reject) => {
recordingServer.once("error", reject);
- recordingServer.listen(recordingSocketPath, resolve);
+ recordingServer.listen(originalPlatform === "win32" ? `\\\\.\\pipe\\${recordingSocketPath}` : recordingSocketPath, resolve);
});
configureIntegrationEnvironment(recordingSocketPath);
diff --git a/src/integration/tests.rs b/src/integration/tests.rs
index a02a549e..b2210074 100644
--- a/src/integration/tests.rs
+++ b/src/integration/tests.rs
@@ -2082,8 +2082,8 @@ fn install_droid_writes_hook_to_settings_and_cleans_legacy_hooks_json() {
fs::write(
droid_dir.join("hooks.json"),
format!(
- r#"{{"hooks":{{"SessionStart":[{{"hooks":[{{"type":"command","command":"{}","timeout":10}}]}}],"PreToolUse":[{{"matcher":"Read","hooks":[{{"type":"command","command":"echo keep","timeout":10}}]}}]}}}}"#,
- legacy_command,
+ r#"{{"hooks":{{"SessionStart":[{{"hooks":[{{"type":"command","command":{},"timeout":10}}]}}],"PreToolUse":[{{"matcher":"Read","hooks":[{{"type":"command","command":"echo keep","timeout":10}}]}}]}}}}"#,
+ serde_json::to_string(&legacy_command).unwrap(),
),
)
.unwrap();
@@ -2207,16 +2207,16 @@ fn uninstall_droid_removes_herdr_hooks_and_preserves_others() {
fs::write(
droid_dir.join("hooks.json"),
format!(
- r#"{{"hooks":{{"SessionStart":[{{"hooks":[{{"type":"command","command":"{}","timeout":10}},{{"type":"command","command":"echo keep","timeout":10}}]}}],"PreToolUse":[{{"matcher":"Read","hooks":[{{"type":"command","command":"echo read","timeout":10}}]}}]}}}}"#,
- command,
+ r#"{{"hooks":{{"SessionStart":[{{"hooks":[{{"type":"command","command":{},"timeout":10}},{{"type":"command","command":"echo keep","timeout":10}}]}}],"PreToolUse":[{{"matcher":"Read","hooks":[{{"type":"command","command":"echo read","timeout":10}}]}}]}}}}"#,
+ serde_json::to_string(&command).unwrap(),
),
)
.unwrap();
fs::write(
droid_dir.join("settings.json"),
format!(
- r#"{{"hooks":{{"SessionStart":[{{"hooks":[{{"type":"command","command":"{}","timeout":10}}]}}],"PostToolUse":[{{"matcher":"Edit","hooks":[{{"type":"command","command":"echo post","timeout":10}}]}}]}}}}"#,
- command,
+ r#"{{"hooks":{{"SessionStart":[{{"hooks":[{{"type":"command","command":{},"timeout":10}}]}}],"PostToolUse":[{{"matcher":"Edit","hooks":[{{"type":"command","command":"echo post","timeout":10}}]}}]}}}}"#,
+ serde_json::to_string(&command).unwrap(),
),
)
.unwrap();
@@ -2812,128 +2812,6 @@ fn bundled_integration_asset_versions_match_expected_versions() {
}
#[test]
-fn bundled_integration_assets_report_session_refs() {
- assert!(PI_EXTENSION_ASSET.contains("agent_session_path"));
- assert!(PI_EXTENSION_ASSET.contains("agent_session_id"));
- assert!(PI_EXTENSION_ASSET.contains("ctx?.mode !== \"tui\""));
- assert!(PI_EXTENSION_ASSET.contains("pane.report_agent_session"));
- assert!(PI_EXTENSION_ASSET.contains("pane.report_agent\""));
- assert!(PI_EXTENSION_ASSET.contains("pi.on(\"agent_start\""));
- assert!(PI_EXTENSION_ASSET.contains("pi.on(\"agent_settled\""));
- assert!(!PI_EXTENSION_ASSET.contains("pi.on(\"session_shutdown\""));
- assert!(OMP_EXTENSION_ASSET.contains("agent_session_path"));
- assert!(OMP_EXTENSION_ASSET.contains("agent_session_id"));
- assert!(OMP_EXTENSION_ASSET.contains("ctx?.hasUI !== true"));
- assert!(OMP_EXTENSION_ASSET.contains("pane.report_agent_session"));
- assert!(OMP_EXTENSION_ASSET.contains("pane.report_agent\""));
- assert!(OMP_EXTENSION_ASSET.contains("pi.on(\"agent_start\""));
- assert!(OMP_EXTENSION_ASSET.contains("pi.on(\"agent_end\""));
- assert!(OMP_EXTENSION_ASSET.contains("pi.on(\"session_shutdown\""));
- assert!(
- CLAUDE_HOOK_ASSET.contains("agent_session_id")
- || CLAUDE_HOOK_ASSET.contains("--agent-session-id")
- );
- assert!(
- CLAUDE_HOOK_ASSET.contains("agent_session_path")
- || CLAUDE_HOOK_ASSET.contains("--agent-session-path")
- );
- assert!(CLAUDE_HOOK_ASSET.contains("agent_id"));
- assert!(
- CLAUDE_HOOK_ASSET.contains("session_start_source")
- || CLAUDE_HOOK_ASSET.contains("--session-start-source")
- );
- assert!(
- CLAUDE_HOOK_ASSET.contains("pane.report_agent_session")
- || CLAUDE_HOOK_ASSET.contains("report-agent-session")
- );
- assert!(!CLAUDE_HOOK_ASSET.contains("\"state\": action"));
- assert!(!CLAUDE_HOOK_ASSET.contains("pane.release_agent"));
- assert!(
- CODEX_HOOK_ASSET.contains("HERDR_HOOK_INPUT_FILE")
- || CODEX_HOOK_ASSET.contains("In.ReadToEnd")
- );
- assert!(
- CODEX_HOOK_ASSET.contains("agent_session_id")
- || CODEX_HOOK_ASSET.contains("--agent-session-id")
- );
- assert!(
- CODEX_HOOK_ASSET.contains("session_start_source")
- || CODEX_HOOK_ASSET.contains("--session-start-source")
- );
- assert!(CODEX_HOOK_ASSET.contains("CODEX_THREAD_ID"));
- assert!(
- CODEX_HOOK_ASSET.contains("pane.report_agent_session")
- || CODEX_HOOK_ASSET.contains("report-agent-session")
- );
- assert!(!CODEX_HOOK_ASSET.contains("\"state\": action"));
- assert!(!CODEX_HOOK_ASSET.contains("pane.release_agent"));
- assert!(KIMI_HOOK_ASSET.contains("source\": \"herdr:kimi"));
- assert!(KIMI_HOOK_ASSET.contains("agent_session_id"));
- assert!(KIMI_HOOK_ASSET.contains("method = \"pane.report_agent_session\""));
- assert!(KIMI_HOOK_ASSET.contains("params[\"session_start_source\"] = \"startup\""));
- assert!(KIMI_HOOK_ASSET.contains("method = \"pane.report_agent\""));
- assert!(KIMI_HOOK_ASSET.contains("params[\"state\"] = action"));
- assert!(!KIMI_HOOK_ASSET.contains("pane.release_agent"));
- assert!(COPILOT_HOOK_ASSET.contains("agent_session_id"));
- assert!(COPILOT_HOOK_ASSET.contains("pane.report_agent_session"));
- assert!(!COPILOT_HOOK_ASSET.contains("\"state\":"));
- assert!(!COPILOT_HOOK_ASSET.contains("pane.release_agent"));
- assert!(DEVIN_HOOK_ASSET.contains("HERDR_DEVIN_LIST_JSON"));
- assert!(DEVIN_HOOK_ASSET.contains("\"method\": \"pane.report_agent_session\""));
- assert!(!DEVIN_HOOK_ASSET.contains("\"method\": \"pane.report_agent\""));
- assert!(!DEVIN_HOOK_ASSET.contains("\"state\":"));
- assert!(!DEVIN_HOOK_ASSET.contains("pane.release_agent"));
- assert!(DEVIN_HOOK_ASSET.contains("agent_session_id"));
- assert!(DROID_HOOK_ASSET.contains("agent_session_id"));
- assert!(DROID_HOOK_ASSET.contains("pane.report_agent_session"));
- assert!(!DROID_HOOK_ASSET.contains("\"state\": action"));
- assert!(!DROID_HOOK_ASSET.contains("pane.release_agent"));
- assert!(OPENCODE_PLUGIN_ASSET.contains("properties?.sessionID"));
- assert!(OPENCODE_PLUGIN_ASSET.contains("params.agent_session_id = sessionID"));
- assert!(OPENCODE_PLUGIN_ASSET.contains("pane.report_agent_session"));
- assert!(OPENCODE_PLUGIN_ASSET.contains("reportState"));
- assert!(!OPENCODE_PLUGIN_ASSET.contains("pane.release_agent"));
- assert!(KILO_PLUGIN_ASSET.contains("SOURCE = \"herdr:kilo\""));
- assert!(KILO_PLUGIN_ASSET.contains("AGENT = \"kilo\""));
- assert!(KILO_PLUGIN_ASSET.contains("pane.report_agent_session"));
- assert!(KILO_PLUGIN_ASSET.contains("session_start_source: \"startup\""));
- assert!(KILO_PLUGIN_ASSET.contains("reportState"));
- assert!(!KILO_PLUGIN_ASSET.contains("pane.release_agent"));
- assert!(QODERCLI_HOOK_ASSET.contains("HERDR_PANE_ID"));
- assert!(QODERCLI_HOOK_ASSET.contains("session_id"));
- assert!(QODERCLI_HOOK_ASSET.contains("report-agent-session"));
- assert!(QODERCLI_HOOK_ASSET.contains("--agent-session-id"));
- assert!(!QODERCLI_HOOK_ASSET.contains("report-agent\""));
- assert!(!QODERCLI_HOOK_ASSET.contains("release-agent"));
- assert!(CURSOR_HOOK_ASSET.contains("HERDR_INTEGRATION_ID=cursor"));
- assert!(CURSOR_HOOK_ASSET.contains("conversation_id"));
- assert!(CURSOR_HOOK_ASSET.contains("conversationId"));
- assert!(CURSOR_HOOK_ASSET.contains("sessionId"));
- assert!(CURSOR_HOOK_ASSET.contains("agent_session_id"));
- assert!(CURSOR_HOOK_ASSET.contains("pane.report_agent_session"));
- assert!(CURSOR_HOOK_ASSET.contains("hook_event_name"));
- assert!(CURSOR_HOOK_ASSET.contains("sessionStart"));
- assert!(!CURSOR_HOOK_ASSET.contains("\"state\":"));
- assert!(!CURSOR_HOOK_ASSET.contains("pane.release_agent"));
- assert!(MASTRACODE_HOOK_ASSET.contains("HERDR_INTEGRATION_ID=mastracode"));
- assert!(MASTRACODE_HOOK_ASSET.contains("HERDR_INTEGRATION_VERSION=2"));
- assert!(MASTRACODE_HOOK_ASSET.contains("session_id"));
- assert!(!MASTRACODE_HOOK_ASSET.contains("run_id"));
- assert!(MASTRACODE_HOOK_ASSET.contains("agent_session_id"));
- assert!(MASTRACODE_HOOK_ASSET.contains("pane.report_agent_session"));
- assert!(MASTRACODE_HOOK_ASSET.contains("session_start_source"));
- assert!(MASTRACODE_HOOK_ASSET.contains("pane.report_agent"));
- assert!(GROK_HOOK_ASSET.contains("HERDR_INTEGRATION_ID=grok"));
- assert!(GROK_HOOK_ASSET.contains("GROK_SESSION_ID"));
- assert!(GROK_HOOK_ASSET.contains("sessionId"));
- assert!(GROK_HOOK_ASSET.contains("agent_session_id"));
- assert!(GROK_HOOK_ASSET.contains("pane.report_agent_session"));
- assert!(GROK_HOOK_ASSET.contains("herdr:grok"));
- assert!(!GROK_HOOK_ASSET.contains("\"state\":"));
- assert!(!GROK_HOOK_ASSET.contains("pane.release_agent"));
-}
-
-#[test]
fn process_owned_integration_assets_do_not_report_release() {
for (name, asset) in [
("pi", PI_EXTENSION_ASSET),
@@ -4061,7 +3939,7 @@ fn install_antigravity_cli_rewrites_stale_herdr_block() {
assert!(entries[0]
.get("command")
.and_then(Value::as_str)
- .is_some_and(|command| command.contains("herdr-agent-state")));
+ .is_some_and(|command| command != "stale" && command != "stale idle"));
std::env::remove_var(ANTIGRAVITY_CLI_CONFIG_DIR_ENV_VAR);
let _ = fs::remove_dir_all(base);
diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs
index ecc12ed5..a804a7fb 100644
--- a/src/pane/terminal.rs
+++ b/src/pane/terminal.rs
@@ -4681,10 +4681,18 @@ mod tests {
unicode: u16::from(b'/'),
control_key_state: 0x0010,
});
+ #[cfg(windows)]
+ let legacy_expected = b"\x1b[55;8;47;1;16;3_".as_slice();
+ #[cfg(not(windows))]
+ let legacy_expected = b"///".as_slice();
assert_eq!(
pane.encode_terminal_key(shifted.clone(), crate::input::KeyboardProtocol::Legacy,),
- b"///"
+ legacy_expected
);
+ let mut terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap();
+ terminal.write(b"\x1b[>15u");
+ let (tx, _rx) = mpsc::channel(4);
+ let pane = GhosttyPaneTerminal::new(terminal, tx).unwrap();
assert_eq!(
pane.encode_terminal_key(shifted, crate::input::KeyboardProtocol::Kitty { flags: 15 },),
b"\x1b[47;2:1u\x1b[47;2:2u\x1b[47;2:2u"
diff --git a/src/platform/mod.rs b/src/platform/mod.rs
index f15c71ef..402b695d 100644
--- a/src/platform/mod.rs
+++ b/src/platform/mod.rs
@@ -227,14 +227,6 @@ mod unix_common;
#[cfg(unix)]
pub(crate) use unix_common::{begin_cli_output, end_cli_output};
-#[cfg(not(windows))]
-pub(crate) fn replace_file(
- source: &std::path::Path,
- destination: &std::path::Path,
-) -> std::io::Result<()> {
- std::fs::rename(source, destination)
-}
-
#[cfg(not(unix))]
pub(crate) fn begin_cli_output() {}
diff --git a/src/platform/windows.rs b/src/platform/windows.rs
index 8d370964..10fbb6f0 100644
--- a/src/platform/windows.rs
+++ b/src/platform/windows.rs
@@ -19,39 +19,6 @@ pub(super) fn read_terminal_grid_size() -> std::io::Result<(u16, u16)> {
crossterm::terminal::size()
}
-pub(crate) fn replace_file(
- source: &std::path::Path,
- destination: &std::path::Path,
-) -> std::io::Result<()> {
- use std::os::windows::ffi::OsStrExt;
- use windows_sys::Win32::Storage::FileSystem::{
- MoveFileExW, MOVEFILE_REPLACE_EXISTING, MOVEFILE_WRITE_THROUGH,
- };
-
- let source = source
- .as_os_str()
- .encode_wide()
- .chain(std::iter::once(0))
- .collect::<Vec<_>>();
- let destination = destination
- .as_os_str()
- .encode_wide()
- .chain(std::iter::once(0))
- .collect::<Vec<_>>();
- let moved = unsafe {
- MoveFileExW(
- source.as_ptr(),
- destination.as_ptr(),
- MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH,
- )
- };
- if moved == 0 {
- Err(std::io::Error::last_os_error())
- } else {
- Ok(())
- }
-}
-
pub(crate) fn set_default_plugin_pane_pwd(
_env: &mut Vec<(String, String)>,
_cwd: &std::path::Path,
diff --git a/src/server/headless/tests/mod.rs b/src/server/headless/tests/mod.rs
index 1e4249b5..e7e73e8b 100644
--- a/src/server/headless/tests/mod.rs
+++ b/src/server/headless/tests/mod.rs
@@ -18,7 +18,7 @@ fn test_headless_server() -> HeadlessServer {
fn test_headless_server_with_event_hub(event_hub: api::EventHub) -> HeadlessServer {
let config = crate::config::Config::default();
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
- let app = crate::app::App::new(
+ let mut app = crate::app::App::new(
&config,
crate::app::AppPolicy::TEST,
None,
@@ -26,6 +26,7 @@ fn test_headless_server_with_event_hub(event_hub: api::EventHub) -> HeadlessServ
event_hub,
);
+ app.state.default_shell = crate::app::exiting_test_command().into();
let dir = std::env::temp_dir().join(format!(
"hh-{}-{}",
std::process::id(),
@@ -619,6 +620,9 @@ async fn client_shell_attach_seeds_workspace() {
#[tokio::test]
async fn client_shell_endpoint_request_uses_the_selected_connection() {
let mut server = test_headless_server();
+ server.app.state.workspaces = vec![crate::workspace::Workspace::test_new("endpoint")];
+ server.app.state.ensure_test_terminals();
+ server.app.state.active = Some(0);
let (writer, control_rx, _render_rx) = test_client_writer();
let client_id = 41;
assert!(
diff --git a/src/terminal/metadata.rs b/src/terminal/metadata.rs
index 2a912b27..91b1d582 100644
--- a/src/terminal/metadata.rs
+++ b/src/terminal/metadata.rs
@@ -881,11 +881,12 @@ mod tests {
});
assert_eq!(terminal.next_agent_metadata_expiry(), Some(old_deadline));
- assert_eq!(
- terminal.effective_presentation().title.as_deref(),
- Some("Prompt title")
+ let presentation = terminal.effective_presentation_for_state_at(
+ terminal.state,
+ old_deadline - Duration::from_millis(1),
);
- assert_eq!(terminal.effective_presentation().display_agent, None);
+ assert_eq!(presentation.title.as_deref(), Some("Prompt title"));
+ assert_eq!(presentation.display_agent, None);
let mutation = terminal
.expire_agent_metadata_at(old_deadline, old_deadline)
diff --git a/tests/api_ping.rs b/tests/api_ping.rs
index 131f78f2..fcfaf41f 100644
--- a/tests/api_ping.rs
+++ b/tests/api_ping.rs
@@ -1,3 +1,5 @@
+#![cfg(unix)]
+
pub mod support;
use std::fs;
diff --git a/tests/auto_detect.rs b/tests/auto_detect.rs
index 49473dbb..ae902dac 100644
--- a/tests/auto_detect.rs
+++ b/tests/auto_detect.rs
@@ -1,6 +1,6 @@
//! Integration tests for auto-detect launch behavior.
-#![cfg(not(target_os = "macos"))]
+#![cfg(all(unix, not(target_os = "macos")))]
pub mod support;
diff --git a/tests/cli.rs b/tests/cli.rs
index ff238996..17648638 100644
--- a/tests/cli.rs
+++ b/tests/cli.rs
@@ -1,4 +1,4 @@
-#![cfg(not(target_os = "macos"))]
+#![cfg(all(unix, not(target_os = "macos")))]
pub mod support;
diff --git a/tests/cross_area.rs b/tests/cross_area.rs
index 91c17800..01c15c68 100644
--- a/tests/cross_area.rs
+++ b/tests/cross_area.rs
@@ -1,5 +1,7 @@
//! Cross-area integration tests for end-to-end persistence flows.
+#![cfg(unix)]
+
pub mod support;
use std::fs;
diff --git a/tests/detach_reattach.rs b/tests/detach_reattach.rs
index cdf0736c..0bbe94b6 100644
--- a/tests/detach_reattach.rs
+++ b/tests/detach_reattach.rs
@@ -1,6 +1,8 @@
//! Integration tests for detach/reattach flow.
//!
+#![cfg(unix)]
+
pub mod support;
use std::fs;
diff --git a/tests/live_handoff.rs b/tests/live_handoff.rs
index 50b39698..3838b888 100644
--- a/tests/live_handoff.rs
+++ b/tests/live_handoff.rs
@@ -1,3 +1,5 @@
+#![cfg(unix)]
+
pub mod support;
use std::fs;
diff --git a/tests/multi_client.rs b/tests/multi_client.rs
index 514493cb..823fa0c1 100644
--- a/tests/multi_client.rs
+++ b/tests/multi_client.rs
@@ -1,5 +1,7 @@
//! Gate B integration tests for the ClientShell protocol.
+#![cfg(unix)]
+
pub mod support;
use std::fs;
diff --git a/tests/server_headless.rs b/tests/server_headless.rs
index f6eb0771..6bb054ad 100644
--- a/tests/server_headless.rs
+++ b/tests/server_headless.rs
@@ -1,5 +1,7 @@
//! Integration tests for headless server mode.
+#![cfg(unix)]
+
pub mod support;
use std::fs;