Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions scripts/compose-drift-watch.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,23 @@
# happens to carry a top-level compose.yml (this repo's own), which is
# what made it look like a stack directory to a naive glob.
KNOWN_NON_PROJECT_DIRS = {"honeypot-arcane", "apiary"}
# #3129: llm-worker is still a manifest entry with a persistent restart
# policy (not retired), but its single container has been deliberately kept
# down since #3023 -- it was issuing a competing /api/chat against the same
# Ollama instance the #1947 benchmark sweep needs cold, so the sweep owner
# gated it off until the benchmark run releases it. That is an operator
# decision, not the "torn down" shape retired_projects() (and #3040's Gate 1
# below) exist to tell apart from real drift. #3040's own scope asked for
# exactly this mechanism -- "Retired/intentionally-absent services need an
# explicit allowlist with a reason, the same shape isolation-audit.sh
# already uses for its known-gap WARN tier" -- so this is the allowlist that
# issue specified, keyed by project name so it only ever silences this
# specific, documented absence. Do not add a second entry here
# without an equally-documented operator decision: that is exactly the
# blanket "no sibling, don't alarm" rule #3040 removed.
EXPECTED_ABSENT_WHILE = {
"llm-worker": "#3023 sweep-owner-gated for the #1947 benchmark run; stays down until the operator releases it",
}


def fail(msg: str) -> "None":
Expand Down Expand Up @@ -554,6 +571,15 @@ def sweep(stacks_root: Path, entries: list[dict], retired: set[str] | None) -> t
if not missing:
continue

# #3129: an operator-gated deliberate absence, distinct from both
# "retired" (no manifest entry at all) and ordinary drift. Checked
# before the sibling logic below since llm-worker structurally never
# has a sibling either way -- this is not a re-run of #3040's removed
# blanket rule, it only matches the one named project in
# EXPECTED_ABSENT_WHILE.
if name in EXPECTED_ABSENT_WHILE:
continue

# #3040: the old rule suppressed *every* project with no sibling
# currently running, on the theory that "the whole stack isn't
# deployed" is self-evident and doesn't need an alarm. That's true
Expand Down
37 changes: 34 additions & 3 deletions scripts/tests/test_compose_drift_watch_sweep.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,46 @@ def _mkproject(self, name: str) -> None:
(d / "compose.yml").write_text("services: {}\n")

def test_single_service_project_missing_container_alarms(self) -> None:
# #3129: llm-worker itself moved into EXPECTED_ABSENT_WHILE (see
# below), so this general "single-service project alarms" pin now
# uses a different real single-service manifest entry instead.
self._mkproject("auth-events-worker")
configs = {"auth-events-worker": ({"auth-events-worker": "unless-stopped"}, [])}
with mock.patch.object(cdw.subprocess, "run", side_effect=_fake_compose_run(configs)):
findings, unresolved = cdw.sweep(self.stacks_root, entries=[], retired=set())
self.assertEqual(unresolved, [])
self.assertEqual(len(findings), 1)
self.assertEqual(findings[0]["project"], "auth-events-worker")
self.assertEqual(findings[0]["service"], "auth-events-worker")
self.assertEqual(findings[0]["siblings_running"], [])

def test_expected_absent_project_stays_quiet(self) -> None:
# #3129: llm-worker is still in the manifest (not retired) with a
# persistent restart policy, but its one container has been
# deliberately down since #3023. EXPECTED_ABSENT_WHILE must silence
# exactly this named project without reopening #3040's blanket
# "no sibling, don't alarm" rule for anything else.
self._mkproject("llm-worker")
configs = {"llm-worker": ({"llm-worker": "unless-stopped"}, [])}
with mock.patch.object(cdw.subprocess, "run", side_effect=_fake_compose_run(configs)):
findings, unresolved = cdw.sweep(self.stacks_root, entries=[], retired=set())
self.assertEqual(findings, [])
self.assertEqual(unresolved, [])

def test_unlisted_single_service_project_still_alarms_next_to_expected_absent(self) -> None:
# Control for the above: a different single-service project with the
# same zero-container, no-sibling shape as llm-worker must still
# alarm -- EXPECTED_ABSENT_WHILE only matches its one named key.
self._mkproject("llm-worker")
self._mkproject("ml-worker")
configs = {
"llm-worker": ({"llm-worker": "unless-stopped"}, []),
"ml-worker": ({"ml-worker": "unless-stopped"}, []),
}
with mock.patch.object(cdw.subprocess, "run", side_effect=_fake_compose_run(configs)):
findings, _ = cdw.sweep(self.stacks_root, entries=[], retired=set())
self.assertEqual(len(findings), 1)
self.assertEqual(findings[0]["project"], "llm-worker")
self.assertEqual(findings[0]["service"], "llm-worker")
self.assertEqual(findings[0]["siblings_running"], [])
self.assertEqual(findings[0]["project"], "ml-worker")

def test_fully_retired_project_down_stays_quiet(self) -> None:
self._mkproject("wordpot")
Expand Down
Loading