Skip to content

Commit ef20e20

Browse files
authored
Merge pull request #84 from compoundingtech/schickling-assistant/2026-07-30-pty-fleet-contract
test: gate st2 against bounded PTY fleet observation
2 parents 16097da + e1e0666 commit ef20e20

3 files changed

Lines changed: 150 additions & 1 deletion

File tree

flake.lock

Lines changed: 23 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

flake.nix

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@
44
inputs = {
55
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
66
flake-utils.url = "github:numtide/flake-utils";
7+
# Packaged PTY fleet-observation gate: the exact `pty list --json` producer revision with
8+
# ambiguity-safe PID reads, EPERM handling, and one fleet-wide socket fallback budget.
9+
pty.url = "github:compoundingtech/pty/afeb3b6234b7010b7db802fd029766ad17c14219";
10+
pty.inputs.nixpkgs.follows = "nixpkgs";
711
};
812

913
outputs =
1014
{
1115
self,
1216
nixpkgs,
1317
flake-utils,
18+
pty,
1419
}:
1520
flake-utils.lib.eachDefaultSystem (
1621
system:
@@ -118,6 +123,18 @@
118123
};
119124
};
120125

126+
# Narrow sandbox-safe integration gate for the atomic snapshot boundary. The main package
127+
# deliberately omits the broad doctor suite because some doctor cases exercise facilities
128+
# unavailable in the Nix sandbox. A dedicated target containing exactly one test makes the
129+
# gate structurally non-vacuous: a missing target is a cargo error, never a zero-match pass.
130+
st2AtomicPtySnapshot = st2.overrideAttrs (_: {
131+
pname = "st2-atomic-pty-snapshot-check";
132+
cargoTestFlags = [
133+
"--test"
134+
"atomic_pty_snapshot"
135+
];
136+
});
137+
121138
hookSuccessorSource = pkgs.runCommand "st2-hook-successor-source" { } ''
122139
cp -R ${self} $out
123140
chmod -R u+w $out
@@ -150,6 +167,62 @@
150167
# commits on every rebase. The devShell ships rustfmt + clippy for whoever
151168
# wants them.
152169
checks.st2 = st2;
170+
checks.atomic-pty-snapshot = st2AtomicPtySnapshot;
171+
172+
# Real producer-consumer contract: st2 consumes `pty list --json` from the exact pty
173+
# revision that owns fleet observation. Fake CLI fixtures below still cover malformed
174+
# output and a wedged child; this check proves the healthy 0/75/100/500-session path crosses
175+
# both packaged binaries within st2's short outer deadline.
176+
checks.pty-fleet-contract = pkgs.runCommand "st2-pty-fleet-contract-${version}" {
177+
nativeBuildInputs = [
178+
pkgs.coreutils
179+
pkgs.jq
180+
pkgs.nodejs
181+
pty.packages.${system}.default
182+
st2
183+
];
184+
} ''
185+
export HOME=$(mktemp -d)
186+
catalog=$(mktemp -d)
187+
mkdir -p "$catalog/agents/contract/gone"
188+
printf '%s\n' \
189+
'agent "gone" { host "contract"; retired #true; command "true" }' \
190+
> "$catalog/agents/contract/gone/agent.kdl"
191+
192+
# Run the exact packaged producer's deterministic fault seams. These prove EPERM avoids
193+
# socket fallback and hundreds of indefinitely-hung ambiguous probes share one deadline.
194+
test_config=$(mktemp --suffix=.mjs)
195+
printf '%s\n' 'export default { test: {} }' > "$test_config"
196+
node \
197+
${pty.packages.${system}.default}/lib/pty/node_modules/vitest/vitest.mjs \
198+
run tests/list-liveness-budget.test.ts \
199+
--config "$test_config" \
200+
--root ${pty.packages.${system}.default}/lib/pty
201+
202+
for fleet_size in 0 75 100 500; do
203+
root=$(mktemp -d)
204+
i=0
205+
while test "$i" -lt "$fleet_size"; do
206+
session=$(printf 'session-%03d' "$i")
207+
: > "$root/$session.sock"
208+
printf '%s\n' "$$" > "$root/$session.pid"
209+
i=$((i + 1))
210+
done
211+
212+
PTY_ROOT="$root" timeout 2s pty list --json > "pty-$fleet_size.json"
213+
jq -e --argjson size "$fleet_size" \
214+
'length == $size and all(.status == "running")' \
215+
"pty-$fleet_size.json" >/dev/null
216+
217+
PTY_ROOT="$root" timeout 2s \
218+
st2 doctor --catalog "$catalog" --host contract \
219+
> "doctor-$fleet_size.out"
220+
grep -F 'contract.gone retirement complete' \
221+
"doctor-$fleet_size.out" >/dev/null
222+
done
223+
224+
touch $out
225+
'';
153226

154227
# Smoke test that the built binary actually runs and its command tree is
155228
# wired, independent of the in-tree `cargo test`.

tests/atomic_pty_snapshot.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#![cfg(unix)]
2+
3+
//! Hermetic producer-consumer boundary: a truncated `pty list --json` response is never consumed
4+
//! as a valid prefix. This is a dedicated single-test target so CI cannot pass on a vacuous libtest
5+
//! name filter.
6+
7+
use std::fs;
8+
use std::os::unix::fs::PermissionsExt;
9+
use std::path::Path;
10+
use std::process::Command;
11+
12+
fn executable(path: &Path, body: &str) {
13+
fs::write(path, body).unwrap();
14+
fs::set_permissions(path, fs::Permissions::from_mode(0o755)).unwrap();
15+
}
16+
17+
#[test]
18+
fn doctor_rejects_a_partial_pty_snapshot_atomically() {
19+
let tmp = tempfile::tempdir().unwrap();
20+
let catalog = tmp.path().join("catalog");
21+
let declaration = catalog.join("agents/h/gone/agent.kdl");
22+
let bin = tmp.path().join("bin");
23+
fs::create_dir_all(declaration.parent().unwrap()).unwrap();
24+
fs::create_dir_all(&bin).unwrap();
25+
fs::write(
26+
declaration,
27+
"agent \"gone\" { host \"h\"; retired #true; command \"true\" }\n",
28+
)
29+
.unwrap();
30+
executable(
31+
&bin.join("pty"),
32+
"#!/bin/sh\nprintf '[{\"name\":\"h.gone.agent\",\"status\":\"running\"},'\n",
33+
);
34+
35+
let output = Command::new(env!("CARGO_BIN_EXE_st2"))
36+
.arg("doctor")
37+
.arg("--catalog")
38+
.arg(&catalog)
39+
.args(["--host", "h"])
40+
.env("PATH", &bin)
41+
.env("XDG_STATE_HOME", tmp.path().join("state"))
42+
.env("PTY_ROOT", tmp.path().join("pty"))
43+
.output()
44+
.unwrap();
45+
46+
assert!(!output.status.success());
47+
let stdout = String::from_utf8_lossy(&output.stdout);
48+
assert!(stdout.contains("✗ task runtime readable"), "{stdout}");
49+
assert!(stdout.contains("parsing `pty list --json`"), "{stdout}");
50+
assert!(
51+
!stdout.contains("retirement complete"),
52+
"a valid prefix must never be consumed as a partial snapshot:\n{stdout}"
53+
);
54+
}

0 commit comments

Comments
 (0)