Compare commits

..
Author SHA1 Message Date
fredandClaude Opus 5 03eda02c20 fix(installer): warn on a failed credentials/ chmod instead of swallowing it
ci/woodpecker/pr/ci Pipeline was successful
scooby's review flag 1 on #1242. The other three chmods warn; this one was
`|| true`. It is the one directory holding secrets, so a chmod that fails
silently there is the failure most worth a line in the output.

Comment-and-warn only. No behaviour change on the success path.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WYgWocp36goy8hj2ui6ps1
2026-08-15 22:40:47 -05:00
fredandClaude Opus 5 3b4055017e fix(installer): pin umask and set the 0700 modes the fleet boundary requires (#1236)
ci/woodpecker/pr/ci Pipeline was canceled
A greenfield install cannot run `mosaic fleet init --write`. It fails with
`unsafe-permissions` on an unnamed `(directory)` and an unhandled Node throw,
and every mutating `mosaic fleet` command fails the same way. Measured on a
reverted-to-greenfield sandbox VM at CLI 0.0.50-next.2413: `~/.config/mosaic`,
`fleet/` and `credentials/` all land at 0775, and 1735 directories under the
framework root carry `mode & 022`.

Two independent causes, and fixing either one alone leaves it broken.

1. The installer inherited the caller's umask. Debian/Ubuntu ship 002, so every
   `mkdir -p` produced 0775. Fedora/RHEL ship 022 and produced 0755. The
   product therefore worked or did not depending on the operator's login shell,
   with nothing in the install output distinguishing the two. 022 is already
   what this script assumes it produces — `make_durable_snapshot` restores the
   ambient umask specifically so "every later sync copy and new framework dir"
   gets 0644/0755 — so pin it rather than inherit it.

2. Even at a correct 0755, three directories are rejected. The fleet code
   guards its managed paths with two masks in two languages:
   `assertPrivateManagedDirectory` (fleet-reconciler.js, `mode & 0o077`) covers
   MOSAIC_HOME and `fleet/` and runs before the roster lock is taken;
   `assert_private_directory` (tools/fleet/start-agent-session.sh, `mode & 077`)
   covers `fleet/agents` and runs before a pane is spawned. Their laxer
   siblings use `mode & 0o022` and accept 0755. The strict mask wins, so the
   installer states 0700 outright instead of hoping a umask implies it.

The `find -perm /022 -exec chmod go-w` sweep repairs a tree installed before
this change, which the umask alone cannot reach. It strips group/other WRITE
only — never read or execute — and is scoped to directories, so it corrects the
boundary violation without changing who may traverse or read anything. It is
not sufficient for `fleet/agents`: stripping write from 0755 yields 0750 and
`mode & 077` is still non-zero, which is why that path gets its own chmod.

Reported as #1236. The `fleet/agents` half was found by scooby reading
start-agent-session.sh; the umask framing is theirs too — my first report
blamed the distro rather than the umask.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WYgWocp36goy8hj2ui6ps1
2026-08-15 22:29:48 -05:00
mos-dt-0 7a6fb024b4 docs: establish canonical documentation architecture (#1210)
ci/woodpecker/push/publish Pipeline failed
2026-08-13 17:56:13 +00:00
mos-dt-0 f82307c4dc fix(lease): raise capability-probe timeout to 10s on both halves (#869) (#1207)
ci/woodpecker/push/publish Pipeline failed
2026-08-13 17:28:00 +00:00
6 changed files with 380 additions and 17 deletions
+71
View File
@@ -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.
+58
View File
@@ -35,6 +35,18 @@ SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TARGET_DIR="${MOSAIC_HOME:-$HOME/.config/mosaic}" TARGET_DIR="${MOSAIC_HOME:-$HOME/.config/mosaic}"
INSTALL_MODE="${MOSAIC_INSTALL_MODE:-prompt}" INSTALL_MODE="${MOSAIC_INSTALL_MODE:-prompt}"
# Normalize the ambient umask so directory modes are a property of the installer
# and not of whatever shell invoked it (#1236). Debian/Ubuntu ship umask 002, so
# every `mkdir -p` below yielded 0775 — and the fleet env boundary rejects any
# managed directory with `mode & 0o022`, which made `mosaic fleet init --write`
# impossible on a stock install of those distros. Fedora/RHEL ship 022 and did
# not trip it, so the product worked or did not depending on the operator's
# login shell. 022 is what this script already assumes it produces: see the
# umask note in make_durable_snapshot, which restores to the ambient value
# precisely so "every later sync copy and new framework dir" gets 0644/0755.
# Now that value is 022 rather than whatever was inherited.
umask 022
# Deliberately parsed from "$@" (a real, explicit, per-invocation argument) — # Deliberately parsed from "$@" (a real, explicit, per-invocation argument) —
# never an environment variable — so this opt-out can never sit silently # never an environment variable — so this opt-out can never sit silently
# inherited in a shell profile. See #869 Point-1 C2. # inherited in a shell profile. See #869 Point-1 C2.
@@ -696,6 +708,52 @@ sync_framework
mkdir -p "$TARGET_DIR/memory" mkdir -p "$TARGET_DIR/memory"
mkdir -p "$TARGET_DIR/credentials" mkdir -p "$TARGET_DIR/credentials"
# Three directories must be 0700, not merely not-group-writable (#1236).
# The fleet code guards them with two different masks in two different
# languages, and the strict one wins:
#
# assertPrivateManagedDirectory (fleet-reconciler.js, `mode & 0o077`)
# -> MOSAIC_HOME and MOSAIC_HOME/fleet, checked before the roster lock is
# taken, so every mutating `mosaic fleet` command dies at 0755.
# assert_private_directory (tools/fleet/start-agent-session.sh, `mode & 077`)
# -> MOSAIC_HOME/fleet/agents, checked before a pane is ever spawned.
#
# Their laxer siblings (`mode & 0o022`) accept 0755, which is why normalizing
# the umask above is necessary and not sufficient — a correct umask-022 install
# still produces 0755 and still cannot run `mosaic fleet init --write`. Say the
# strict modes outright rather than inferring them from a umask.
#
# Only these. The rest of the tree is content, stays 0755, and is only ever
# reached by the 0o022 checks, which 0755 satisfies.
chmod 700 "$TARGET_DIR" 2>/dev/null || \
warn "Could not set 0700 on $TARGET_DIR — 'mosaic fleet' mutations will fail as unsafe-permissions."
if [[ -d "$TARGET_DIR/fleet" ]]; then
chmod 700 "$TARGET_DIR/fleet" 2>/dev/null || \
warn "Could not set 0700 on $TARGET_DIR/fleet — 'mosaic fleet' mutations will fail as unsafe-permissions."
fi
# fleet/agents does not exist on a first install — the CLI creates it 0700 on
# demand. It is chmod'd here for the UPGRADE case: a tree built under umask 002
# has it at 0775, and the repair sweep below cannot rescue it, because stripping
# group/other write from 0755 leaves 0750 and `mode & 077` is still non-zero.
if [[ -d "$TARGET_DIR/fleet/agents" ]]; then
chmod 700 "$TARGET_DIR/fleet/agents" 2>/dev/null || \
warn "Could not set 0700 on $TARGET_DIR/fleet/agents — agent sessions will fail to start as unsafe-permissions."
fi
# credentials/ holds secrets and was never meant to be group-readable either.
# It is not on the fleet boundary, so a failure here breaks nothing — but it is
# the one directory where a silently-failed chmod leaves secrets group-readable,
# which is precisely the failure worth a line in the output.
chmod 700 "$TARGET_DIR/credentials" 2>/dev/null || \
warn "Could not set 0700 on $TARGET_DIR/credentials — stored secrets may be readable by other users on this host."
# Repair an existing tree. The umask above only governs directories this run
# creates, so a host installed under umask 002 before this fix keeps its 0775
# dirs through every upgrade and stays broken. Strips group/other WRITE only —
# never read or execute — so it can repair the boundary violation without
# changing who can traverse or read anything. Scoped to directories: file modes
# are the manifest's business, not this fix's.
find "$TARGET_DIR" -type d -perm /022 -exec chmod go-w {} + 2>/dev/null || true
# Reconcile contract files from defaults/ into the framework root: framework-owned # Reconcile contract files from defaults/ into the framework root: framework-owned
# files (CONSTITUTION/AGENTS/STANDARDS) are overwritten every upgrade (a divergent # files (CONSTITUTION/AGENTS/STANDARDS) are overwritten every upgrade (a divergent
# copy is backed up once); user-seeded files (TOOLS) are written on first install only. # copy is backed up once); user-seeded files (TOOLS) are written on first install only.
@@ -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:
result = VERSION_GATE.default_probe_activation_capability( # Keep even a deliberate ambient-lookup mutation away from any host
{"PATH": "/nonexistent-bin-dir-for-869-c4-test"} # 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(
{"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")