fix(framework): fail closed on drift blind spots (#1194)
ci/woodpecker/pr/ci Pipeline failed

This commit is contained in:
coder3
2026-08-13 02:25:21 -05:00
parent f9e1be5391
commit 8b97d3ee7b
6 changed files with 257 additions and 102 deletions
+13 -5
View File
@@ -8,11 +8,12 @@ The reported queue-guard source defect was already fixed on `main` by `58b971ab`
## Classification ## Classification
The existing `framework-manifest.txt` is authoritative: The existing `framework-manifest.txt` is authoritative. The detector invokes the canonical shared `tools/_lib/manifest.sh classify` implementation over the complete source census and refuses missing, unreadable, malformed, incomplete, or zero-framework ownership output. Policy is therefore read rather than duplicated:
- Every source file under `tools/**` is framework-owned and required in the deployed tools tree. - Current policy classifies source files under `tools/**` as framework-owned and required in the deployed tools tree.
- `tools/_lib/credentials.json` is explicitly operator-owned and excluded from byte comparison. - Current policy explicitly classifies `tools/_lib/credentials.json` operator-owned and excludes it from byte comparison; future policy changes take effect without a detector edit.
- A file present only in the deployed tools tree is operator-owned/unknown by the manifest's fail-safe default. The detector reports it as `INSTALLED_ONLY operator-or-unknown` but does not fail or delete it. - A file present only in the deployed tools tree is operator-owned/unknown by the manifest's fail-safe default. The detector reports it as `INSTALLED_ONLY operator-or-unknown` under `--verbose` but does not fail or delete it.
- Empty/partial source traversal, unreadable directories/files, symlinked census entries, root aliases, and descendant source aliases all return `CANNOT_ASSERT` rather than manufacturing agreement.
This means `NOT_INSTALLED` is not suppressed by filename guesses such as “test” or “README”: if it ships below source `tools/**`, the installer contract says it should be installed. Source-only implementation files outside `tools/**` are outside this detector population by construction. This means `NOT_INSTALLED` is not suppressed by filename guesses such as “test” or “README”: if it ships below source `tools/**`, the installer contract says it should be installed. Source-only implementation files outside `tools/**` are outside this detector population by construction.
@@ -60,4 +61,11 @@ Do not run this while agent seats are active: the stale set includes identity se
## Probe evidence ## Probe evidence
The detector regression constructs a stale installed tool plus a missing shipped tool and observes rc 1 with distinct `STALE` and `NOT_INSTALLED` lines. That case would pass or be invisible before this change because no installed-vs-shipped comparison existed. Controls prove byte-identical tools pass, the credential carve-out is ignored, installed-only paths are classified without deletion/failure, and comparing a tree to itself refuses with `CANNOT_ASSERT`. The detector regression constructs a stale installed tool plus a missing shipped tool and observes rc 1 with distinct `STALE` and `NOT_INSTALLED` lines. That case would pass or be invisible before this change because no installed-vs-shipped comparison existed. Additional review-red controls prove:
- empty and unreadable source censuses return `CANNOT_ASSERT` (they returned clean rc 0 at the first PR head);
- deleting the manifest returns `CANNOT_ASSERT`, while changing manifest ownership changes the verdict through the canonical resolver (the first head never opened the manifest);
- root and descendant symlink/source aliases cannot return clean (the first head returned clean for a source-backed installed subtree);
- a checker hung during doctor is terminated by a bounded watchdog, emits `CANNOT_ASSERT`, and doctor reaches its final warnings line (the first head hung and suppressed the remaining audit).
Controls retain byte-identical success, exact credential carve-out behavior, and installed-only preservation.
@@ -160,11 +160,26 @@ echo "[mosaic-doctor] Mosaic home: $MOSAIC_HOME"
framework_drift_checker="$(cd -- "$(dirname -- "$0")/../quality/scripts" && pwd)/framework-drift-check.py" framework_drift_checker="$(cd -- "$(dirname -- "$0")/../quality/scripts" && pwd)/framework-drift-check.py"
if [[ -f "$framework_drift_checker" ]]; then if [[ -f "$framework_drift_checker" ]]; then
echo "[mosaic-doctor] Checking installed framework-tool drift..." echo "[mosaic-doctor] Checking installed framework-tool drift..."
if python3 "$framework_drift_checker" --installed-root "$MOSAIC_HOME/tools"; then drift_timeout="${MOSAIC_DOCTOR_DRIFT_TIMEOUT_SEC:-15}"
pass "Installed framework tools match shipped source" if ! [[ "$drift_timeout" =~ ^[1-9][0-9]*$ ]]; then
else warn "Invalid MOSAIC_DOCTOR_DRIFT_TIMEOUT_SEC='$drift_timeout' (expected positive integer); using 15s"
drift_timeout=15
fi
if command -v timeout >/dev/null 2>&1; then
set +e
timeout --signal=TERM --kill-after=2 "${drift_timeout}s" \
python3 "$framework_drift_checker" --installed-root "$MOSAIC_HOME/tools"
drift_rc=$? drift_rc=$?
warn "Installed framework-tool drift detected (checker exit $drift_rc; no files changed)" set -e
if [[ "$drift_rc" -eq 0 ]]; then
pass "Installed framework tools match shipped source"
elif [[ "$drift_rc" -eq 124 || "$drift_rc" -eq 137 ]]; then
warn "CANNOT_ASSERT framework drift checker timed out after ${drift_timeout}s; continuing remaining doctor checks"
else
warn "Installed framework-tool drift detected (checker exit $drift_rc; no files changed)"
fi
else
warn "CANNOT_ASSERT timeout utility unavailable; refusing unbounded framework drift check and continuing remaining doctor checks"
fi fi
else else
warn "Framework drift checker is absent from the shipped tools tree" warn "Framework drift checker is absent from the shipped tools tree"
+128 -66
View File
@@ -1,11 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Compare deployed Mosaic framework tools with the shipped framework source. """Fail-closed comparison of deployed Mosaic tools to manifest-owned shipped tools."""
The framework ownership manifest declares tools/** framework-owned. Consequently every
regular file shipped below source tools/ is expected below MOSAIC_HOME/tools/, except
the explicit operator credential carve-out. Files that exist only in the deployed tree
are operator/unknown state and are reported but never treated as framework drift.
"""
from __future__ import annotations from __future__ import annotations
@@ -13,10 +7,10 @@ import argparse
import hashlib import hashlib
import os import os
from pathlib import Path from pathlib import Path
import stat
import subprocess
import sys import sys
OPERATOR_CARVE_OUTS = {"_lib/credentials.json"}
def digest(path: Path) -> str: def digest(path: Path) -> str:
value = hashlib.sha256() value = hashlib.sha256()
@@ -27,76 +21,150 @@ def digest(path: Path) -> str:
def default_source_tools() -> Path: def default_source_tools() -> Path:
# .../tools/quality/scripts/framework-drift-check.py -> .../tools
return Path(__file__).resolve().parents[2] return Path(__file__).resolve().parents[2]
def normalize_source(path: Path) -> Path: def normalize_source(path: Path) -> Path:
candidate = path.resolve() candidate = path.resolve()
if (candidate / "tools").is_dir(): return candidate / "tools" if (candidate / "tools").is_dir() else candidate
candidate = candidate / "tools"
return candidate
def files_below(root: Path) -> dict[str, Path]: def assert_traversable_directory(path: Path) -> None:
return { mode = stat.S_IMODE(path.stat(follow_symlinks=False).st_mode)
path.relative_to(root).as_posix(): path # At least one principal class must have both read and search. This catches
for path in root.rglob("*") # mode-000 even for privileged reviewers for whom os.access() would lie.
if path.is_file() if not any(mode & read and mode & execute for read, execute in ((0o400, 0o100), (0o040, 0o010), (0o004, 0o001))):
} raise PermissionError(f"directory has no readable/searchable mode: {path}")
def census(root: Path, *, reject_symlinks: bool) -> dict[str, Path]:
result: dict[str, Path] = {}
def onerror(error: OSError) -> None:
raise error
for current, directories, filenames in os.walk(root, topdown=True, followlinks=False, onerror=onerror):
current_path = Path(current)
assert_traversable_directory(current_path)
for name in directories:
entry = current_path / name
if entry.is_symlink() and reject_symlinks:
# A source symlink makes the shipped census incomplete. Deployed
# aliases are assessed later only when they occupy a required
# framework path; installed-only aliases remain operator state.
raise OSError(f"symlinked directory is not an independent census entry: {entry}")
for name in filenames:
entry = current_path / name
if entry.is_symlink():
if reject_symlinks:
raise OSError(f"symlinked file is not an independent census entry: {entry}")
result[entry.relative_to(root).as_posix()] = entry
continue
mode = entry.stat(follow_symlinks=False).st_mode
if not stat.S_ISREG(mode):
raise OSError(f"non-regular census entry: {entry}")
if stat.S_IMODE(mode) & 0o444 == 0:
raise PermissionError(f"file has no readable mode: {entry}")
result[entry.relative_to(root).as_posix()] = entry
return result
def classify_with_manifest(source: Path, relatives: list[str]) -> dict[str, str]:
framework = source.parent
manifest = framework / "framework-manifest.txt"
resolver = source / "_lib" / "manifest.sh"
if not manifest.is_file() or not os.access(manifest, os.R_OK):
raise OSError(f"ownership manifest is missing or unreadable: {manifest}")
if not resolver.is_file() or not os.access(resolver, os.R_OK):
raise OSError(f"canonical manifest resolver is missing or unreadable: {resolver}")
payload = "".join(f"tools/{relative}\n" for relative in relatives)
completed = subprocess.run(
["bash", str(resolver), "classify"],
input=payload,
text=True,
capture_output=True,
check=False,
env={**os.environ, "MANIFEST_FILE": str(manifest)},
)
if completed.returncode != 0:
detail = completed.stderr.strip() or f"resolver rc={completed.returncode}"
raise OSError(f"ownership manifest failed canonical resolution: {detail}")
classified: dict[str, str] = {}
for line in completed.stdout.splitlines():
ownership, separator, manifest_path = line.partition("\t")
if not separator or not manifest_path.startswith("tools/") or ownership not in {"framework", "operator"}:
raise OSError(f"invalid canonical ownership output: {line!r}")
relative = manifest_path.removeprefix("tools/")
if relative in classified:
raise OSError(f"duplicate canonical ownership output: {relative}")
classified[relative] = ownership
if set(classified) != set(relatives):
raise OSError("canonical ownership output did not classify the complete source census")
return classified
def has_symlinked_component(root: Path, relative: str) -> bool:
current = root
for component in Path(relative).parts:
current = current / component
if current.is_symlink():
return True
return False
def main() -> int: def main() -> int:
parser = argparse.ArgumentParser(description="Detect deployed Mosaic framework-tool drift") parser = argparse.ArgumentParser(description="Detect deployed Mosaic framework-tool drift")
parser.add_argument( parser.add_argument("--source-root", type=Path, default=Path(os.environ["MOSAIC_FRAMEWORK_SOURCE_ROOT"]) if os.environ.get("MOSAIC_FRAMEWORK_SOURCE_ROOT") else default_source_tools())
"--source-root", parser.add_argument("--installed-root", type=Path, default=Path(os.environ.get("MOSAIC_HOME", Path.home() / ".config/mosaic")) / "tools")
type=Path, parser.add_argument("--verbose", action="store_true")
default=Path(os.environ["MOSAIC_FRAMEWORK_SOURCE_ROOT"])
if os.environ.get("MOSAIC_FRAMEWORK_SOURCE_ROOT")
else default_source_tools(),
help="shipped framework root or tools root (default: this script's shipped tools tree)",
)
parser.add_argument(
"--installed-root",
type=Path,
default=Path(os.environ.get("MOSAIC_HOME", Path.home() / ".config/mosaic")) / "tools",
help="deployed tools root (default: $MOSAIC_HOME/tools)",
)
parser.add_argument("--verbose", action="store_true", help="list in-sync paths too")
args = parser.parse_args() args = parser.parse_args()
source = normalize_source(args.source_root) source = normalize_source(args.source_root)
installed = args.installed_root.resolve() installed = args.installed_root.resolve()
if not source.is_dir(): try:
print(f"[framework-drift] CANNOT_ASSERT source tools missing: {source}", file=sys.stderr) if not source.is_dir():
return 2 raise OSError(f"source tools missing: {source}")
if not installed.is_dir(): if not installed.is_dir():
print(f"[framework-drift] CANNOT_ASSERT installed tools missing: {installed}", file=sys.stderr) raise OSError(f"installed tools missing: {installed}")
return 2 if source.samefile(installed):
if source == installed: raise OSError("source and installed roots identify the same filesystem object")
print( source_files = census(source, reject_symlinks=True)
"[framework-drift] CANNOT_ASSERT source and installed roots are identical; " if not source_files:
"run the checker from the bundled package or pass --source-root", raise OSError("source tools census is empty")
file=sys.stderr, ownership = classify_with_manifest(source, sorted(source_files))
) required = sorted(relative for relative, owner in ownership.items() if owner == "framework")
if not required:
raise OSError("ownership manifest classifies zero shipped tools as framework-owned")
installed_files = census(installed, reject_symlinks=False)
except (OSError, PermissionError) as error:
print(f"[framework-drift] CANNOT_ASSERT {error}", file=sys.stderr)
return 2 return 2
source_files = files_below(source)
installed_files = files_below(installed)
required = sorted(set(source_files) - OPERATOR_CARVE_OUTS)
in_sync: list[str] = [] in_sync: list[str] = []
stale: list[str] = [] stale: list[str] = []
not_installed: list[str] = [] not_installed: list[str] = []
unsafe_alias: list[str] = []
for relative in required: for relative in required:
deployed = installed / relative deployed = installed / relative
if not deployed.is_file(): if not deployed.is_file():
not_installed.append(relative) not_installed.append(relative)
elif digest(source_files[relative]) == digest(deployed): continue
in_sync.append(relative) if has_symlinked_component(installed, relative):
else: unsafe_alias.append(relative)
stale.append(relative) continue
try:
if source_files[relative].samefile(deployed):
unsafe_alias.append(relative)
elif digest(source_files[relative]) == digest(deployed):
in_sync.append(relative)
else:
stale.append(relative)
except OSError as error:
print(f"[framework-drift] CANNOT_ASSERT cannot compare {relative}: {error}", file=sys.stderr)
return 2
installed_only = sorted(set(installed_files) - set(source_files) - OPERATOR_CARVE_OUTS) source_relative = set(source_files)
installed_only = sorted(set(installed_files) - source_relative)
if args.verbose: if args.verbose:
for relative in in_sync: for relative in in_sync:
print(f"[framework-drift] IN_SYNC {relative}") print(f"[framework-drift] IN_SYNC {relative}")
@@ -104,6 +172,8 @@ def main() -> int:
print(f"[framework-drift] STALE {relative}") print(f"[framework-drift] STALE {relative}")
for relative in not_installed: for relative in not_installed:
print(f"[framework-drift] NOT_INSTALLED {relative}") print(f"[framework-drift] NOT_INSTALLED {relative}")
for relative in unsafe_alias:
print(f"[framework-drift] UNSAFE_ALIAS {relative}")
if args.verbose: if args.verbose:
for relative in installed_only: for relative in installed_only:
print(f"[framework-drift] INSTALLED_ONLY operator-or-unknown {relative}") print(f"[framework-drift] INSTALLED_ONLY operator-or-unknown {relative}")
@@ -111,19 +181,11 @@ def main() -> int:
print( print(
"[framework-drift] summary " "[framework-drift] summary "
f"in-sync={len(in_sync)} stale={len(stale)} not-installed={len(not_installed)} " f"in-sync={len(in_sync)} stale={len(stale)} not-installed={len(not_installed)} "
f"installed-only={len(installed_only)}" f"unsafe-alias={len(unsafe_alias)} installed-only={len(installed_only)}"
) )
print( print("[framework-drift] classification canonical framework-manifest ownership; installed-only=operator-or-unknown-preserved")
"[framework-drift] classification tools/**=framework-owned-required; " if stale or not_installed or unsafe_alias:
"tools/_lib/credentials.json=operator-owned-excluded; " print("[framework-drift] FAIL deployed framework tools do not match independent shipped source; schedule a reviewed framework reseed", file=sys.stderr)
"installed-only=operator-or-unknown-preserved"
)
if stale or not_installed:
print(
"[framework-drift] FAIL deployed framework tools do not match shipped source; "
"schedule a reviewed framework reseed",
file=sys.stderr,
)
return 1 return 1
return 0 return 0
+63 -26
View File
@@ -3,73 +3,110 @@ from __future__ import annotations
import os import os
from pathlib import Path from pathlib import Path
import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
import unittest import unittest
CHECKER = Path(__file__).with_name("framework-drift-check.py") CHECKER = Path(__file__).with_name("framework-drift-check.py")
REAL_RESOLVER = CHECKER.parents[2] / "_lib" / "manifest.sh"
class FrameworkDriftCheckTests(unittest.TestCase): class FrameworkDriftCheckTests(unittest.TestCase):
def setUp(self) -> None: def setUp(self) -> None:
self.temp = tempfile.TemporaryDirectory() self.temp = tempfile.TemporaryDirectory()
root = Path(self.temp.name) root = Path(self.temp.name)
self.source = root / "framework" / "tools" self.framework = root / "framework"
self.source = self.framework / "tools"
self.installed = root / "home" / "tools" self.installed = root / "home" / "tools"
for directory in (self.source / "git", self.source / "_lib", self.installed / "git", self.installed / "_lib"): for directory in (self.source / "git", self.source / "_lib", self.installed / "git", self.installed / "_lib"):
directory.mkdir(parents=True, exist_ok=True) directory.mkdir(parents=True, exist_ok=True)
shutil.copy2(REAL_RESOLVER, self.source / "_lib" / "manifest.sh")
(self.source / "git" / "guard.sh").write_text("fixed\n") (self.source / "git" / "guard.sh").write_text("fixed\n")
(self.source / "git" / "new-wrapper.sh").write_text("new\n") (self.source / "git" / "new-wrapper.sh").write_text("new\n")
(self.source / "_lib" / "credentials.json").write_text("source-placeholder\n") (self.source / "_lib" / "credentials.json").write_text("source-placeholder\n")
self.write_manifest()
def tearDown(self) -> None: def tearDown(self) -> None:
self.temp.cleanup() self.temp.cleanup()
def write_manifest(self, operator_extra: str = "") -> None:
(self.framework / "framework-manifest.txt").write_text(
"[framework]\ntools/**\n[operator]\ntools/_lib/credentials.json\n" + operator_extra
)
def run_check(self, *extra: str) -> subprocess.CompletedProcess[str]: def run_check(self, *extra: str) -> subprocess.CompletedProcess[str]:
return subprocess.run( return subprocess.run(
[sys.executable, str(CHECKER), "--source-root", str(self.source.parent), "--installed-root", str(self.installed), *extra], [sys.executable, str(CHECKER), "--source-root", str(self.framework), "--installed-root", str(self.installed), *extra],
text=True, text=True, capture_output=True, check=False,
capture_output=True,
check=False,
env={**os.environ, "PYTHONDONTWRITEBYTECODE": "1"}, env={**os.environ, "PYTHONDONTWRITEBYTECODE": "1"},
) )
def install_matching(self) -> None:
for relative in ("git/guard.sh", "git/new-wrapper.sh", "_lib/manifest.sh"):
shutil.copy2(self.source / relative, self.installed / relative)
(self.installed / "_lib" / "credentials.json").write_text("different-operator-secret\n")
def test_fails_loudly_and_classifies_stale_missing_and_installed_only(self) -> None: def test_fails_loudly_and_classifies_stale_missing_and_installed_only(self) -> None:
(self.installed / "git" / "guard.sh").write_text("broken\n") (self.installed / "git" / "guard.sh").write_text("broken\n")
shutil.copy2(self.source / "_lib" / "manifest.sh", self.installed / "_lib" / "manifest.sh")
(self.installed / "local-helper.sh").write_text("operator\n") (self.installed / "local-helper.sh").write_text("operator\n")
(self.installed / "_lib" / "credentials.json").write_text("secret\n")
result = self.run_check("--verbose") result = self.run_check("--verbose")
self.assertEqual(result.returncode, 1) self.assertEqual(result.returncode, 1)
self.assertIn("STALE git/guard.sh", result.stdout) self.assertIn("STALE git/guard.sh", result.stdout)
self.assertIn("NOT_INSTALLED git/new-wrapper.sh", result.stdout) self.assertIn("NOT_INSTALLED git/new-wrapper.sh", result.stdout)
self.assertIn("INSTALLED_ONLY operator-or-unknown local-helper.sh", result.stdout) self.assertIn("INSTALLED_ONLY operator-or-unknown local-helper.sh", result.stdout)
self.assertNotIn("STALE _lib/credentials.json", result.stdout)
self.assertNotIn("NOT_INSTALLED _lib/credentials.json", result.stdout)
self.assertIn("in-sync=0 stale=1 not-installed=1 installed-only=1", result.stdout)
self.assertIn("FAIL deployed framework tools", result.stderr) self.assertIn("FAIL deployed framework tools", result.stderr)
def test_passes_only_when_every_framework_owned_source_file_matches(self) -> None: def test_passes_only_when_every_manifest_owned_source_file_matches(self) -> None:
(self.installed / "git" / "guard.sh").write_text("fixed\n") self.install_matching()
(self.installed / "git" / "new-wrapper.sh").write_text("new\n")
(self.installed / "_lib" / "credentials.json").write_text("different-operator-secret\n")
result = self.run_check() result = self.run_check()
self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("in-sync=2 stale=0 not-installed=0 installed-only=0", result.stdout) self.assertIn("stale=0 not-installed=0 unsafe-alias=0", result.stdout)
def test_refuses_self_comparison_that_would_make_drift_unobservable(self) -> None: def test_manifest_is_required_and_policy_changes_take_effect(self) -> None:
result = subprocess.run( self.install_matching()
[sys.executable, str(CHECKER), "--source-root", str(self.source), "--installed-root", str(self.source)], (self.installed / "git" / "guard.sh").write_text("operator-divergence\n")
text=True, self.write_manifest("tools/git/guard.sh\n")
capture_output=True, self.assertEqual(self.run_check().returncode, 0)
check=False, (self.framework / "framework-manifest.txt").unlink()
) result = self.run_check()
self.assertEqual(result.returncode, 2) self.assertEqual(result.returncode, 2)
self.assertIn("source and installed roots are identical", result.stderr) self.assertIn("CANNOT_ASSERT ownership manifest is missing", result.stderr)
def test_empty_and_unreadable_source_census_cannot_assert(self) -> None:
empty_framework = Path(self.temp.name) / "empty-framework"
empty_source = empty_framework / "tools"
empty_source.mkdir(parents=True)
shutil.copy2(self.framework / "framework-manifest.txt", empty_framework / "framework-manifest.txt")
# The canonical resolver is supplied outside the empty census solely so
# this probe reaches the explicit minimum-population guard.
result = subprocess.run([sys.executable, str(CHECKER), "--source-root", str(empty_framework), "--installed-root", str(self.installed)], text=True, capture_output=True)
self.assertEqual(result.returncode, 2)
self.assertIn("CANNOT_ASSERT", result.stderr)
blocked = self.source / "blocked"
blocked.mkdir(); (blocked / "hidden.sh").write_text("hidden\n"); blocked.chmod(0)
try:
result = self.run_check()
finally:
blocked.chmod(0o700)
self.assertEqual(result.returncode, 2)
self.assertIn("CANNOT_ASSERT", result.stderr)
self.assertTrue("Permission denied" in result.stderr or "no readable/searchable mode" in result.stderr)
def test_root_and_descendant_aliases_cannot_report_clean(self) -> None:
result = subprocess.run([sys.executable, str(CHECKER), "--source-root", str(self.framework), "--installed-root", str(self.source)], text=True, capture_output=True)
self.assertEqual(result.returncode, 2)
self.assertIn("same filesystem object", result.stderr)
shutil.copy2(self.source / "_lib" / "manifest.sh", self.installed / "_lib" / "manifest.sh")
shutil.rmtree(self.installed / "git")
(self.installed / "git").symlink_to(self.source / "git", target_is_directory=True)
result = self.run_check()
self.assertNotEqual(result.returncode, 0)
self.assertTrue("symlinked directory" in result.stderr or "UNSAFE_ALIAS" in result.stdout)
if __name__ == "__main__": if __name__ == "__main__":
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# Doctor must contain a stalled drift checker and continue its remaining audit.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DOCTOR="$SCRIPT_DIR/../../_scripts/mosaic-doctor"
WORK="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/framework-drift-doctor}"
rm -rf "$WORK"
mkdir -p "$WORK/source/tools/quality/scripts" "$WORK/source/tools/_scripts" "$WORK/home/tools"
cp "$DOCTOR" "$WORK/source/tools/_scripts/mosaic-doctor"
cat > "$WORK/source/tools/quality/scripts/framework-drift-check.py" <<'PY'
import time
time.sleep(30)
PY
start=$(date +%s)
set +e
output=$(MOSAIC_HOME="$WORK/home" MOSAIC_DOCTOR_DRIFT_TIMEOUT_SEC=1 \
bash "$WORK/source/tools/_scripts/mosaic-doctor" --fail-on-warn 2>&1)
rc=$?
set -e
elapsed=$(( $(date +%s) - start ))
[[ "$rc" -ne 0 ]] || { echo "FAIL: checker timeout became doctor success" >&2; exit 1; }
[[ "$elapsed" -lt 10 ]] || { echo "FAIL: checker hang escaped watchdog (${elapsed}s)" >&2; exit 1; }
[[ "$output" == *"CANNOT_ASSERT framework drift checker timed out"* ]] || {
echo "FAIL: missing timeout CANNOT_ASSERT diagnostic" >&2; printf '%s\n' "$output" >&2; exit 1;
}
[[ "$output" == *"[mosaic-doctor] warnings="* ]] || {
echo "FAIL: doctor did not continue after checker timeout" >&2; printf '%s\n' "$output" >&2; exit 1;
}
echo "framework drift doctor watchdog regression passed"
+1 -1
View File
@@ -25,7 +25,7 @@
"lint": "eslint src", "lint": "eslint src",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"test": "vitest run --passWithNoTests && pnpm run test:framework-shell", "test": "vitest run --passWithNoTests && pnpm run test:framework-shell",
"test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/fleet/test-start-agent-session.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh" "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/tools/fleet/test-start-agent-session.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh"
}, },
"dependencies": { "dependencies": {
"@mosaicstack/brain": "workspace:*", "@mosaicstack/brain": "workspace:*",