Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c35a250de | ||
|
|
10a1f82031 | ||
|
|
61a907a12f | ||
|
|
7a6fb024b4 | ||
|
|
f82307c4dc |
@@ -0,0 +1,71 @@
|
|||||||
|
# REPORT A1207
|
||||||
|
|
||||||
|
Date: 2026-08-13
|
||||||
|
Branch: `fix/869-lease-probe-timeout`
|
||||||
|
Starting head: `2373a5ad345fb316ad2460f6390baab1f45ba08f`
|
||||||
|
Base: `216cd72226cd9ee17eea461cfe7cd0e010a22f02`
|
||||||
|
|
||||||
|
## What changed
|
||||||
|
|
||||||
|
- Added Python behavior tests using isolated temporary directories and marker-writing fake `mosaic` executables. They prove that the supplied `PATH` wins over ambient `os.environ["PATH"]`, and that absent or empty supplied `PATH` values do not search ambient paths, platform defaults, or the current directory.
|
||||||
|
- Bound Python override behavior with executable fakes: a valid `MOSAIC_LEASE_VERSION_PROBE_COMMAND` wins over supplied and ambient `PATH`; an invalid override returns `None` without PATH fallback.
|
||||||
|
- Added a Python runner binding test that captures kwargs and requires `timeout=10.0`. Existing timeout, transport-error, and nonzero-exit checks remain fail-closed with `None`.
|
||||||
|
- Added the optional TypeScript dependency-injection seam `CapabilityProbeExecFile`, defaulting to the existing real `execFileSync` implementation. Production callers have no behavior change.
|
||||||
|
- Added TypeScript tests that capture child-process options and require exactly `timeout: 10_000`. Injected timeout, spawn-error, nonzero-exit, unparseable JSON, and malformed-object cases all return `null`.
|
||||||
|
- Removed the ambient no-dependency TypeScript smoke case that could execute a built checkout's real CLI. Default resolver and supervisor behavior retain their isolated tests, while capability transport tests now use an isolated artifact or the injected transport.
|
||||||
|
|
||||||
|
No Python production code changed relative to `2373a5ad`. The only production delta is the optional TypeScript child-process injection seam.
|
||||||
|
|
||||||
|
## Hermeticity incident and correction
|
||||||
|
|
||||||
|
An initial ambient-lookup mutation run exposed that the pre-existing Python "not resolvable" test left ambient process PATH uncontrolled. On this host, that mutation resolved and executed the host `mosaic` capability probe. A post-build intermediate TypeScript run also let the pre-existing no-dependency smoke case execute the checkout's built `dist/cli.js` capability probe. No `claude` process was run. I then isolated the Python test's ambient PATH, removed the TypeScript ambient smoke case, repeated the PATH mutation using only marker-writing temporary fakes, and repeated the final suites without either real probe path.
|
||||||
|
|
||||||
|
## Mutation evidence
|
||||||
|
|
||||||
|
Each mutation was applied independently, its focused suite was run, and the production source was restored before the final run.
|
||||||
|
|
||||||
|
| Mutation | Result | Reddened test name(s) |
|
||||||
|
| ------------------------------------------------------------------------------------------ | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `shutil.which("mosaic", path=environ.get("PATH", ""))` to ambient `shutil.which("mosaic")` | RED, three failures | `ProbeActivationCapabilityTest.test_supplied_path_wins_over_ambient_process_path`; `ProbeActivationCapabilityTest.test_absent_or_empty_supplied_path_never_falls_back_or_executes` for both absent and empty PATH subtests |
|
||||||
|
| Python `PROBE_TIMEOUT_SECONDS: 10.0` to `2.0` | RED, one failure | `ProbeActivationCapabilityTest.test_probe_passes_ten_second_timeout_to_runner` |
|
||||||
|
| TypeScript `LEASE_CAPABILITY_PROBE_TIMEOUT_MS: 10_000` to `2_000` | RED, one failure | `defaultCapabilityProbe > passes the exact ten-second timeout to the injected child-process transport` |
|
||||||
|
|
||||||
|
## Final test run
|
||||||
|
|
||||||
|
Dependencies were installed first with `pnpm install --frozen-lockfile`. Workspace dependencies were then built with `pnpm --filter '@mosaicstack/mosaic...' run build` so package type declarations were available.
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ cd packages/mosaic && python3 src/mutator-gate/version_coupling_unittest.py
|
||||||
|
...................
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
Ran 19 tests in 0.007s
|
||||||
|
|
||||||
|
OK
|
||||||
|
|
||||||
|
$ pnpm exec vitest run src/commands/lease-activation-probe.spec.ts
|
||||||
|
✓ src/commands/lease-activation-probe.spec.ts (20 tests) 80ms
|
||||||
|
Test Files 1 passed (1)
|
||||||
|
Tests 20 passed (20)
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ pnpm exec prettier --check packages/mosaic/src/commands/lease-activation-probe.ts packages/mosaic/src/commands/lease-activation-probe.spec.ts
|
||||||
|
Checking formatting...
|
||||||
|
All matched files use Prettier code style!
|
||||||
|
|
||||||
|
$ pnpm --filter @mosaicstack/mosaic lint
|
||||||
|
> eslint src
|
||||||
|
|
||||||
|
$ pnpm --filter @mosaicstack/mosaic typecheck
|
||||||
|
> tsc --noEmit
|
||||||
|
|
||||||
|
$ python3 -m py_compile packages/mosaic/src/mutator-gate/version_coupling_unittest.py packages/mosaic/framework/tools/lease-broker/activation_version_gate.py
|
||||||
|
|
||||||
|
$ git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
All commands above exited zero.
|
||||||
|
|
||||||
|
## Ambiguities skipped
|
||||||
|
|
||||||
|
None.
|
||||||
@@ -128,6 +128,14 @@ EOF
|
|||||||
sleep 30
|
sleep 30
|
||||||
EOF
|
EOF
|
||||||
chmod 700 "$AGENT_BIN/mosaic"
|
chmod 700 "$AGENT_BIN/mosaic"
|
||||||
|
# The launcher resolves the roster's runtime against PANE_PATH before it
|
||||||
|
# spawns anything (#1241), so the runtime this projection names has to be
|
||||||
|
# present here even though the fake `mosaic` above never execs it.
|
||||||
|
cat > "$AGENT_BIN/pi" <<'EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
sleep 30
|
||||||
|
EOF
|
||||||
|
chmod 700 "$AGENT_BIN/pi"
|
||||||
server_environment_before=$(tmux -L "$TEST_SOCKET" show-environment -g | sort)
|
server_environment_before=$(tmux -L "$TEST_SOCKET" show-environment -g | sort)
|
||||||
server_sessions_before=$(tmux -L "$TEST_SOCKET" list-sessions | sort)
|
server_sessions_before=$(tmux -L "$TEST_SOCKET" list-sessions | sort)
|
||||||
if /usr/bin/env -i HOME="$HOLDER_HOME" PATH=/usr/bin:/bin MOSAIC_HOME="$AGENT_HOME" \
|
if /usr/bin/env -i HOME="$HOLDER_HOME" PATH=/usr/bin:/bin MOSAIC_HOME="$AGENT_HOME" \
|
||||||
|
|||||||
@@ -286,6 +286,36 @@ _build_runtime_bin_prefix() {
|
|||||||
MOSAIC_RUNTIME_BIN_PREFIX=$(_build_runtime_bin_prefix)
|
MOSAIC_RUNTIME_BIN_PREFIX=$(_build_runtime_bin_prefix)
|
||||||
PANE_PATH=${MOSAIC_RUNTIME_BIN_PREFIX:+${MOSAIC_RUNTIME_BIN_PREFIX}:}/usr/local/bin:/usr/bin:/bin
|
PANE_PATH=${MOSAIC_RUNTIME_BIN_PREFIX:+${MOSAIC_RUNTIME_BIN_PREFIX}:}/usr/local/bin:/usr/bin:/bin
|
||||||
|
|
||||||
|
# #1241. The pane runs `mosaic yolo <runtime>` under PANE_PATH with a cleared
|
||||||
|
# environment. A binary missing from *that* path is a pane that dies in under a
|
||||||
|
# second, inside a session nobody is attached to, with its diagnostic scrolled
|
||||||
|
# into a pane tmux then destroys. Resolve both here, before any effect, where
|
||||||
|
# the failure is still attributable to the thing that caused it.
|
||||||
|
#
|
||||||
|
# `mosaic yolo <runtime>` runs checkRuntime(runtime) and the binary it looks for
|
||||||
|
# is named exactly like the runtime, so resolving the runtime name is the same
|
||||||
|
# question the pane will ask a moment later — asked while an operator can still
|
||||||
|
# see the answer.
|
||||||
|
_resolve_in_pane_path() {
|
||||||
|
PATH="$PANE_PATH" command -v -- "$1" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Exit 69 (EX_UNAVAILABLE): the seat cannot be provided. Distinguished from the
|
||||||
|
# 64 (EX_USAGE) rejections above, which mean the projection itself was bad —
|
||||||
|
# here the data is fine and the host is not ready. Callers tell the individual
|
||||||
|
# cases apart by `code=`, the same way fail_env's many codes share exit 64.
|
||||||
|
fail_launch() {
|
||||||
|
local code="$1"
|
||||||
|
shift
|
||||||
|
echo "ERROR: agent launch aborted: code=${code} agent=${AGENT_NAME} $*" >&2
|
||||||
|
exit 69
|
||||||
|
}
|
||||||
|
|
||||||
|
for required_binary in mosaic "$MOSAIC_AGENT_RUNTIME"; do
|
||||||
|
_resolve_in_pane_path "$required_binary" >/dev/null ||
|
||||||
|
fail_launch missing-binary "'${required_binary}' is not on the pane PATH (${PANE_PATH})"
|
||||||
|
done
|
||||||
|
|
||||||
_ensure_claude_workdir_trusted() {
|
_ensure_claude_workdir_trusted() {
|
||||||
local workdir="$1"
|
local workdir="$1"
|
||||||
local resolved
|
local resolved
|
||||||
@@ -384,6 +414,19 @@ if [ -n "$PANE_PID" ]; then
|
|||||||
_start_heartbeat_sidecar "$AGENT_NAME" "$PANE_PID" \
|
_start_heartbeat_sidecar "$AGENT_NAME" "$PANE_PID" \
|
||||||
"$MOSAIC_HEARTBEAT_RUN_DIR" "$MOSAIC_HEARTBEAT_INTERVAL" || \
|
"$MOSAIC_HEARTBEAT_RUN_DIR" "$MOSAIC_HEARTBEAT_INTERVAL" || \
|
||||||
echo "WARNING: heartbeat sidecar could not be started for $AGENT_NAME" >&2
|
echo "WARNING: heartbeat sidecar could not be started for $AGENT_NAME" >&2
|
||||||
|
elif _tmux has-session -t "=${AGENT_NAME}:0.0" 2>/dev/null; then
|
||||||
|
# #1241. Session present, no pane PID after a second of retries. Whatever this
|
||||||
|
# is, it is not a seat an operator can use, so it is not a success either.
|
||||||
|
fail_launch pane-pid-unresolved \
|
||||||
|
"tmux reports the session but no pane PID after 5 attempts"
|
||||||
else
|
else
|
||||||
echo "WARNING: could not resolve pane PID for $AGENT_NAME — heartbeat sidecar not started" >&2
|
# #1241. This branch used to print a WARNING about the heartbeat sidecar and
|
||||||
|
# exit 0. It is not a heartbeat problem: tmux destroys a session when its pane
|
||||||
|
# command exits, so an absent session one second after new-session means the
|
||||||
|
# runtime died on startup. Reporting it as success is what let `fleet start`
|
||||||
|
# return 0 over three dead panes — the launcher knew, and said the wrong thing
|
||||||
|
# at the wrong severity to the wrong layer.
|
||||||
|
fail_launch pane-did-not-survive \
|
||||||
|
"the pane exited immediately and tmux destroyed the session;" \
|
||||||
|
"run 'mosaic yolo ${MOSAIC_AGENT_RUNTIME}' in ${MOSAIC_AGENT_WORKDIR} to see why"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -23,8 +23,26 @@ index=0
|
|||||||
if [ "${args[0]:-}" = -L ]; then index=2; fi
|
if [ "${args[0]:-}" = -L ]; then index=2; fi
|
||||||
case "${args[$index]:-}" in
|
case "${args[$index]:-}" in
|
||||||
has-session)
|
has-session)
|
||||||
|
# The holder always answers. MOSAIC_TEST_HELD_SESSIONS lets a case add
|
||||||
|
# other targets that should answer too — without it there is no way to
|
||||||
|
# model "tmux still reports the session" for a non-holder agent, and the
|
||||||
|
# launcher's pane-pid-unresolved branch is unreachable from this harness.
|
||||||
|
#
|
||||||
|
# A listed target answers only AFTER new-session, because the launcher asks
|
||||||
|
# this question twice about the same name: once before launching, where a
|
||||||
|
# yes means "already running, nothing to do, exit 0", and once after, where
|
||||||
|
# a yes means "the session survived". A shim that answered yes to both
|
||||||
|
# would short-circuit at the first and never reach the branch under test —
|
||||||
|
# it would look like coverage and measure the idempotency path instead.
|
||||||
for argument in "${args[@]}"; do
|
for argument in "${args[@]}"; do
|
||||||
[ "$argument" = '=_holder:0.0' ] && exit 0
|
[ "$argument" = '=_holder:0.0' ] && exit 0
|
||||||
|
case " ${MOSAIC_TEST_HELD_SESSIONS:-} " in
|
||||||
|
*" $argument "*)
|
||||||
|
if tr '\0' '\n' < "${MOSAIC_TEST_TMUX_CALLS:?}" | grep -qxF new-session; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
done
|
done
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
@@ -62,6 +80,30 @@ env -0 > "${MOSAIC_HOME:?}/fleet/pane-environment"
|
|||||||
SHIM
|
SHIM
|
||||||
chmod +x "$FAKE_BIN/mosaic"
|
chmod +x "$FAKE_BIN/mosaic"
|
||||||
|
|
||||||
|
# The runtime the rosters below name. The launcher resolves it against PANE_PATH
|
||||||
|
# before spawning (#1241), so it has to exist somewhere the pane would find it —
|
||||||
|
# not merely on the launcher's own PATH.
|
||||||
|
printf '#!/usr/bin/env bash\nexit 0\n' > "$FAKE_BIN/pi"
|
||||||
|
chmod +x "$FAKE_BIN/pi"
|
||||||
|
|
||||||
|
# PANE_PATH is derived partly from `npm config get prefix`. Left to the real npm
|
||||||
|
# it would splice whatever the host has installed into the path under test, and
|
||||||
|
# the missing-binary cases below would pass or fail by accident of the machine.
|
||||||
|
cat > "$FAKE_BIN/npm" <<'SHIM'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
printf '%s\n' "${MOSAIC_TEST_NPM_PREFIX:-/nonexistent}"
|
||||||
|
SHIM
|
||||||
|
chmod +x "$FAKE_BIN/npm"
|
||||||
|
|
||||||
|
# PANE_PATH always ends in the system path. A host that installs these there can
|
||||||
|
# not measure the missing-binary cases at all, and a green run would mean
|
||||||
|
# nothing — so say so instead of passing.
|
||||||
|
for host_binary in mosaic pi; do
|
||||||
|
if PATH=/usr/local/bin:/usr/bin:/bin command -v "$host_binary" >/dev/null 2>&1; then
|
||||||
|
fail "host provides '$host_binary' in the system path; missing-binary cases are not measurable here"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
write_generated() {
|
write_generated() {
|
||||||
local home="$1"
|
local home="$1"
|
||||||
local agent="$2"
|
local agent="$2"
|
||||||
@@ -81,6 +123,19 @@ MOSAIC_TMUX_SOCKET=mosaic-test
|
|||||||
EOF
|
EOF
|
||||||
chmod 600 "$home/fleet/agents/$agent.env.generated"
|
chmod 600 "$home/fleet/agents/$agent.env.generated"
|
||||||
mkdir -p "$home/work"
|
mkdir -p "$home/work"
|
||||||
|
install_pane_binaries "$home"
|
||||||
|
}
|
||||||
|
|
||||||
|
# `$PANE_HOME/.npm-global/bin` is one of the prefixes the launcher folds into
|
||||||
|
# PANE_PATH, so this is the pane's own view of "installed", distinct from the
|
||||||
|
# launcher's PATH. Tests that need a binary *absent* remove it from here.
|
||||||
|
install_pane_binaries() {
|
||||||
|
local pane_home="$1"
|
||||||
|
mkdir -p "$pane_home/.npm-global/bin"
|
||||||
|
local binary
|
||||||
|
for binary in mosaic pi; do
|
||||||
|
ln -sf "$FAKE_BIN/$binary" "$pane_home/.npm-global/bin/$binary"
|
||||||
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
run_start() {
|
run_start() {
|
||||||
@@ -88,6 +143,7 @@ run_start() {
|
|||||||
local agent="$2"
|
local agent="$2"
|
||||||
HOME="$home" PATH="$FAKE_BIN:$PATH" MOSAIC_TEST_TMUX_CALLS="$TMUX_CALLS" \
|
HOME="$home" PATH="$FAKE_BIN:$PATH" MOSAIC_TEST_TMUX_CALLS="$TMUX_CALLS" \
|
||||||
MOSAIC_TEST_PANE_PID="${MOSAIC_TEST_PANE_PID:-}" \
|
MOSAIC_TEST_PANE_PID="${MOSAIC_TEST_PANE_PID:-}" \
|
||||||
|
MOSAIC_TEST_HELD_SESSIONS="${MOSAIC_TEST_HELD_SESSIONS:-}" \
|
||||||
MOSAIC_TEST_HOME="$home" \
|
MOSAIC_TEST_HOME="$home" \
|
||||||
MOSAIC_TEST_FLEET_OWNER=123e4567-e89b-12d3-a456-426614174000 \
|
MOSAIC_TEST_FLEET_OWNER=123e4567-e89b-12d3-a456-426614174000 \
|
||||||
MOSAIC_HOME="$home" "$START" "$agent"
|
MOSAIC_HOME="$home" "$START" "$agent"
|
||||||
@@ -98,7 +154,10 @@ run_start() {
|
|||||||
HOME_VALID="$ROOT/valid"
|
HOME_VALID="$ROOT/valid"
|
||||||
AGENT_VALID="coder0"
|
AGENT_VALID="coder0"
|
||||||
write_generated "$HOME_VALID" "$AGENT_VALID"
|
write_generated "$HOME_VALID" "$AGENT_VALID"
|
||||||
run_start "$HOME_VALID" "$AGENT_VALID"
|
# A live pane PID is part of what "valid launch" means. Until #1241 this case
|
||||||
|
# ran with none, so the suite's one success path was itself a dead pane the
|
||||||
|
# launcher reported as fine.
|
||||||
|
MOSAIC_TEST_PANE_PID=$$ run_start "$HOME_VALID" "$AGENT_VALID"
|
||||||
valid_args=$(tr '\0' '\n' < "$TMUX_CALLS")
|
valid_args=$(tr '\0' '\n' < "$TMUX_CALLS")
|
||||||
echo "$valid_args" | grep -qF new-session || fail "valid generated projection did not reach tmux"
|
echo "$valid_args" | grep -qF new-session || fail "valid generated projection did not reach tmux"
|
||||||
echo "$valid_args" | grep -qF 'mosaic' || fail "fixed mosaic launcher command missing"
|
echo "$valid_args" | grep -qF 'mosaic' || fail "fixed mosaic launcher command missing"
|
||||||
@@ -245,6 +304,13 @@ PANE_BASH_ENV="$ROOT/pane-boundary.bash-env"
|
|||||||
printf 'MOSAIC_RUNTIME_BIN=%s\n' "$FAKE_BIN" > \
|
printf 'MOSAIC_RUNTIME_BIN=%s\n' "$FAKE_BIN" > \
|
||||||
"$HOME_PANE_BOUNDARY/fleet/agents/coder-pane-boundary.env.local"
|
"$HOME_PANE_BOUNDARY/fleet/agents/coder-pane-boundary.env.local"
|
||||||
chmod 600 "$HOME_PANE_BOUNDARY/fleet/agents/coder-pane-boundary.env.local"
|
chmod 600 "$HOME_PANE_BOUNDARY/fleet/agents/coder-pane-boundary.env.local"
|
||||||
|
# This case does not go through run_start, so its pane binaries come from
|
||||||
|
# MOSAIC_RUNTIME_BIN=$FAKE_BIN in the env.local written above — not from the
|
||||||
|
# symlinks install_pane_binaries planted under the generated home, which this
|
||||||
|
# launcher never consults because HOME here is the trusted parent. That is a
|
||||||
|
# legitimate resolution path, but it means dropping MOSAIC_RUNTIME_BIN from
|
||||||
|
# this case on the belief that the symlinks cover it would break the #1241
|
||||||
|
# binary check rather than exercise it.
|
||||||
LD_PRELOAD='/not/loaded/by-clean-bootstrap.so' \
|
LD_PRELOAD='/not/loaded/by-clean-bootstrap.so' \
|
||||||
BASH_ENV="$PANE_BASH_ENV" \
|
BASH_ENV="$PANE_BASH_ENV" \
|
||||||
MOSAIC_UNTRUSTED_SENTINEL='must-not-reach-pane' \
|
MOSAIC_UNTRUSTED_SENTINEL='must-not-reach-pane' \
|
||||||
@@ -258,6 +324,7 @@ PATH="$PANE_STALE_PATH" \
|
|||||||
"MOSAIC_TEST_HOME=$PANE_TRUSTED_HOME" \
|
"MOSAIC_TEST_HOME=$PANE_TRUSTED_HOME" \
|
||||||
MOSAIC_TEST_FLEET_OWNER=123e4567-e89b-12d3-a456-426614174000 \
|
MOSAIC_TEST_FLEET_OWNER=123e4567-e89b-12d3-a456-426614174000 \
|
||||||
MOSAIC_TEST_EXECUTE_PANE=1 \
|
MOSAIC_TEST_EXECUTE_PANE=1 \
|
||||||
|
"MOSAIC_TEST_PANE_PID=$$" \
|
||||||
"$START" coder-pane-boundary
|
"$START" coder-pane-boundary
|
||||||
pane_args=$(tr '\0' '\n' < "$TMUX_CALLS")
|
pane_args=$(tr '\0' '\n' < "$TMUX_CALLS")
|
||||||
echo "$pane_args" | grep -qxF "HOME=$PANE_TRUSTED_HOME" || \
|
echo "$pane_args" | grep -qxF "HOME=$PANE_TRUSTED_HOME" || \
|
||||||
@@ -392,6 +459,75 @@ echo "$interaction_policy_args" | grep -qF 'new-session' && \
|
|||||||
echo "$output" | grep -qF 'operator interaction service requires runtime pi' || \
|
echo "$output" | grep -qF 'operator interaction service requires runtime pi' || \
|
||||||
fail "interaction pinned-policy check did not follow strict parsing"
|
fail "interaction pinned-policy check did not follow strict parsing"
|
||||||
|
|
||||||
|
# #1241. The pane runs `mosaic yolo <runtime>` against PANE_PATH. A binary
|
||||||
|
# missing from that path is a launch failure, and it has to be named before the
|
||||||
|
# session is created — after it, the diagnostic dies with the pane.
|
||||||
|
assert_missing_pane_binary_rejected() {
|
||||||
|
local binary="$1"
|
||||||
|
local home="$ROOT/missing-$binary"
|
||||||
|
local agent="coder-missing-$binary"
|
||||||
|
write_generated "$home" "$agent"
|
||||||
|
rm -f "$home/.npm-global/bin/$binary"
|
||||||
|
|
||||||
|
: > "$TMUX_CALLS"
|
||||||
|
local output
|
||||||
|
if output=$(MOSAIC_TEST_PANE_PID=$$ run_start "$home" "$agent" 2>&1); then
|
||||||
|
fail "launch succeeded with '$binary' absent from the pane PATH"
|
||||||
|
fi
|
||||||
|
echo "$output" | grep -qF 'code=missing-binary' || fail "missing '$binary' diagnostic missing"
|
||||||
|
echo "$output" | grep -qF "'$binary'" || fail "missing-binary diagnostic did not name $binary"
|
||||||
|
if tr '\0' '\n' < "$TMUX_CALLS" | grep -qF new-session; then
|
||||||
|
fail "launcher created a session it knew would die ($binary absent)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_missing_pane_binary_rejected mosaic
|
||||||
|
assert_missing_pane_binary_rejected pi
|
||||||
|
|
||||||
|
# #1241. tmux destroys a session when its pane command exits, so no pane PID a
|
||||||
|
# second after new-session means the runtime died on startup. This used to be a
|
||||||
|
# WARNING about the heartbeat sidecar followed by exit 0 — three layers above it
|
||||||
|
# then reported a fleet that was not running.
|
||||||
|
: > "$TMUX_CALLS"
|
||||||
|
HOME_DEAD_PANE="$ROOT/dead-pane"
|
||||||
|
write_generated "$HOME_DEAD_PANE" "coder-dead-pane"
|
||||||
|
if output=$(MOSAIC_TEST_PANE_PID='' run_start "$HOME_DEAD_PANE" coder-dead-pane 2>&1); then
|
||||||
|
fail "launcher reported success over a pane that did not survive"
|
||||||
|
fi
|
||||||
|
echo "$output" | grep -qF 'code=pane-did-not-survive' || fail "dead-pane diagnostic missing"
|
||||||
|
if echo "$output" | grep -qiF 'heartbeat'; then
|
||||||
|
fail "dead pane is still being reported as a heartbeat-sidecar problem"
|
||||||
|
fi
|
||||||
|
tr '\0' '\n' < "$TMUX_CALLS" | grep -qF new-session || \
|
||||||
|
fail "dead-pane case did not reach the launch it is measuring"
|
||||||
|
|
||||||
|
# #1241, the other way a pane fails. Above, tmux destroyed the session and
|
||||||
|
# has-session said so. Here the session is still there and no PID comes back
|
||||||
|
# after the retries — a different fault (the pane is alive but unusable, or
|
||||||
|
# tmux is answering inconsistently) that an operator has to be told apart from
|
||||||
|
# a runtime that died on startup.
|
||||||
|
#
|
||||||
|
# This case exists because the branch that handles it shipped with nothing able
|
||||||
|
# to reach it: the shim answered has-session only for the holder, so every
|
||||||
|
# non-holder agent landed in the session-is-gone branch no matter what. A
|
||||||
|
# defensive branch nothing exercises is the same shape as the bug this whole
|
||||||
|
# change is about, one layer down.
|
||||||
|
: > "$TMUX_CALLS"
|
||||||
|
HOME_NO_PID="$ROOT/pane-no-pid"
|
||||||
|
write_generated "$HOME_NO_PID" "coder-no-pid"
|
||||||
|
if output=$(MOSAIC_TEST_PANE_PID='' MOSAIC_TEST_HELD_SESSIONS='=coder-no-pid:0.0' \
|
||||||
|
run_start "$HOME_NO_PID" coder-no-pid 2>&1); then
|
||||||
|
fail "launcher reported success over a session with no resolvable pane PID"
|
||||||
|
fi
|
||||||
|
echo "$output" | grep -qF 'code=pane-pid-unresolved' || \
|
||||||
|
fail "session-present/no-PID was not reported as pane-pid-unresolved: $output"
|
||||||
|
if echo "$output" | grep -qF 'code=pane-did-not-survive'; then
|
||||||
|
fail "a session tmux still reports was diagnosed as a destroyed session"
|
||||||
|
fi
|
||||||
|
if echo "$output" | grep -qiF 'heartbeat'; then
|
||||||
|
fail "an unresolvable pane PID is still being reported as a heartbeat-sidecar problem"
|
||||||
|
fi
|
||||||
|
|
||||||
# Exact stop derives the socket exclusively from the validated generated
|
# Exact stop derives the socket exclusively from the validated generated
|
||||||
# projection and ignores an ambient socket supplied by the caller.
|
# projection and ignores an ambient socket supplied by the caller.
|
||||||
: > "$TMUX_CALLS"
|
: > "$TMUX_CALLS"
|
||||||
|
|||||||
@@ -62,7 +62,14 @@ EXPECTED_ACTIVATION_CAPABILITY: Final[ActivationCapability] = {
|
|||||||
# capability as compact JSON.
|
# capability as compact JSON.
|
||||||
LEASE_CAPABILITY_PROBE_COMMAND: Final = "__lease-capability"
|
LEASE_CAPABILITY_PROBE_COMMAND: Final = "__lease-capability"
|
||||||
|
|
||||||
PROBE_TIMEOUT_SECONDS: Final = 2.0
|
# Budget for the out-of-process `mosaic __lease-capability` probe. The CLI
|
||||||
|
# is a Node program whose cold start alone measures 2.2-2.3s on a mid-range
|
||||||
|
# workstation (sb-it-1-dt, 2026-08-13), so a 2s budget made every launch on
|
||||||
|
# such hosts fail closed with the #869 skew message even though the
|
||||||
|
# capability matched. The timeout only bounds the pathological hang case —
|
||||||
|
# the happy path returns as soon as the probe exits — so a generous budget
|
||||||
|
# costs nothing on healthy hosts.
|
||||||
|
PROBE_TIMEOUT_SECONDS: Final = 10.0
|
||||||
|
|
||||||
# Override hook: a full shell-style command line (parsed with `shlex.split`)
|
# Override hook: a full shell-style command line (parsed with `shlex.split`)
|
||||||
# to run INSTEAD of resolving `mosaic` on PATH and appending the probe
|
# to run INSTEAD of resolving `mosaic` on PATH and appending the probe
|
||||||
@@ -88,7 +95,13 @@ def _resolve_probe_command(environ: Mapping[str, str]) -> list[str] | None:
|
|||||||
if override:
|
if override:
|
||||||
parsed = shlex.split(override)
|
parsed = shlex.split(override)
|
||||||
return parsed or None
|
return parsed or None
|
||||||
resolved = shutil.which("mosaic")
|
# Resolve against the PROVIDED environment's PATH, not the ambient
|
||||||
|
# os.environ. Before this, a test passing a hermetic environ still
|
||||||
|
# resolved (and spawned) the host's real `mosaic` — masked only on hosts
|
||||||
|
# where the real probe happened to exceed the old 2s timeout. No PATH in
|
||||||
|
# the provided environment means nothing is resolvable (fail-closed),
|
||||||
|
# matching the probe's overall contract.
|
||||||
|
resolved = shutil.which("mosaic", path=environ.get("PATH", ""))
|
||||||
if resolved is None:
|
if resolved is None:
|
||||||
return None
|
return None
|
||||||
return [resolved, LEASE_CAPABILITY_PROBE_COMMAND]
|
return [resolved, LEASE_CAPABILITY_PROBE_COMMAND]
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ import { fileURLToPath } from 'node:url';
|
|||||||
import {
|
import {
|
||||||
LEASE_ACTIVATION_CAPABILITY,
|
LEASE_ACTIVATION_CAPABILITY,
|
||||||
LEASE_CAPABILITY_PROBE_COMMAND,
|
LEASE_CAPABILITY_PROBE_COMMAND,
|
||||||
|
LEASE_CAPABILITY_PROBE_TIMEOUT_MS,
|
||||||
defaultCapabilityProbe,
|
defaultCapabilityProbe,
|
||||||
defaultResolveCliEntry,
|
defaultResolveCliEntry,
|
||||||
defaultSupervisorProbe,
|
defaultSupervisorProbe,
|
||||||
leaseEnforcementActivatable,
|
leaseEnforcementActivatable,
|
||||||
registerLeaseCapabilityProbe,
|
registerLeaseCapabilityProbe,
|
||||||
|
type CapabilityProbeExecFile,
|
||||||
type LeaseActivationCapability,
|
type LeaseActivationCapability,
|
||||||
type SupervisorProbeResult,
|
type SupervisorProbeResult,
|
||||||
} from './lease-activation-probe.js';
|
} from './lease-activation-probe.js';
|
||||||
@@ -35,6 +37,17 @@ const presentSupervisor: SupervisorProbeResult = {
|
|||||||
socketPath: '/run/user/1000/mosaic-lease/broker.sock',
|
socketPath: '/run/user/1000/mosaic-lease/broker.sock',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function withScratchCli<T>(run: (cliPath: string) => T): T {
|
||||||
|
const scratchDir = mkdtempSync(join(tmpdir(), 'mosaic-lease-capability-probe-'));
|
||||||
|
try {
|
||||||
|
const cliPath = join(scratchDir, 'cli.js');
|
||||||
|
writeFileSync(cliPath, '// isolated fake; injected execFile means this is never executed\n');
|
||||||
|
return run(cliPath);
|
||||||
|
} finally {
|
||||||
|
rmSync(scratchDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
describe('leaseEnforcementActivatable', () => {
|
describe('leaseEnforcementActivatable', () => {
|
||||||
it('is false when the activation capability is absent (null)', () => {
|
it('is false when the activation capability is absent (null)', () => {
|
||||||
const result = leaseEnforcementActivatable({
|
const result = leaseEnforcementActivatable({
|
||||||
@@ -100,15 +113,6 @@ describe('leaseEnforcementActivatable', () => {
|
|||||||
});
|
});
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses the real default probes when no deps are injected (does not throw)', () => {
|
|
||||||
// No live broker / built CLI is guaranteed in a test environment, so this
|
|
||||||
// only asserts the predicate degrades to a safe boolean rather than
|
|
||||||
// throwing — the fail-closed behavior itself is covered by the injected
|
|
||||||
// cases above.
|
|
||||||
expect(() => leaseEnforcementActivatable()).not.toThrow();
|
|
||||||
expect(typeof leaseEnforcementActivatable()).toBe('boolean');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('defaultCapabilityProbe', () => {
|
describe('defaultCapabilityProbe', () => {
|
||||||
@@ -127,6 +131,61 @@ describe('defaultCapabilityProbe', () => {
|
|||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('passes the exact ten-second timeout to the injected child-process transport', () => {
|
||||||
|
withScratchCli((cliPath) => {
|
||||||
|
let captured:
|
||||||
|
| {
|
||||||
|
file: string;
|
||||||
|
args: string[];
|
||||||
|
options: Parameters<CapabilityProbeExecFile>[2];
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
const execFile: CapabilityProbeExecFile = (file, args, options) => {
|
||||||
|
captured = { file, args, options };
|
||||||
|
return JSON.stringify(LEASE_ACTIVATION_CAPABILITY);
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = defaultCapabilityProbe({ resolveCliEntry: () => cliPath, execFile });
|
||||||
|
|
||||||
|
expect(result).toEqual(LEASE_ACTIVATION_CAPABILITY);
|
||||||
|
expect(captured).toEqual({
|
||||||
|
file: process.execPath,
|
||||||
|
args: [cliPath, LEASE_CAPABILITY_PROBE_COMMAND],
|
||||||
|
options: {
|
||||||
|
encoding: 'utf-8',
|
||||||
|
timeout: 10_000,
|
||||||
|
stdio: ['ignore', 'pipe', 'ignore'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(captured?.options.timeout).toBe(LEASE_CAPABILITY_PROBE_TIMEOUT_MS);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['timeout', Object.assign(new Error('timed out'), { code: 'ETIMEDOUT' })],
|
||||||
|
['spawn error', Object.assign(new Error('spawn failed'), { code: 'ENOENT' })],
|
||||||
|
['nonzero exit', Object.assign(new Error('child exited 1'), { status: 1 })],
|
||||||
|
])('returns null (fail-closed) on child-process %s', (_failure, error) => {
|
||||||
|
withScratchCli((cliPath) => {
|
||||||
|
const execFile: CapabilityProbeExecFile = () => {
|
||||||
|
throw error;
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(defaultCapabilityProbe({ resolveCliEntry: () => cliPath, execFile })).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['unparseable JSON', 'not-json'],
|
||||||
|
['malformed object', JSON.stringify({ name: LEASE_ACTIVATION_CAPABILITY.name })],
|
||||||
|
])('returns null (fail-closed) on %s output', (_failure, output) => {
|
||||||
|
withScratchCli((cliPath) => {
|
||||||
|
const execFile: CapabilityProbeExecFile = () => output;
|
||||||
|
|
||||||
|
expect(defaultCapabilityProbe({ resolveCliEntry: () => cliPath, execFile })).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('positive path — injected resolver, isolated scratch dir (never the real dist/)', () => {
|
describe('positive path — injected resolver, isolated scratch dir (never the real dist/)', () => {
|
||||||
// A prior version of this test staged the stub cli.js at the package's
|
// A prior version of this test staged the stub cli.js at the package's
|
||||||
// REAL resolved dist/ path and relied on afterEach to clean up "only
|
// REAL resolved dist/ path and relied on afterEach to clean up "only
|
||||||
|
|||||||
@@ -55,6 +55,19 @@ export const LEASE_ACTIVATION_CAPABILITY: LeaseActivationCapability = {
|
|||||||
/** Hidden CLI probe subcommand name — wired via {@link registerLeaseCapabilityProbe}. */
|
/** Hidden CLI probe subcommand name — wired via {@link registerLeaseCapabilityProbe}. */
|
||||||
export const LEASE_CAPABILITY_PROBE_COMMAND = '__lease-capability';
|
export const LEASE_CAPABILITY_PROBE_COMMAND = '__lease-capability';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Budget for the out-of-process capability probe. The probe launches a fresh
|
||||||
|
* Node process on the built CLI entrypoint, whose cold start alone measures
|
||||||
|
* 2.2-2.3s on a mid-range workstation (sb-it-1-dt, 2026-08-13) — so the
|
||||||
|
* previous 2s budget made the probe time out and report NO capability on
|
||||||
|
* such hosts, failing every launch with the #869 skew message even though
|
||||||
|
* the capability matched. The timeout only bounds the pathological hang
|
||||||
|
* case; the happy path returns as soon as the probe exits. Mirrors
|
||||||
|
* PROBE_TIMEOUT_SECONDS in the enforcement half
|
||||||
|
* (framework/tools/lease-broker/activation_version_gate.py).
|
||||||
|
*/
|
||||||
|
export const LEASE_CAPABILITY_PROBE_TIMEOUT_MS = 10_000;
|
||||||
|
|
||||||
function capabilityMatches(candidate: LeaseActivationCapability | null): boolean {
|
function capabilityMatches(candidate: LeaseActivationCapability | null): boolean {
|
||||||
return (
|
return (
|
||||||
candidate !== null &&
|
candidate !== null &&
|
||||||
@@ -110,12 +123,28 @@ export function defaultResolveCliEntry(
|
|||||||
return join(dirname(mainEntry), 'cli.js');
|
return join(dirname(mainEntry), 'cli.js');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Narrow injectable seam for the synchronous child process used by the
|
||||||
|
* capability probe. */
|
||||||
|
export type CapabilityProbeExecFile = (
|
||||||
|
file: string,
|
||||||
|
args: string[],
|
||||||
|
options: {
|
||||||
|
encoding: BufferEncoding;
|
||||||
|
timeout: number;
|
||||||
|
stdio: ['ignore', 'pipe', 'ignore'];
|
||||||
|
},
|
||||||
|
) => string;
|
||||||
|
|
||||||
/** Injectable inputs for {@link defaultCapabilityProbe}. */
|
/** Injectable inputs for {@link defaultCapabilityProbe}. */
|
||||||
export interface CapabilityProbeDeps {
|
export interface CapabilityProbeDeps {
|
||||||
/** Resolve the CLI entrypoint (`cli.js`) to probe. Defaults to
|
/** Resolve the CLI entrypoint (`cli.js`) to probe. Defaults to
|
||||||
* {@link defaultResolveCliEntry}. Inject to point at an isolated scratch
|
* {@link defaultResolveCliEntry}. Inject to point at an isolated scratch
|
||||||
* location in tests — never at the real package's `dist/`. */
|
* location in tests — never at the real package's `dist/`. */
|
||||||
resolveCliEntry?: () => string;
|
resolveCliEntry?: () => string;
|
||||||
|
/** Execute the resolved CLI entrypoint. Defaults to the real
|
||||||
|
* `execFileSync`. Inject so transport behavior and options can be tested
|
||||||
|
* without spawning a process. */
|
||||||
|
execFile?: CapabilityProbeExecFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -139,9 +168,10 @@ export function defaultCapabilityProbe(
|
|||||||
const cliEntry = resolveCliEntry();
|
const cliEntry = resolveCliEntry();
|
||||||
if (!existsSync(cliEntry)) return null;
|
if (!existsSync(cliEntry)) return null;
|
||||||
|
|
||||||
const output = execFileSync(process.execPath, [cliEntry, LEASE_CAPABILITY_PROBE_COMMAND], {
|
const execFile: CapabilityProbeExecFile = deps.execFile ?? execFileSync;
|
||||||
|
const output = execFile(process.execPath, [cliEntry, LEASE_CAPABILITY_PROBE_COMMAND], {
|
||||||
encoding: 'utf-8',
|
encoding: 'utf-8',
|
||||||
timeout: 2000,
|
timeout: LEASE_CAPABILITY_PROBE_TIMEOUT_MS,
|
||||||
stdio: ['ignore', 'pipe', 'ignore'],
|
stdio: ['ignore', 'pipe', 'ignore'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -24,11 +24,15 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import io
|
import io
|
||||||
|
import os
|
||||||
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
from contextlib import redirect_stderr
|
from contextlib import redirect_stderr
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
|
||||||
TOOLS_DIR = Path(__file__).parents[2] / "framework/tools/lease-broker"
|
TOOLS_DIR = Path(__file__).parents[2] / "framework/tools/lease-broker"
|
||||||
@@ -57,6 +61,20 @@ def matching_capability() -> dict[str, object]:
|
|||||||
return dict(VERSION_GATE.EXPECTED_ACTIVATION_CAPABILITY)
|
return dict(VERSION_GATE.EXPECTED_ACTIVATION_CAPABILITY)
|
||||||
|
|
||||||
|
|
||||||
|
def write_fake_mosaic(directory: Path, marker: Path) -> Path:
|
||||||
|
directory.mkdir(parents=True, exist_ok=True)
|
||||||
|
executable = directory / "mosaic"
|
||||||
|
executable.write_text(
|
||||||
|
"#!/bin/sh\n"
|
||||||
|
f"printf '%s\\n' executed >> {shlex.quote(str(marker))}\n"
|
||||||
|
"printf '%s\\n' "
|
||||||
|
"'{\"name\":\"lease-runtime-activation\",\"version\":1}'\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
executable.chmod(0o755)
|
||||||
|
return executable
|
||||||
|
|
||||||
|
|
||||||
class AssertActivationCapabilityMatchesTest(unittest.TestCase):
|
class AssertActivationCapabilityMatchesTest(unittest.TestCase):
|
||||||
"""Unit-level coverage of `activation_version_gate.py`'s own assertion,
|
"""Unit-level coverage of `activation_version_gate.py`'s own assertion,
|
||||||
isolated from the launch-runtime.py seam it is wired into below."""
|
isolated from the launch-runtime.py seam it is wired into below."""
|
||||||
@@ -110,11 +128,102 @@ class ProbeActivationCapabilityTest(unittest.TestCase):
|
|||||||
handling — never spawns a real `mosaic` process."""
|
handling — never spawns a real `mosaic` process."""
|
||||||
|
|
||||||
def test_returns_none_when_mosaic_is_not_resolvable_on_path(self) -> None:
|
def test_returns_none_when_mosaic_is_not_resolvable_on_path(self) -> None:
|
||||||
|
# Keep even a deliberate ambient-lookup mutation away from any host
|
||||||
|
# installation. The dedicated hermeticity tests below provide fake
|
||||||
|
# ambient executables and markers.
|
||||||
|
with mock.patch.dict(
|
||||||
|
os.environ, {"PATH": "/nonexistent-ambient-bin-dir-for-869-c4-test"}
|
||||||
|
):
|
||||||
result = VERSION_GATE.default_probe_activation_capability(
|
result = VERSION_GATE.default_probe_activation_capability(
|
||||||
{"PATH": "/nonexistent-bin-dir-for-869-c4-test"}
|
{"PATH": "/nonexistent-bin-dir-for-869-c4-test"}
|
||||||
)
|
)
|
||||||
self.assertIsNone(result)
|
self.assertIsNone(result)
|
||||||
|
|
||||||
|
def test_supplied_path_wins_over_ambient_process_path(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
root = Path(temporary)
|
||||||
|
supplied_marker = root / "supplied.marker"
|
||||||
|
ambient_marker = root / "ambient.marker"
|
||||||
|
supplied_bin = root / "supplied-bin"
|
||||||
|
ambient_bin = root / "ambient-bin"
|
||||||
|
write_fake_mosaic(supplied_bin, supplied_marker)
|
||||||
|
write_fake_mosaic(ambient_bin, ambient_marker)
|
||||||
|
|
||||||
|
with mock.patch.dict(os.environ, {"PATH": str(ambient_bin)}):
|
||||||
|
result = VERSION_GATE.default_probe_activation_capability(
|
||||||
|
{"PATH": str(supplied_bin)}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result, matching_capability())
|
||||||
|
self.assertTrue(supplied_marker.exists())
|
||||||
|
self.assertFalse(ambient_marker.exists())
|
||||||
|
|
||||||
|
def test_absent_or_empty_supplied_path_never_falls_back_or_executes(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
root = Path(temporary)
|
||||||
|
ambient_marker = root / "ambient.marker"
|
||||||
|
current_directory_marker = root / "current-directory.marker"
|
||||||
|
ambient_bin = root / "ambient-bin"
|
||||||
|
current_directory = root / "current-directory"
|
||||||
|
write_fake_mosaic(ambient_bin, ambient_marker)
|
||||||
|
write_fake_mosaic(current_directory, current_directory_marker)
|
||||||
|
original_directory = Path.cwd()
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.chdir(current_directory)
|
||||||
|
with mock.patch.dict(os.environ, {"PATH": str(ambient_bin)}):
|
||||||
|
for supplied_environment in ({}, {"PATH": ""}):
|
||||||
|
with self.subTest(environ=supplied_environment):
|
||||||
|
result = VERSION_GATE.default_probe_activation_capability(
|
||||||
|
supplied_environment
|
||||||
|
)
|
||||||
|
self.assertIsNone(result)
|
||||||
|
self.assertFalse(ambient_marker.exists())
|
||||||
|
self.assertFalse(current_directory_marker.exists())
|
||||||
|
finally:
|
||||||
|
os.chdir(original_directory)
|
||||||
|
|
||||||
|
def test_valid_override_wins_and_invalid_override_does_not_fall_back_to_path(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
root = Path(temporary)
|
||||||
|
supplied_marker = root / "supplied.marker"
|
||||||
|
ambient_marker = root / "ambient.marker"
|
||||||
|
override_marker = root / "override.marker"
|
||||||
|
supplied_bin = root / "supplied-bin"
|
||||||
|
ambient_bin = root / "ambient-bin"
|
||||||
|
override_bin = root / "override-bin"
|
||||||
|
write_fake_mosaic(supplied_bin, supplied_marker)
|
||||||
|
write_fake_mosaic(ambient_bin, ambient_marker)
|
||||||
|
override_executable = write_fake_mosaic(override_bin, override_marker)
|
||||||
|
|
||||||
|
with mock.patch.dict(os.environ, {"PATH": str(ambient_bin)}):
|
||||||
|
result = VERSION_GATE.default_probe_activation_capability(
|
||||||
|
{
|
||||||
|
"PATH": str(supplied_bin),
|
||||||
|
VERSION_GATE.MOSAIC_COMMAND_OVERRIDE_VAR: str(override_executable),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertEqual(result, matching_capability())
|
||||||
|
self.assertTrue(override_marker.exists())
|
||||||
|
self.assertFalse(supplied_marker.exists())
|
||||||
|
self.assertFalse(ambient_marker.exists())
|
||||||
|
|
||||||
|
override_marker.unlink()
|
||||||
|
result = VERSION_GATE.default_probe_activation_capability(
|
||||||
|
{
|
||||||
|
"PATH": str(supplied_bin),
|
||||||
|
VERSION_GATE.MOSAIC_COMMAND_OVERRIDE_VAR: str(
|
||||||
|
root / "invalid-override" / "mosaic"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertIsNone(result)
|
||||||
|
self.assertFalse(override_marker.exists())
|
||||||
|
self.assertFalse(supplied_marker.exists())
|
||||||
|
self.assertFalse(ambient_marker.exists())
|
||||||
|
|
||||||
def test_override_command_is_parsed_and_the_probe_subcommand_is_not_double_appended(
|
def test_override_command_is_parsed_and_the_probe_subcommand_is_not_double_appended(
|
||||||
self,
|
self,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -135,6 +244,29 @@ class ProbeActivationCapabilityTest(unittest.TestCase):
|
|||||||
self.assertEqual(result, {"name": "lease-runtime-activation", "version": 1})
|
self.assertEqual(result, {"name": "lease-runtime-activation", "version": 1})
|
||||||
self.assertEqual(captured, [["/fake/mosaic", "__lease-capability"]])
|
self.assertEqual(captured, [["/fake/mosaic", "__lease-capability"]])
|
||||||
|
|
||||||
|
def test_probe_passes_ten_second_timeout_to_runner(self) -> None:
|
||||||
|
captured_argv: list[str] = []
|
||||||
|
captured_kwargs: dict[str, object] = {}
|
||||||
|
|
||||||
|
class FakeCompleted:
|
||||||
|
returncode = 0
|
||||||
|
stdout = '{"name": "lease-runtime-activation", "version": 1}'
|
||||||
|
|
||||||
|
def fake_run(argv: list[str], **kwargs: object) -> FakeCompleted:
|
||||||
|
captured_argv.extend(argv)
|
||||||
|
captured_kwargs.update(kwargs)
|
||||||
|
return FakeCompleted()
|
||||||
|
|
||||||
|
result = VERSION_GATE.default_probe_activation_capability(
|
||||||
|
{VERSION_GATE.MOSAIC_COMMAND_OVERRIDE_VAR: "/fake/mosaic"},
|
||||||
|
run=fake_run,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result, matching_capability())
|
||||||
|
self.assertEqual(captured_argv, ["/fake/mosaic"])
|
||||||
|
self.assertEqual(captured_kwargs["timeout"], 10.0)
|
||||||
|
self.assertEqual(captured_kwargs["check"], False)
|
||||||
|
|
||||||
def test_fails_closed_on_nonzero_exit_malformed_json_and_missing_fields(self) -> None:
|
def test_fails_closed_on_nonzero_exit_malformed_json_and_missing_fields(self) -> None:
|
||||||
class NonZeroExit:
|
class NonZeroExit:
|
||||||
returncode = 1
|
returncode = 1
|
||||||
@@ -174,7 +306,7 @@ class ProbeActivationCapabilityTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_fails_closed_on_timeout_and_transport_error(self) -> None:
|
def test_fails_closed_on_timeout_and_transport_error(self) -> None:
|
||||||
def timeout_run(*_args: object, **_kwargs: object) -> None:
|
def timeout_run(*_args: object, **_kwargs: object) -> None:
|
||||||
raise subprocess.TimeoutExpired(cmd="mosaic", timeout=2.0)
|
raise subprocess.TimeoutExpired(cmd="mosaic", timeout=10.0)
|
||||||
|
|
||||||
def oserror_run(*_args: object, **_kwargs: object) -> None:
|
def oserror_run(*_args: object, **_kwargs: object) -> None:
|
||||||
raise OSError("no such file or directory")
|
raise OSError("no such file or directory")
|
||||||
|
|||||||
Reference in New Issue
Block a user