Skip to content

harden(ci): require hermetic lifecycle smoke subset on pull-request CI#2247

Open
danielmeppiel wants to merge 6 commits into
mainfrom
danielmeppiel-required-lifecycle-smoke
Open

harden(ci): require hermetic lifecycle smoke subset on pull-request CI#2247
danielmeppiel wants to merge 6 commits into
mainfrom
danielmeppiel-required-lifecycle-smoke

Conversation

@danielmeppiel

@danielmeppiel danielmeppiel commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

tests/integration/** -- including the curated Consume/Produce/Govern lifecycle contracts and the direct regression tests for #2226 (ADO lock coordinates) and #2240 (virtual lifecycle convergence) -- currently only runs on merge_group, never at PR time. This PR adds a new required Lifecycle Smoke (Linux) job that runs the smallest stable, fully hermetic subset of that suite (13 tests, ~17-18s bare pytest locally / 14.75s on hosted CI at the marker-refactor head, ~44s measured job wall time at the current head) as a PR-time gate, selected declaratively via a registered lifecycle_smoke pytest marker, and adds a self-guarding topology test so the wiring itself cannot silently drift.

Note

Pure CI/test-topology + docs change. No src/apm_cli/** changes. The mutation pilot (#2241) stays nightly/manual -- not touched here.

Problem (WHY)

Approach (WHAT)

# Fix
1 Add lifecycle-smoke job (Lifecycle Smoke (Linux)) to ci.yml: declarative pytest --strict-markers -m lifecycle_smoke tests/integration, timeout-minutes: 3, no secrets.
2 Add Lifecycle Smoke (Linux) to both branches of merge-gate.yml's EXPECTED_CHECKS ternary.
3 Register lifecycle_smoke in pyproject.toml's pytest markers list; mark the four lifecycle modules at module level, only the AC14 function in test_architecture_authorities.py, and (post-#2249) only the [claude] parametrization of the prune-hook reconciliation test.
4 Extend tests/quality/test_ci_topology.py with a positive guard + 11 mutation/negative tests proving the wiring itself is self-guarding, using semantic contracts (marker registered, command shape, non-empty collection) instead of an exact target-list equality check.
5 Update docs/.../integration-testing.md, CONTRIBUTING.md, and CHANGELOG.md.

Implementation (HOW)

  • .github/workflows/ci.yml -- lifecycle-smoke job, placed after apm-self-check. Run step: uv run --extra dev pytest -p no:cacheprovider -q --strict-markers -m lifecycle_smoke tests/integration. The job's rationale comment now explains the marker mechanism, why these four modules, one function, and (post-fix(prune): reconcile merged hook ownership after package removal #2249) one parametrized test case are marked, and the measured before/after timing. This file required zero edit for the fix(prune): reconcile merged hook ownership after package removal #2249 prune-hook promotion -- confirmed via git diff showing no change to the lifecycle-smoke job section between the marker-refactor head and this head; the marker-driven command auto-picked up the newly-marked test.
  • pyproject.toml -- registers lifecycle_smoke in [tool.pytest.ini_options].markers, the single existing canonical marker registry (no new manifest constant added).
  • Marked modules -- test_install_content_hash_roundtrip.py, test_policy_pinned_constraint_e2e.py, test_virtual_claude_skill_lock_convergence.py (all component-marked in critical_suite.toml), and test_virtual_package_lifecycle_matrix.py (direct fix(virtual-packages): restore hermetic lifecycle convergence #2240 regression) all carry pytest.mark.lifecycle_smoke at module level (added to their existing pytestmark list). Only test_architecture_authorities.py::test_ado_lock_coordinates_have_single_owner (direct fix(ado): preserve lock coordinates for outdated and update #2226 AC14 static guard) carries a function-level @pytest.mark.lifecycle_smoke decorator -- the file's other 32 tests remain unmarked, per the maintainer's explicit "do not mark unrelated tests" instruction. Post-fix(prune): reconcile merged hook ownership after package removal #2249: test_prune_hook_reconciliation_e2e.py::test_prune_removes_merged_hook_entries_and_sidecar carries the mark scoped to only its claude parametrization, via pytest.param("claude", marks=pytest.mark.lifecycle_smoke) inside the existing @pytest.mark.parametrize decorator -- the sibling [cursor] parametrization and the file's other five tests remain unmarked.
  • .github/workflows/merge-gate.yml -- unchanged from the prior round: Lifecycle Smoke (Linux) in EXPECTED_CHECKS for both the pull_request and merge_group branches.
  • tests/quality/test_ci_topology.py -- replaced the exact-target-list assertion and its two target-based mutation tests with semantic contracts: marker registered (read from pyproject.toml's existing markers list via a new _pytest_ini_markers() helper -- not a new manifest), command uses --strict-markers + -m lifecycle_smoke + the bounded tests/integration root (token-level check on the exact invocation), job stays required and hermetic (unchanged assertions), and the marker family collects a non-empty set of tests right now (a real --collect-only subprocess check). Six new mutation tests prove each contract is load-bearing: dropped -m expression, wrong marker string, unbounded root, dropped --strict-markers, unregistered marker name, and an empty marker family (pytest exit 5). The four pre-existing hermeticity/required-check mutation tests (job-level env credential, with:-block credential, github.token alias, dropped from EXPECTED_CHECKS) are retained unchanged -- they remain valid regardless of the selection mechanism.
  • docs/.../integration-testing.md -- Tier 3 section rewritten to describe marker-based selection (the "Location" bullet now names the marker, the "Selection mechanism" bullet explains the exit-5 empty-family behavior); added a CI-selection row to the marker-axes table naming lifecycle_smoke as a fourth, orthogonal axis.
  • CONTRIBUTING.md -- contributor-facing CI-tier summary now names the lifecycle_smoke marker and the updated ~65-70s timing.
  • CHANGELOG.md -- Unreleased entry updated to mention the marker-based selection and the updated timing.

Diagrams

Legend: PR-time critical path today ends at merge-gate's existing four checks; Lifecycle Smoke (dashed = new) joins that gate, while the 4-way integration shard and the rest of ci-integration.yml remain merge-queue-only. (Unchanged from the prior round -- this refactor only changed how the job selects its tests, not its position in the topology.)

flowchart LR
    subgraph PRTime["PR-time required checks (ci.yml)"]
        S1[Build and Test Shard 1]
        S2[Build and Test Shard 2]
        ASC[APM Self-Check]
        NDC[NOTICE Drift Check]
        LS["Lifecycle Smoke<br/>(-m lifecycle_smoke)"]:::new
    end
    subgraph MergeQueue["merge_group only (ci-integration.yml)"]
        BLD[Build Linux]
        SMK[Smoke Test]
        INT[Integration Tests 4-way shard]
        REL[Release Validation]
    end
    PR[Pull request opened] --> S1
    PR --> S2
    PR --> ASC
    PR --> NDC
    PR --> LS
    S1 --> GATE[merge-gate EXPECTED_CHECKS]
    S2 --> GATE
    ASC --> GATE
    NDC --> GATE
    LS --> GATE
    GATE -->|queued| BLD --> SMK --> INT --> REL
    classDef new stroke-dasharray: 5 5;
    class LS new;
Loading

Trade-offs

  • Marker-based selection, superseding the prior explicit-path-list trade-off. A maintainer follow-up asked for a registered @pytest.mark.lifecycle_smoke marker instead of the initial explicit-path-list design, so that an emptied family fails loudly (pytest exit 5) rather than a path list silently shrinking. This is the design this PR now ships.
  • Marker-based collection scans all of tests/integration/ (299 files) to resolve -m lifecycle_smoke, not five explicit paths. Measured overhead: local bare-pytest time went from ~16-19s (explicit paths) to ~18-19.5s (marker, after the noise settled across 8 total repeated runs); the real hosted-CI run measured pytest at 14.75s and the full job at 31s wall time -- both comfortably inside the 60-90s target and the 3-minute hard ceiling.
  • fix(deps): bound best-effort ref lookup retries #2238 deliberately not duplicated here. Its regression coverage is unit-level and already required via the existing shards.
  • 4th and 5th targets stay outside critical_suite.toml; the 6th (prune-hook) does too. All three carry only pytest.mark.integration/pytest.mark.lifecycle_smoke, not a taxonomy behavioral marker, so no manifest edit was needed for any of them -- keeping this PR the sole owner of critical-suite/taxonomy/workflow-promotion surfaces.
  • test_architecture_authorities.py's other 32 tests excluded/unmarked. Four of them shell out to a subprocess pytest run (~30s each); including them would roughly double the job's wall time for no additional regression coverage relevant to this PR.
  • No ci-integration.yml changes. The 4-way integration shard keeps its full scope and no -m filter, so all lifecycle_smoke-marked tests continue to run there unaffected; some overlap with the new PR-time job is expected and fine.
  • No new manifest/target-list constant. _pytest_ini_markers() reads pyproject.toml's pre-existing, already-canonical [tool.pytest.ini_options].markers list -- not a parallel list of file paths.

Benefits

  1. A PR that reverts the fix(ado): preserve lock coordinates for outdated and update #2226 or fix(virtual-packages): restore hermetic lifecycle convergence #2240 fix now fails at PR time (proven below, re-verified against the final marker-based mechanism), not after reaching the merge queue.
  2. Zero new flake/cost surface: hermetic, no network, no credentials, no built binary -- ~31s measured total job wall time on real hosted CI (well inside the 60-90s target).
  3. CI wiring itself is now regression-tested with semantic, not exact-target, contracts: test_ci_topology.py fails if the marker is unregistered, the command's shape drifts (missing --strict-markers, wrong -m expression, unbounded root), the job's timeout/required-check membership drifts, or the marker family becomes empty -- 12 tests (1 positive + 11 mutation) total, up from 8. These assertions are count-agnostic by design, so growing the marker family from 12 to 13 tests (the fix(prune): reconcile merged hook ownership after package removal #2249 prune-hook promotion) required zero change to this file.
  4. The empty-family failure mode is structural, not a maintained assertion: if every lifecycle_smoke mark were ever accidentally stripped, pytest itself exits code 5 ("no tests collected") and the required job goes red -- no bespoke check has to remember to catch that case.

Validation

actionlint on both changed workflows: clean, no output (re-verified at final head).

Canonical lint mirror (re-verified at final head 954b563): ruff check (All checks passed!), ruff format --check (clean), pylint R0801 (10.00/10), lint-auth-signals.sh (clean), lint-architecture-boundaries.sh (all 14 AC guards clean, including AC14).

Timing: before (explicit paths) vs marker refactor vs prune-hook promotion (12 -> 13 tests) -- local + real hosted CI
BEFORE (explicit 5-target list, local, 3 repeats):
  run 1: 12 passed, pytest 16.16s, real 16.67s
  run 2: 12 passed, pytest 18.40s, real 19.01s
  run 3: 12 passed, pytest 17.02s, real 17.31s

MARKER REFACTOR (marker-based -m lifecycle_smoke tests/integration, local, 8 total repeats):
  runs 1-5 (initial):  pytest 17.67s-24.73s, real 18.88s-27.03s (one noisy outlier)
  runs 6-8 (re-check): pytest 17.73s-18.42s, real 19.44s-19.55s (settled/stable)

MARKER REFACTOR, real hosted GitHub Actions CI at head 954b56383d:
  pytest-reported: "12 passed, 10613 deselected in 14.75s"
  Lifecycle Smoke (Linux) job total wall time: 31s

PRUNE-HOOK PROMOTION (13 tests, after merging origin/main + #2249, local, 3 repeats):
  run 1: 13 passed, 17.90s
  run 2: 13 passed, 16.58s
  run 3: 13 passed, 16.52s

PRUNE-HOOK PROMOTION, real hosted GitHub Actions CI at final head 0b7bd060c0:
  Lifecycle Smoke (Linux) job total wall time: 44s

Net effect: adding the 13th test (prune-hook [claude]) did not measurably change local bare-pytest wall time (16.5-17.9s, within the same noise band as the 12-test 17.7-18.4s settled baseline) since the added test's own execution cost is small relative to marker-scan overhead already paid for scanning tests/integration/. The hosted job's total wall time (44s, including checkout/setup-python/setup-uv/uv sync/pytest) remains well inside the 60-90s target and the 3-minute (timeout-minutes: 3) hard ceiling, with no ceiling change needed.

Marker/target parity proof (marker refactor) + additive-promotion proof (prune-hook)

pytest --collect-only -m lifecycle_smoke tests/integration collected the exact same 12 test IDs as the prior explicit 5-path/node-id list at the marker-refactor head -- confirmed via a direct diff of collected test IDs before removing the old invocation.

Post-#2249, the same collect-only command now returns exactly 13 IDs: the prior 12 plus test_prune_hook_reconciliation_e2e.py::test_prune_removes_merged_hook_entries_and_sidecar[claude]. The sibling [cursor] parametrization is confirmed absent from the collected set -- verified with a scoped collect-only run against just that file (1/7 tests collected (6 deselected), the 1 being exactly [claude]).

Empty-family failure proof (the marker's core design requirement, unaffected by the prune-hook promotion)
$ uv run --extra dev pytest -p no:cacheprovider -q --strict-markers \
    -m 'lifecycle_smoke and nonexistent_marker_xyz_never_registered' tests/integration
10625 deselected in 4.20s
$ echo $?
5

Exit code 5 ("no tests collected") is pytest's own structural signal -- if every @pytest.mark.lifecycle_smoke were ever stripped, the required CI job goes red without any bespoke assertion having to detect it. --strict-markers does not validate -m expression tokens (it only rejects unregistered markers used as @pytest.mark.xxx decorators at collection time); a nonexistent name inside a -m filter is legal syntax that simply matches zero tests, which is exactly the mechanism this proof and the corresponding test_lifecycle_smoke_marker_family_empty_fails mutation test rely on.

Full topology + taxonomy + architecture-authorities suite (67 passed, unaffected by the prune-hook promotion since these are semantic/count-agnostic contracts)
67 passed in 203.95s (0:03:23)

(tests/quality/test_ci_topology.py + tests/quality/test_test_taxonomy.py + tests/integration/test_architecture_authorities.py -- none of it is on the required PR-time critical path. No topology assertion needed updating for the prune-hook promotion: the guard's contracts are marker-registration/command-shape/non-empty-collection checks, not an exact test count.)

Failure-injection proof: the gate fails if the load-bearing fix is reverted/inverted (re-run against the final marker-based mechanism, including a new #2249 proof, then fully restored)

#2240 -- reverted src/apm_cli/commands/update.py to its pre-fix (77953cc) content, ran the exact final -m lifecycle_smoke command:

FAILED tests/integration/test_virtual_package_lifecycle_matrix.py::test_virtual_package_lifecycle_matrix[content-drift-pinned-virtual-file]
AssertionError: assert 'Restored dependency cache without changing refs.' in '...[+] All dependencies already at their latest matching refs.\n'
1 failed, 11 passed, 10613 deselected in 17.82s

#2226 -- reverted lockfile.py to pre-fix (796e229) content, ran the exact final -m lifecycle_smoke command scoped to the AC14 file:

FAILED tests/integration/test_architecture_authorities.py::test_ado_lock_coordinates_have_single_owner
AssertionError: assert 'with_derived_provider_coordinates' in '...def to_dependency_ref(self) -> DependencyReference: ...'
1 failed, 36 deselected in 0.54s

#2249 (new) -- reverted prune.py, hook_integrator.py, uninstall/engine.py, and apm_package.py to their pre-#2249 content (git apply -R of the PR's own production-code diff), ran the exact final -m lifecycle_smoke command:

FAILED tests/integration/test_prune_hook_reconciliation_e2e.py::test_prune_removes_merged_hook_entries_and_sidecar[claude]
AssertionError: pkg-a's merged hook entry must be reconciled out of .claude/apm-hooks.json
assert 'pkg-a' not in {'pkg-a'}
1 failed, 12 passed, 10619 deselected in 26.55s

All three reverts restored to HEAD immediately after; git status --short confirmed a clean tree each time, and the full 13-test suite re-passed afterward.

Scenario Evidence

# Scenario (user promise) Principle(s) Test(s) proving it Type
1 A PR that reintroduces the #2226 ADO lock-coordinate bug (persisting provider-specific fields instead of deriving them) fails CI before it can be merged Governed by policy, Secure by default tests/integration/test_architecture_authorities.py::test_ado_lock_coordinates_have_single_owner (@pytest.mark.lifecycle_smoke, regression-trap for #2226) integration
2 A PR that reintroduces the #2240 virtual/manifestless lifecycle-drift bug (dependency cache not repaired without changing refs) fails CI before it can be merged Governed by policy tests/integration/test_virtual_package_lifecycle_matrix.py (module pytestmark includes lifecycle_smoke, regression-trap for #2240) integration
3 Install content-hash roundtrip, policy pinned-constraint enforcement, and virtual-skill lock convergence -- the curated component-marked lifecycle contracts -- are enforced pre-merge, not only once a PR reaches the merge queue Governed by policy, DevX tests/integration/test_install_content_hash_roundtrip.py, tests/integration/test_policy_pinned_constraint_e2e.py, tests/integration/test_virtual_claude_skill_lock_convergence.py (all lifecycle_smoke-marked) integration
4 The Lifecycle Smoke job's marker registration, command shape (--strict-markers/-m lifecycle_smoke/bounded root), timeout, required-check membership, and non-empty collection cannot silently drift without a test noticing -- including job-level env, with:-block, github.token-alias credential injection, and an emptied marker family Governed by policy, Secure by default, OSS / community-driven tests/quality/test_ci_topology.py::test_lifecycle_smoke_is_required_and_hermetic + 11 mutation tests unit
5 A PR that reintroduces the #2249 prune-hook bug (orphaned package removal leaving merged hook entries and ownership sidecars behind) fails CI before it can be merged -- scoped to exactly the claude target, not the cursor sibling Governed by policy, Secure by default tests/integration/test_prune_hook_reconciliation_e2e.py::test_prune_removes_merged_hook_entries_and_sidecar[claude] (pytest.param(..., marks=pytest.mark.lifecycle_smoke), regression-trap for #2249) integration

How to test

  • Open this PR against main and confirm Lifecycle Smoke (Linux) appears as a new required check alongside the existing Windows Compatibility Gate and the rest of the PR-time gates.
  • git log --oneline origin/main..HEAD -> 6 commits ahead of the current main tip, 13 files changed (adds a merge commit for origin/main plus the prune-hook promotion commit: test marker addition, CHANGELOG, and docs).
  • uv run --extra dev pytest -p no:cacheprovider -q --strict-markers -m lifecycle_smoke tests/integration -> 13 passed.
  • uv run --extra dev pytest -p no:cacheprovider -q tests/quality/test_ci_topology.py tests/quality/test_test_taxonomy.py -> 30 passed (unaffected by the prune-hook promotion).
  • Confirm the job actually runs and passes on the PR's own CI run (already verified: 44s, pass, at final head 0b7bd06).

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

Add a new required "Lifecycle Smoke (Linux)" job to ci.yml, wired into
merge-gate.yml's EXPECTED_CHECKS, running the smallest stable hermetic
subset of the real Consume/Produce/Govern lifecycle contracts at PR
time -- a gap since tests/integration/ previously only ran on
merge_group.

Targets (12 tests, ~13s bare pytest, ~60s projected job wall time,
timeout-minutes: 3):
- test_install_content_hash_roundtrip.py (component, critical_suite.toml)
- test_policy_pinned_constraint_e2e.py (component, critical_suite.toml)
- test_virtual_claude_skill_lock_convergence.py (component, critical_suite.toml)
- test_virtual_package_lifecycle_matrix.py (direct #2240 regression)
- test_architecture_authorities.py::test_ado_lock_coordinates_have_single_owner
  (direct #2226 AC14 static guard)

All five are hermetic: IsolatedApmEnvironment denies all AF_INET/
AF_INET6 socket use (incl. loopback), no requires_apm_binary/
requires_github_token/requires_ado_pat markers, no secrets wired into
the new job.

tests/quality/test_ci_topology.py gains a positive guard (job name,
timeout ceiling, exact target tuple derived dynamically from
critical_suite.toml's `component`-marked entries plus the two direct
regression targets, no forbidden credential env, EXPECTED_CHECKS
membership plus four mutation/negative tests proving the guard
actually catches drift (dropped target, swapped AC14 node-id,
injected credential env, removed from EXPECTED_CHECKS).

#2238 is intentionally not duplicated here: its regression coverage is
unit-level and already required via the existing shards.

Updates docs/src/content/docs/contributing/integration-testing.md and
CHANGELOG.md. No src/apm_cli/** changes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 75891045-fa50-40b7-9354-b4649195f724
EOF
)
Copilot AI review requested due to automatic review settings July 16, 2026 10:07
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 75891045-fa50-40b7-9354-b4649195f724

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens PR-time CI by introducing a new Lifecycle Smoke (Linux) job that runs a small, hermetic subset of integration lifecycle tests on pull_request (and merge queue), and adds topology tests to ensure the required-check wiring cannot silently drift.

Changes:

  • Add a new lifecycle-smoke job in ci.yml running 5 explicit pytest targets under a tight timeout.
  • Require Lifecycle Smoke (Linux) in merge-gate.yml’s aggregated EXPECTED_CHECKS set.
  • Extend tests/quality/test_ci_topology.py to validate the job’s targets/timeout/hermeticity and add mutation-style negative tests.
  • Document the new PR-time gate and add a changelog entry.
Show a summary per file
File Description
.github/workflows/ci.yml Adds the Lifecycle Smoke (Linux) PR-time job with explicit pytest targets and a 3-minute timeout.
.github/workflows/merge-gate.yml Adds Lifecycle Smoke to the aggregated required checks in both PR-time and merge-queue contexts.
tests/quality/test_ci_topology.py Adds topology assertions + mutation tests to guard the new job wiring and hermetic contract.
docs/src/content/docs/contributing/integration-testing.md Documents the new Lifecycle Smoke tier as a PR-time required check.
CHANGELOG.md Records the new required PR-time Lifecycle Smoke gate under Unreleased/Changed.

Review details

  • Files reviewed: 5/5 changed files
  • Comments generated: 5
  • Review effort level: Low

Comment thread CHANGELOG.md Outdated
Comment on lines +12 to +20
- A hermetic ~60s `Lifecycle Smoke (Linux)` check is now required at PR time
(`ci.yml`, gated via `merge-gate.yml`), covering the smallest stable subset
of the real Consume/Produce/Govern lifecycle contracts: install
content-hash roundtrip, policy pinned-constraint enforcement, virtual-skill
lock convergence, the virtual/manifestless lifecycle matrix (#2240), and
the ADO lock-coordinate ownership guard (#2226). No built binary, network,
or credentials required; `tests/quality/test_ci_topology.py` guards the
job's targets, timeout, and required-check membership against drift.
(#2247)
Comment on lines +36 to +41
### 3. **Lifecycle Smoke** (PR-time required check)
- **Location**: `tests/integration/test_install_content_hash_roundtrip.py`, `test_policy_pinned_constraint_e2e.py`, `test_virtual_claude_skill_lock_convergence.py`, `test_virtual_package_lifecycle_matrix.py`, and `test_architecture_authorities.py::test_ado_lock_coordinates_have_single_owner`
- **Purpose**: Promote the smallest stable, hermetic slice of the real Consume/Produce/Govern lifecycle contracts onto the PR-time critical path, so a regression in install/lock/audit convergence or ADO lock-coordinate handling fails the PR instead of only surfacing once a change reaches the merge queue
- **Scope**: install content-hash roundtrip, policy pinned-constraint enforcement, virtual-skill lock convergence, the virtual/manifestless lifecycle matrix, and the ADO lock-coordinate ownership guard -- no built binary, no network, no credentials
- **Duration**: ~60s job wall time (hard 3-minute timeout)
- **Trigger**: every pull request and merge queue run (`ci.yml`'s `lifecycle-smoke` job, required via `merge-gate.yml`)
LIFECYCLE_SMOKE_JOB = "lifecycle-smoke"
LIFECYCLE_SMOKE_CHECK = "Lifecycle Smoke (Linux)"
LIFECYCLE_SMOKE_RUN_STEP = "Run required lifecycle smoke subset"
LIFECYCLE_SMOKE_MAX_TIMEOUT_MINUTES = 5
Comment on lines +380 to +382
expected_checks = wait_step["env"]["EXPECTED_CHECKS"]
assert isinstance(expected_checks, str)
assert LIFECYCLE_SMOKE_CHECK in expected_checks
Comment on lines +364 to +374
def _assert_lifecycle_smoke_hermetic(job: WorkflowNode) -> None:
assert "secrets" not in job
for step in _run_steps(job):
env = step.get("env")
if isinstance(env, dict):
for key in FORBIDDEN_CREDENTIAL_ENV:
assert key not in env, f"lifecycle-smoke step must not bind {key}"
run = step.get("run")
if isinstance(run, str):
for key in FORBIDDEN_CREDENTIAL_ENV:
assert key not in run, f"lifecycle-smoke step must not reference {key}"
@danielmeppiel

Copy link
Copy Markdown
Collaborator Author

Docs sync advisory

Verdict: no_change * Pages affected: 0 * LLM calls: 0/15 * Took: 2s

Summary

Every file this PR touches matches an no_impact_paths pattern in .apm/docs-index.yml, and none match user_surface_paths -- the L0 deterministic path gate resolves the verdict with zero LLM calls.

No documentation changes needed for this PR.

L0 path classification (.apm/docs-index.yml#no_impact_paths / #user_surface_paths):

Changed file Matches Pattern
.github/workflows/ci.yml no_impact .github/workflows/**
.github/workflows/merge-gate.yml no_impact .github/workflows/**
tests/quality/test_ci_topology.py no_impact tests/**
docs/src/content/docs/contributing/integration-testing.md no_impact docs/** (already author-patched -- see below)
CHANGELOG.md no_impact *.md (root-level meta doc)

No file in the diff matches any user_surface_paths entry (src/apm_cli/cli.py, src/apm_cli/commands/**, src/apm_cli/core/policy/**, src/apm_cli/install/**, etc.) -- this PR does not touch src/apm_cli/** at all, so there is no user-observable CLI/flag/schema surface for the panel to check against the corpus.

Note: this PR's own diff already includes a doc update (docs/src/content/docs/contributing/integration-testing.md, new "Lifecycle Smoke" subsection) describing the new required CI check for contributors. That page falls under docs/**, which .apm/docs-index.yml intentionally excludes from user_surface_paths scope ("doc-only PRs need narrative review, not docs-sync") -- it was reviewed manually as part of authoring this PR, consistent with the contributing-docs sync rule in .github/instructions/* requiring docs/src/content/docs/contributing/integration-testing.md to stay current with CI topology changes.


How this advisory was produced
  • Classifier verdict: no_change (confidence: high, source: L0 deterministic path gate)
  • Panel composition: none spawned (L0 short-circuit)
  • Tool-verified claims: 0 (0 verified, 0 refuted, 0 inconclusive)
  • CDO redraft rounds: 0/3

This is an advisory comment from the docs-sync skill (source). It does not gate merge. The maintainer ships.

Re-run by removing and re-applying the docs-sync label.

@danielmeppiel

Copy link
Copy Markdown
Collaborator Author

APM Review Panel: ship_with_followups

Moves lifecycle-contract smoke tests to PR-time CI (~60s wall), guarded by a self-verifying topology suite -- ship with one high-signal follow-up on the hermetic guard's job-level env blind spot.

cc @danielmeppiel @sergio-sisternes-epam -- a fresh advisory pass is ready for your review.

This PR delivers a concrete competitive advantage: lifecycle-contract verification that previously only ran in the merge queue now runs on every pull request in ~60 seconds, giving contributors immediate feedback on the five most important target surfaces. The manifest-derived authority pattern (lifecycle contract manifest -> _manifest_component_targets() -> ci.yml job targets -> merge-gate.yml EXPECTED_CHECKS) is well-designed, and the self-guarding topology test suite (5 tests, 1 positive + 4 mutation/negative) is exactly the kind of structural integrity check that makes CI configuration durable rather than aspirational. Every active panelist -- python-architect, supply-chain-security, devx-ux, doc-writer, oss-growth-hacker, test-coverage, and cli-logging -- found this PR net-positive. No blocking-severity findings were raised.

The single highest-signal finding is the hermetic guard's blind spot on job-level env blocks, independently identified by three personas (python-architect, supply-chain-security-expert, test-coverage-expert) and empirically proven by test-coverage-expert via actual execution: injecting GITHUB_APM_PAT into job['env'] does not trigger the guard. Because this gap sits on a secure-by-default surface, and the test-coverage-expert supplied outcome: missing evidence with the secure-by-default principle tag, I weight this at near-blocking tier per the evidence protocol -- the absence of an automated guardrail on a declared security surface is a real defect even though the actual CI job is credential-free today (permissions: contents: read, pull_request trigger, no secrets block). The practical risk is low right now, but the guard's contract says "structurally credential-free" and that contract has a hole. Update: this gap has already been closed in a follow-up commit on this PR -- see the recommendation section below.

The second convergence cluster -- three personas (devx-ux-expert, doc-writer, oss-growth-hacker) independently flagging the missing local-repro command and stale CONTRIBUTING.md -- is a contributor-experience concern worth tracking. With 19 open external-fork PRs, a surprise required check with no documented one-liner to reproduce locally is exactly the kind of friction that costs us contributors. Update: this has also been folded into this PR -- see below.

Aligned with: Portable by manifest (targets derive from the curated lifecycle manifest, not hardcoded literals); Secure by default (net improvement, with the one identified gap closed in-PR); Governed by policy (the topology suite enforces CI-config/manifest sync as policy-as-code); OSS community driven (shifts feedback from merge-queue-only to PR-time for every contributor); Pragmatic like npm (~60s is fast enough to stay inside a contributor's iteration loop).

Growth signal. The oss-growth-hacker's note is on target: the next release narrative should lead with the speed-of-feedback story ("lifecycle-contract feedback in ~60s on your PR, not after the merge queue") rather than the internal hardening angle. With 19 open external-fork PRs, this is a tangible DX improvement worth calling out in release comms.

Panel summary

Persona B R N Takeaway
Python Architect 0 1 2 Clean manifest-derived authority design; one recommended gap in _assert_lifecycle_smoke_hermetic (job-level env not checked) -- now folded.
CLI Logging Expert 0 0 0 No CLI output surface touched; pytest -q produces clear per-test tracebacks on failure. Ship.
DevX UX Expert 0 1 2 New required check is well-justified and hermetic, but docs lacked a copy-paste local reproduction command -- now added.
Supply Chain Security Expert 0 0 2 Net security improvement: pull_request trigger (not pull_request_target), permissions: contents: read, no secrets, no credential env, self-guarding topology tests with mutation proofs; two minor guard-coverage gaps -- both closed.
OSS Growth Hacker 0 1 2 CONTRIBUTING.md's contributor-facing CI summary was stale -- now updated to reframe the gate as faster feedback.
Doc Writer 0 3 2 Doc/changelog additions are factually accurate against ci.yml/merge-gate.yml/test_ci_topology.py -- no drift; recommended a local-repro command, symmetric Verify/Debugging sections, and a topology-guard cross-ref, all now added, plus an ASCII encoding fix.
Test Coverage Expert 0 1 0 5 new topology tests are well-structured and pass; the job-level env blind spot was empirically confirmed by actually running the guard -- now closed with 3 new mutation tests.

B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.

Top 5 follow-ups

  1. [Test Coverage Expert] Close the hermetic guard's job-level env blind spot: extend _assert_lifecycle_smoke_hermetic to scan job['env'] for credential keys, not just step-level env/run strings. -- Three independent personas converged on this gap and test-coverage-expert empirically proved it (injected GITHUB_APM_PAT at job-level env, guard did not raise). This is outcome=missing on a secure-by-default surface. DONE: fixed, plus with:-block and github.token-alias coverage, with 3 new mutation tests (18/18 passing).
  2. [DevX UX Expert] Add a copy-paste local-repro command for the exact lifecycle-smoke 5-target subset to integration-testing.md's "Common invocations" section. -- Three personas independently flagged this; a failing required check with no documented repro is the fastest way to lose a first-time contributor. DONE.
  3. [OSS Growth Hacker] Update CONTRIBUTING.md's contributor-facing CI-tier summary to mention the new Lifecycle Smoke required check. -- CONTRIBUTING.md is the only place an external contributor learns what checks to expect; with 19 open external-fork PRs a surprise check is a friction spike. DONE.
  4. [Supply Chain Security Expert] Extend the hermetic guard to scan action with: blocks and recognize the github.token expression alias alongside the GITHUB_TOKEN env-var substring. -- Low practical risk today, but closes blind spots in a guard that claims structural completeness. DONE (folded into follow-up 1's fix).
  5. [Doc Writer] Add Lifecycle Smoke entries to the "What the Tests Verify" and "Debugging Test Failures" sections in integration-testing.md, matching Tiers 1/2. -- Asymmetry would be noticed the first time this check goes red on a PR. DONE.

Architecture

classDiagram
    direction LR
    class critical_suite_toml {
      <<Manifest>>
      schema_version int
      modules list~path and marker~
    }
    class test_ci_topology {
      <<IOBoundary>>
      +LIFECYCLE_SMOKE_TARGETS tuple
      +LIFECYCLE_SMOKE_DIRECT_TARGETS tuple
      +FORBIDDEN_CREDENTIAL_ENV tuple
      +_manifest_component_targets() tuple
      +_assert_lifecycle_smoke_targets(job)
      +_assert_lifecycle_smoke_hermetic(job)
      +_assert_lifecycle_smoke_required(gate)
    }
    class workflow_contracts {
      <<Pure>>
      +WorkflowNode TypeAlias
      +load_workflow(path) WorkflowNode
      +workflow_job(wf, name) WorkflowNode
      +workflow_step(job, name) WorkflowNode
      +shell_commands(step) list
    }
    class ci_yml {
      <<GHAWorkflow>>
      +lifecycle_smoke job
      +targets 5 pytest paths
      +timeout_minutes 3
    }
    class merge_gate_yml {
      <<GHAWorkflow>>
      +EXPECTED_CHECKS ternary str
      +gate job
    }
    class test_test_taxonomy {
      <<IOBoundary>>
      +test_tm002 single_authority guard
    }
    critical_suite_toml ..> test_ci_topology : _manifest_component_targets reads
    test_ci_topology ..> workflow_contracts : uses load_workflow etc
    test_ci_topology ..> ci_yml : parses and asserts targets
    test_ci_topology ..> merge_gate_yml : parses EXPECTED_CHECKS
    test_test_taxonomy ..> critical_suite_toml : TM002 guards single authority
    note for test_ci_topology "Manifest-derived authority:\n_manifest_component_targets() reads\ncritical_suite.toml dynamically\nto satisfy TM002 no-literal-duplication"
    note for ci_yml "New lifecycle-smoke job:\ntimeout 3min, permissions read-only,\nno secrets, no credential env"
    class test_ci_topology:::touched
    class ci_yml:::touched
    class merge_gate_yml:::touched
    classDef touched fill:#fff3b0,stroke:#d47600
Loading
flowchart TD
    A["PR push to main"] --> B["ci.yml triggers on pull_request"]
    B --> C["lifecycle-smoke job\nubuntu-24.04, timeout 3m"]
    C --> D["[FS] actions/checkout@v4"]
    D --> E["setup-python + setup-uv"]
    E --> F["[EXEC] uv sync --extra dev"]
    F --> G["[EXEC] uv run pytest -q\n5 explicit targets"]
    G --> H{All 5 targets pass?}
    H -- yes --> I["lifecycle-smoke: success"]
    H -- no --> J["lifecycle-smoke: failure"]
    A --> K["merge-gate.yml triggers"]
    K --> L["gate job: Wait for required checks"]
    L --> M["[NET] Poll Checks API\nfor EXPECTED_CHECKS including\nLifecycle Smoke Linux"]
    I --> M
    J --> M
    M --> N{All EXPECTED_CHECKS\ncompleted successfully?}
    N -- yes --> O["gate: success"]
    N -- no --> P["gate: failure"]
    subgraph test_ci_topology_guard["test_ci_topology.py static guard"]
        Q["[I/O] load_workflow ci.yml"] --> R["[I/O] _manifest_component_targets\nreads critical_suite.toml"]
        R --> S["Build LIFECYCLE_SMOKE_TARGETS\n= component_paths + direct_targets"]
        S --> T{_pytest_targets job\n== LIFECYCLE_SMOKE_TARGETS?}
        T -- mismatch --> U["AssertionError: target drift"]
        T -- match --> V{_assert_lifecycle_smoke_hermetic:\nNo secrets key? No credential\nenv/with/run in job or step?}
        V -- found --> W["AssertionError: hermetic violation"]
        V -- clean --> X["[I/O] load merge-gate.yml"]
        X --> Y{LIFECYCLE_SMOKE_CHECK\nin EXPECTED_CHECKS?}
        Y -- missing --> Z["AssertionError: not required"]
        Y -- present --> AA["PASS"]
    end
Loading

Recommendation

Ship this PR. It is a net-positive for security (hermetic CI guard, now with the identified gaps closed), contributor experience (PR-time lifecycle feedback in ~60s, with a documented local-repro command), and CI durability (self-guarding topology tests, now 18/18 passing including the corroborated job-level-env fix). No panelist raised a blocking-severity finding; all active panelists endorsed the change. All 5 curated follow-ups have been folded into this PR at the head commit; nothing outstanding remains for a future issue.


Full per-persona findings

Python Architect

  • [recommended] _assert_lifecycle_smoke_hermetic does not scan job-level env for credential keys at tests/quality/test_ci_topology.py:364
    GitHub Actions propagates job-level env to all steps; the guard only checked step-level env, leaving a bypass path for a future edit binding GITHUB_APM_PAT at job level.
    Suggested: Add a job-level env check before the step loop, plus a mutation test.
    Status: folded -- job-level env check + test_lifecycle_smoke_job_level_credential_env_fails added.
  • [nit] EXPECTED_CHECKS substring check does not distinguish ternary branches at tests/quality/test_ci_topology.py:382
    Pre-existing pattern shared with REQUIRED_SHARD_CHECKS; low risk, not introduced by this PR. Not folded (out of this PR's stated scope -- pre-existing pattern shared across the file).
  • [nit] Design-patterns note: manifest-derived authority + mutation-test proof is the right abstraction level at this scope. No action needed.

CLI Logging Expert

No findings.

DevX UX Expert

  • [recommended] integration-testing.md's "Common invocations" section lacked a copy-paste local-repro command for the exact lifecycle-smoke subset at docs/src/content/docs/contributing/integration-testing.md:131
    A first-time contributor whose PR fails this new required check had no documented one-liner to reproduce locally.
    Status: folded -- exact CI command added under Tier 3.
  • [nit] Doc's file-list dropped the tests/integration/ prefix after the first entry at docs/src/content/docs/contributing/integration-testing.md:37
    Status: folded -- all five targets now explicitly qualified.
  • [nit] "Debugging Test Failures" section had no Lifecycle Smoke subsection at docs/src/content/docs/contributing/integration-testing.md:357
    Status: folded -- "Lifecycle Smoke Failures" subsection added.

Supply Chain Security Expert

  • [nit] Hermetic guard's credential scan didn't cover action with: blocks at tests/quality/test_ci_topology.py:366
    Low risk today given permissions: contents: read + pull_request (not pull_request_target) + no secrets block, but the guard itself had a blind spot.
    Status: folded -- with: block scan + mutation test added.
  • [nit] FORBIDDEN_CREDENTIAL_ENV didn't catch the github.token expression alias at tests/quality/test_ci_topology.py:85
    Status: folded -- FORBIDDEN_CREDENTIAL_EXPRESSIONS added + mutation test.

OSS Growth Hacker

  • [recommended] CONTRIBUTING.md's contributor-facing CI-tier summary omitted the new Lifecycle Smoke required check at CONTRIBUTING.md:152
    The only place a first-time external contributor learns what checks to expect; with 19 open external-fork PRs, a surprise required check risks a friction spike.
    Status: folded -- new bullet added reframing the gate as PR-time feedback.
  • [nit] CHANGELOG wording led with maintainer jargon rather than the repostable DX-win framing.
    Status: not further reworded in-PR (already tightened once via apm-strategy review); flagged for the next release narrative per the growth note.
  • [nit] PR title "harden(ci)" undersells the contributor-experience angle for release comms. Noted for whoever drafts release notes; no PR action needed.

Auth Expert -- inactive

PR touches only CI workflow YAML (.github/workflows/ci.yml, merge-gate.yml), CHANGELOG.md, docs (integration-testing.md), and a new test-topology guard (test_ci_topology.py). No auth-relevant files changed -- no modifications to auth.py, token_manager.py, credential resolution, host classification, HTTP authorization headers, or remote-host fallback semantics.

Doc Writer

  • [recommended] No copy-paste local-repro command for the exact Lifecycle Smoke subset at docs/src/content/docs/contributing/integration-testing.md:41
    Status: folded (same fix as DevX UX Expert's finding).
  • [recommended] New Tier 3 had no matching entry in "What the Tests Verify" or "Debugging Test Failures" at docs/src/content/docs/contributing/integration-testing.md:311
    Status: folded -- both sections extended.
  • [recommended] Doc didn't cross-reference tests/quality/test_ci_topology.py, the drift guard the CHANGELOG credits, at docs/src/content/docs/contributing/integration-testing.md:41
    Status: folded -- cross-reference added under Tier 3.
  • [nit] CHANGELOG entry used a real em-dash instead of the file's ASCII -- convention at CHANGELOG.md:14
    Status: folded -- fixed to ASCII.
  • [nit] Doc's file list dropped the tests/integration/ prefix after the first entry.
    Status: folded (same fix as DevX UX Expert's nit).

Test Coverage Expert

  • [recommended] Hermetic guard's job-level env blind spot, empirically confirmed by execution at tests/quality/test_ci_topology.py:364
    Proof (missing): tests/quality/test_ci_topology.py::test_lifecycle_smoke_job_level_credential_env_fails -- proves: lifecycle-smoke job remains credential-free even when credentials are added at job-level env, not just step-level env [secure-by-default]
    Status: folded -- guard fixed, mutation test added, full suite re-run at 18/18 passing.

Performance Expert -- inactive

No runtime hot-path files touched (none of src/apm_cli/cache/**, deps/**, install/phases/**, install/pipeline.py, install/resolve.py, utils/**, marketplace/**, compilation/**); all 5 changed files are CI infrastructure and docs, not application performance surfaces.

This panel is advisory. It does not block merge. Re-apply the
panel-review label after addressing feedback to re-run.

danielmeppiel and others added 2 commits July 16, 2026 12:38
apm-strategy (apm-ceo review): tighten the CHANGELOG entry to a single
contributor-facing line per changelog.instructions.md's one-line-per-PR
convention, and fix a stray non-ASCII em-dash.

apm-review-panel (9-persona fan-out, CEO synthesis: ship_with_followups,
no blocking findings): fold all 5 curated follow-ups.

1. [test-coverage-expert / python-architect / supply-chain-security-expert,
   corroborated + empirically proven] Close the hermetic guard's blind
   spots in tests/quality/test_ci_topology.py:
   - job-level env: was never checked (GitHub Actions inherits it to
     every step); add the check + test_lifecycle_smoke_job_level_credential_env_fails.
   - step with: blocks could smuggle a credential expression past the
     env/run-only scan; add the check + test_lifecycle_smoke_with_block_credential_fails.
   - the github.token expression alias resolves to the same automatic
     token but doesn't match any FORBIDDEN_CREDENTIAL_ENV substring; add
     FORBIDDEN_CREDENTIAL_EXPRESSIONS + test_lifecycle_smoke_github_token_expression_alias_fails.
   Full suite: 18/18 passing (was 15/15).

2. [devx-ux-expert / doc-writer] Add the exact copy-paste local-repro
   command for the 5-target lifecycle-smoke subset to
   integration-testing.md, so a contributor whose PR fails the new
   required check has a documented one-liner.

3. [oss-growth-hacker] Update CONTRIBUTING.md's contributor-facing CI-tier
   summary (the only place a first-time external contributor learns what
   checks to expect) to mention the new Lifecycle Smoke check, framed as
   PR-time feedback rather than an unexplained new gate.

4. [doc-writer] Add symmetric "Lifecycle Smoke Verifies" / "Lifecycle
   Smoke Failures" entries to integration-testing.md (Tiers 1/2 already
   had both), plus a cross-reference to test_ci_topology.py as the
   drift guard, and fix the file-list's inconsistent tests/integration/
   prefix.

All 9 panelists returned no blocking-severity findings; CEO stance is
ship_with_followups, all curated follow-ups folded in this commit.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 75891045-fa50-40b7-9354-b4649195f724
…arker

Supersedes explicit file/node-id targeting with a declarative
@pytest.mark.lifecycle_smoke marker so an emptied family fails loudly
(pytest exit code 5, 'no tests collected') instead of a path list
silently shrinking.

- Register lifecycle_smoke in pyproject.toml's pytest markers list.
- Mark the four lifecycle modules at module level
  (test_install_content_hash_roundtrip.py,
  test_policy_pinned_constraint_e2e.py,
  test_virtual_claude_skill_lock_convergence.py,
  test_virtual_package_lifecycle_matrix.py) and, at function level,
  only test_architecture_authorities.py::test_ado_lock_coordinates_have_single_owner
  (the file's other 32 tests remain unmarked).
- ci.yml's lifecycle-smoke job now runs
  `pytest --strict-markers -m lifecycle_smoke tests/integration`;
  rewrote the job's rationale comment to match.
- tests/quality/test_ci_topology.py: replaced the exact-target-list
  assertion and its two target-based mutation tests with semantic
  contracts -- marker registered (read from pyproject.toml's existing
  markers list, no new manifest constant), command uses
  --strict-markers/-m lifecycle_smoke/bounded tests/integration root,
  job stays required and hermetic, and the marker family collects
  non-empty right now -- backed by six mutation tests proving each
  contract is load-bearing (dropped -m expression, wrong marker
  string, unbounded root, dropped --strict-markers, unregistered
  marker, empty family).
- Docs: integration-testing.md's Tier 3 section and marker-axis table,
  CONTRIBUTING.md, and CHANGELOG.md updated to describe marker-based
  selection and the measured ~65-70s job wall time (was ~60s; the
  full tests/integration/ directory scan for marker deselection adds
  ~2-8s of collection overhead vs the prior five explicit targets).

Verified: marker-based collection selects the identical 12 tests as
the prior explicit-target list (parity confirmed via --collect-only
diff). Re-ran both failure-injection proofs (pre-#2226 lockfile.py,
pre-#2240 update.py) against the new marker-based command -- both
still fail exactly as before. Confirmed marked tests are unaffected
in ci-integration.yml's merge_group integration job (no -m filter
there). Full lint mirror (ruff check/format, pylint R0801,
lint-auth-signals.sh, lint-architecture-boundaries.sh, actionlint)
clean. tests/quality/test_ci_topology.py + test_test_taxonomy.py +
test_architecture_authorities.py: 67 passed.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 75891045-fa50-40b7-9354-b4649195f724
@danielmeppiel

Copy link
Copy Markdown
Collaborator Author

Amendment: marker-based selection supersedes a stated trade-off

A maintainer follow-up asked to refactor this PR's test-selection mechanism from the explicit file/node-id target list to a registered @pytest.mark.lifecycle_smoke pytest marker (pytest --strict-markers -m lifecycle_smoke tests/integration), so an emptied test family fails loudly (pytest exit code 5, "no tests collected") instead of a path list silently shrinking.

This directly supersedes the Trade-offs bullet above: "Explicit test-path targets, not a new marker. Reuses the test-architecture job's proven explicit-list style instead of adding a new pytest marker/selection framework." That trade-off no longer reflects the shipped design.

What changed (commit 954b56383d, current head):

  • lifecycle_smoke registered in pyproject.toml's existing [tool.pytest.ini_options].markers list -- no new manifest/constant.
  • The four lifecycle modules marked at module level; only the AC14 function in test_architecture_authorities.py marked at function level (its other 32 tests remain unmarked).
  • ci.yml's job now runs the marker-based command above.
  • tests/quality/test_ci_topology.py replaced its exact-target-list assertion with semantic contracts (marker registered, command shape, non-empty collection) backed by 11 mutation tests (was 7).

Verified this refactor is behavior-preserving and still catches the regressions it was built for:

  • Marker-based collection selects the identical 12 tests as the prior explicit list (parity confirmed via --collect-only diff).
  • Re-ran both failure-injection proofs (reverted fix(ado): preserve lock coordinates for outdated and update #2226's lockfile.py, reverted fix(virtual-packages): restore hermetic lifecycle convergence #2240's update.py) against the new marker-based command -- both still fail exactly as before, then fully restored.
  • Empty-family behavior confirmed: an impossible compound -m expression exits code 5.
  • Real hosted CI at this head: Lifecycle Smoke (Linux) passed in 31s total (pytest-reported 14.75s), still comfortably inside the 60-90s target and the 3-minute hard ceiling.

No other findings from the original panel pass are affected. This is a plain amendment comment, not a fresh panel run -- the panel's single-comment-per-run discipline stays intact.

danielmeppiel and others added 2 commits July 16, 2026 13:47
…d-lifecycle-smoke

# Conflicts:
#	.github/workflows/merge-gate.yml
Adds pytest.mark.lifecycle_smoke to only the `claude` parametrization
of test_prune_removes_merged_hook_entries_and_sidecar (deliberately
excluding the `cursor` sibling), now that #2249's prune-hook fix has
merged. The existing marker-driven `-m lifecycle_smoke tests/integration`
command in ci.yml auto-picks up the newly-marked test with zero workflow
edit -- confirmed via `git diff` showing no change to the lifecycle-smoke
job section.

Family grows from 12 to 13 tests, all passing (13 passed, ~16.5-17.9s
local across 3 repeated runs -- comparable to the prior 12-test baseline
of ~17.7-18.4s). Failure-injection re-proof: reverting #2249's production
fix (prune.py, hook_integrator.py, uninstall/engine.py, apm_package.py)
to pre-fix content causes exactly `[claude]` to fail under `-m
lifecycle_smoke` selection (12 passed, 1 failed), confirming the gate is
load-bearing for this regression; restored cleanly afterward (working
tree verified clean pre/post).

Also merges origin/main (additive merge commit, no rebase/force-push)
to bring in #2249 plus intervening #2237 (Windows CI fix) and #2246
(lockfile-property tests). Resolved one real conflict in
merge-gate.yml's EXPECTED_CHECKS where main's #2237 follow-on
independently added a new required "Windows Compatibility Gate" check
to the same ternary our branch extended with "Lifecycle Smoke (Linux)"
-- both required-check names now coexist in both event branches.

Docs (integration-testing.md, CHANGELOG.md) updated to enumerate the
6th target and prune's Consume/Produce/Govern coverage. No change
needed to test_ci_topology.py's semantic assertions (count-agnostic
by design) or ci.yml (marker-driven auto-pickup, zero edit).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 75891045-fa50-40b7-9354-b4649195f724
@danielmeppiel

Copy link
Copy Markdown
Collaborator Author

Amendment: 6th lifecycle-smoke target promoted (prune-hook reconciliation)

Now that #2249 (fix(prune): reconcile merged hook ownership after package removal) has merged (49bfdc676b), this PR promotes the deferred 6th target per the earlier-recorded plan.

What changed:

  • Merged origin/main into this branch additively (merge commit 2f4d2b20c7, no rebase/force-push) -- brings in fix(prune): reconcile merged hook ownership after package removal #2249, plus intervening fix: Windows CI CRLF baseline and WebSocket shutdown thread-join (closes #2233) #2237 (Windows CI fix, adds required Windows Compatibility Gate) and test(deps): add field-complete lockfile round-trip and reconstruction properties #2246 (lockfile-property tests). One real conflict in merge-gate.yml's EXPECTED_CHECKS (independent additions from both branches) resolved by combining both required-check names.
  • Added pytest.mark.lifecycle_smoke to only the claude parametrization of test_prune_removes_merged_hook_entries_and_sidecar via pytest.param("claude", marks=pytest.mark.lifecycle_smoke) -- the sibling [cursor] parametrization and the file's other five tests remain unmarked.
  • Zero edit to ci.yml: the existing -m lifecycle_smoke tests/integration command auto-picked up the newly-marked test (confirmed via git diff showing no change to the lifecycle-smoke job section).
  • No change needed to tests/quality/test_ci_topology.py's semantic assertions (marker-registered / command-shape / non-empty-collection contracts are count-agnostic by design).
  • Docs (integration-testing.md) and CHANGELOG.md updated to enumerate the prune-hook target.

Verification:

  • Marker collection: 13 tests (was 12) -- exactly the prior 12 plus test_prune_removes_merged_hook_entries_and_sidecar[claude]; [cursor] confirmed excluded via a scoped collect-only check (1/7 collected).
  • Full 13-test family: 3 repeated runs, all green, 16.5-17.9s local (comparable to the prior 12-test 17.7-18.4s settled baseline).
  • New failure-injection proof: reverted fix(prune): reconcile merged hook ownership after package removal #2249's production fix (prune.py, hook_integrator.py, uninstall/engine.py, apm_package.py) via git apply -R of the PR's own diff -- [claude] failed under -m lifecycle_smoke selection (12 passed, 1 failed), proving the gate is load-bearing for this regression class; restored cleanly, working tree verified clean before/after.
  • Existing fix(ado): preserve lock coordinates for outdated and update #2226/fix(virtual-packages): restore hermetic lifecycle convergence #2240 failure-injection proofs re-verified, still catch their regressions.
  • Full lint mirror (ruff check/format, pylint R0801 10.00/10, lint-auth-signals.sh, lint-architecture-boundaries.sh) + actionlint on both modified workflow files: all clean.
  • 67 tests passing across test_ci_topology.py + test_test_taxonomy.py + test_architecture_authorities.py (unaffected by the promotion).
  • Real hosted CI green at final head 0b7bd060c0: all 18 checks pass, Lifecycle Smoke (Linux) 44s, Windows Compatibility Gate 86s (both now required, coexisting cleanly in merge-gate.yml).

Timing before/after:

Local (settled) Hosted CI job total
Marker refactor (12 tests) 17.7-18.4s 31s
+ prune-hook promotion (13 tests) 16.5-17.9s 44s

Both comfortably inside the 60-90s target; the 3-minute hard ceiling (timeout-minutes: 3) is untouched.

PR body updated to reflect the 6th target, 13-test count, and final timing. This PR remains OPEN and has not been merged, per standing instruction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] apm prune leaves orphaned hook entries in merged configs (settings.json / apm-hooks.json)

2 participants