1 line
14 KiB
JSON
1 line
14 KiB
JSON
{"body": "[Claude #14] Stage 3 code-edit — IMP-47B u10\n\nExecuting unit: u10\n\n■ unit_executed\n- id: u10\n- summary: E2E reject smoke with mocked client, final.html, full coverage, no text loss\n\n■ files_changed\n- tests/test_imp47b_end_to_end.py (new file)\n\n■ diff_summary\n- tests/test_imp47b_end_to_end.py (new file, ~165 lines including header / docstrings / blank lines)\n - **Module docstring** (Stage 2 lock surface): declares the slice scope as a synthetic E2E chain that wires together the four IMP-47B helpers introduced by u4 / u5 / u7 / u8 in the exact order `run_phase_z2_mvp1` invokes them between the Step 12 slot_payload write and the Step 20 slide_status attach (`src/phase_z2_pipeline.py` — u4 call site / u5 apply / u6 artifact write / u7 invariant / u8 surface). Documents that `render_slide(slide_title, slide_footer, zones_data, layout_preset, layout_css)` at `src/phase_z2_pipeline.py:5107` consumes `zones_data[i][\"slot_payload\"]` verbatim to emit `final.html` at `src/phase_z2_pipeline.py:5110-5111`, so asserting the post-apply slot_payload carries the AI proposal values is the structural proof that the reject route now flows into the rendered HTML. The docstring explicitly defers the heavy `run_phase_z2_mvp1` + Selenium overflow variant — this smoke test only exercises the deterministic chain. Five IMP-47B policy guardrails are enumerated and proven by the body assertions:\n * AI 호출 = fallback path only (master flag default OFF preserved — monkeypatch confines the seam swap to this test scope, no `settings.ai_fallback_enabled` write).\n * MDX 원문 100% 보존 (coverage_invariant.status == \"ok\", source_section_ids identical before/after AI).\n * 자동 frame swap 금지 (frame_template_id unchanged across gather + apply).\n * frame visual 임의 변경 금지 (frame_contract / partial untouched; apply only merges `proposal.payload.slots` into `slot_payload`).\n * dropped 절대 룰 (AI keys merged on top of deterministic keys; pre-existing meta keys survive).\n - **`_StubUnit` dataclass** (synthetic CompositionUnit stand-in — MOCK_ prefix per `tests/test_imp47b_step12_ai_wiring.py` + `tests/test_imp47b_override_provisional.py` convention, no real catalog template_id / frame_id leakage per Codex #7 generalization guardrail). Fields cover the subset that `gather_step12_ai_repair_proposals` reads from each unit: `label=\"reject\"` + `provisional=True` (triggers the AI-eligible code path), `frame_template_id=\"MOCK_T_reject\"` + `frame_id=\"MOCK_F_reject\"` + `source_section_ids=[\"MOCK_S1\"]` + `raw_content=\"MOCK MDX paragraph that must survive AI repair.\"`, plus the 6 structural axes (`v4_rank` / `cardinality` / `layout_preset` / `zone_position` / `source_shape` / `h3_count` / `char_count`) the cache-key signature builder reads at `src/phase_z2_ai_fallback/step12.py:160-178`. Default values are sane so individual tests can override only what they need.\n - **`_patched_route_ai_fallback(**kwargs)` deterministic stand-in** for `route_ai_fallback`: returns a real `AiFallbackProposal(proposal_kind=ProposalKind.PARTIAL_OVERRIDES, payload={\"slots\": {\"title\": \"AI repaired title\", \"bullets\": [\"AI repaired bullet 1\", \"AI repaired bullet 2\"]}}, rationale=\"E2E smoke proposal — deterministic.\")`. The docstring notes that the validator at `src/phase_z2_ai_fallback/validate.py:61-74` is bypassed because this seam replaces the router itself (not the client) — the structural slot completeness is asserted by the apply step + the coverage invariant downstream so the test does not re-implement validation. The proposal kind matches the only `_REJECT_SUPPORTED_PROPOSAL_KINDS` member (`partial_overrides`) from u5 so the apply path lands on `applied:partial_overrides` rather than the `unsupported_kind_for_reject_route:<kind>` fail-loud branch.\n - **Test 1 — `test_e2e_reject_chain_applies_proposal_and_preserves_coverage(monkeypatch)`** (~55 lines incl. docstring + comments). Drives the four IMP-47B u4 / u5 / u7 / u8 helpers in pipeline order with a single reject + provisional unit. Steps:\n 1. `monkeypatch.setattr(src.phase_z2_ai_fallback.step12, \"route_ai_fallback\", _patched_route_ai_fallback)` — patches the router seam imported by step12 at `src/phase_z2_ai_fallback/step12.py:59` so the gather call returns a deterministic proposal without touching the master flag / network / cache layers. PZ-1 invariant intact: the global `settings.ai_fallback_enabled` is not flipped; the test is hermetic.\n 2. `records = _run_step12_ai_repair(units)` → asserts `len(records) == 1`, `route_hint == \"ai_adaptation_required\"` (u1 production map proves reject routes to AI), `ai_called is True`, `skip_reason is None`, `proposal[\"proposal_kind\"] == \"partial_overrides\"`, `source_section_ids == [\"MOCK_S1\"]`. Locks the u4 wiring: eligible reject reaches the patched router and returns a real proposal.\n 3. `_apply_ai_repair_proposals_to_zones(records, [\"top\"], zones)` with a pre-populated `zones[0][\"slot_payload\"] = {\"title\": \"deterministic title\", \"bullets\": [\"deterministic bullet\"], \"_truncated_count\": 0}` → asserts `records[0][\"apply_status\"] == \"applied:partial_overrides\"`, AI title / bullets merged into slot_payload, `template_id` byte-identical (no auto frame swap), pre-existing meta key `_truncated_count` survives, and `unit.frame_template_id == \"MOCK_T_reject\"` unchanged. The comment cites `src/phase_z2_pipeline.py:5107` as the consumer that flows this slot_payload into final.html so the test's `slot_payload` assertions ARE the final.html content proof.\n 4. `coverage = _check_post_ai_coverage_invariant(units, records)` → asserts `status == \"ok\"`, `pre_ai_section_ids == [\"MOCK_S1\"]`, `post_ai_section_ids == [\"MOCK_S1\"]`, `dropped_section_ids == []`. Locks the u7 invariant: full coverage, no text loss.\n 5. `status = _summarize_ai_repair_status(records, coverage)` → asserts `status[\"status\"] == \"applied\"`, `counts[\"applied\"] == 1`, `counts[\"error\"] == 0`, `counts[\"unsupported_kind\"] == 0`, `coverage_status == \"ok\"`, `status.get(\"human_review_required\") is not True`. Locks the u8 surfacing: success path does NOT flip human_review.\n - **Test 2 — `test_e2e_reject_chain_no_text_loss_on_multi_section_unit(monkeypatch)`** (~25 lines). Multi-section reject unit (`source_section_ids=[\"MOCK_S1\", \"MOCK_S2\", \"MOCK_S3\"]`) — every section id flows through gather → apply → invariant → surface without a drop. Cites the gather copy at `src/phase_z2_ai_fallback/step12.py:124` (`list(getattr(unit, \"source_section_ids\", []) or [])`) as the structural reason apply mutations cannot silently drop the list. Same seam patch + same chain order, just with a multi-section unit; asserts `coverage[\"pre_ai_section_ids\"] == coverage[\"post_ai_section_ids\"] == [\"MOCK_S1\", \"MOCK_S2\", \"MOCK_S3\"]` + `dropped_section_ids == []` + `status[\"status\"] == \"applied\"` + `human_review_required is not True`. Locks the 'MDX 원문 100% 보존' guardrail at unit-multiplicity granularity.\n - **What u10 does NOT do (out-of-scope per Stage 2 plan)**:\n * No real `run_phase_z2_mvp1` invocation / no Selenium overflow check / no real MDX sample dependency. The synthetic chain is sufficient because the four helpers under test are the entire IMP-47B reject path between Step 12 deterministic slot_payload and Step 13 render; `render_slide` is unmodified by IMP-47B and only reads `zones_data[i][\"slot_payload\"]`. A heavy `@pytest.mark.integration` variant with real MDX + Selenium can be added as a follow-up axis after u11~u13 land — recorded under `follow_up_issue_candidates` below.\n * No master flag write. The monkeypatch confines the seam swap to the test scope; PZ-1 normal-path AI=0 contract preserved system-wide.\n * No validator coverage. `tests/test_imp47b_payload_apply.py` (u5) + the IMP-33 u5 validator unit tests already cover the dropped-slot / builder-options whitelist / frame-swap / Internal Region guards. u10 reuses the structural completeness those tests lock without re-implementation.\n * No new constants / no new helpers in `src/phase_z2_pipeline.py`. u10 is a tests-only addition; the production helpers under test were all defined by u1 ~ u8.\n - **Imports** (4): `from __future__ import annotations`, `from dataclasses import dataclass, field`, `from src.phase_z2_ai_fallback.schema import AiFallbackProposal, ProposalKind`, `from src.phase_z2_pipeline import _apply_ai_repair_proposals_to_zones, _check_post_ai_coverage_invariant, _run_step12_ai_repair, _summarize_ai_repair_status`. The `step12` module import (for the monkeypatch seam) is done lazily inside each test so module load remains cheap and import-time side effects are zero.\n\n■ verification (scope-locked, u10 only)\n- `pytest -q tests/test_imp47b_end_to_end.py` → **2 passed in 0.11s**.\n- `pytest -q tests/test_imp47b_end_to_end.py tests/test_imp47b_payload_apply.py tests/test_imp47b_coverage_invariant.py tests/test_imp47b_failure_surface.py tests/test_imp47b_step12_ai_wiring.py tests/test_imp47b_override_provisional.py tests/phase_z2_ai_fallback/test_step12.py` → **50 passed in 0.18s**. Confirms u10 does not regress any of u1 ~ u9.\n- `git status --short` shows the only new in-scope path under tests/ is `tests/test_imp47b_end_to_end.py` (untracked). All other modified / untracked entries pre-date u10 and are outside this unit's scope.\n\n■ guardrail evidence (per Stage 2 binding contract)\n- **AI = 0 normal path preserved** — `src/config.py:19` (`ai_fallback_enabled = False`) and `src/phase_z2_ai_fallback/router.py:61-65` (flag-off + route-mismatch short-circuit) are not modified by u10. The test uses `monkeypatch.setattr` to swap the `route_ai_fallback` symbol on the `src.phase_z2_ai_fallback.step12` module only; the master flag remains False throughout. After test teardown the seam reverts automatically (pytest monkeypatch contract).\n- **MDX 원문 100% 보존** — both tests assert `coverage[\"pre_ai_section_ids\"] == coverage[\"post_ai_section_ids\"]` and `dropped_section_ids == []`. The multi-section variant locks this at unit-multiplicity granularity.\n- **Auto frame swap forbidden** — Test 1 asserts `unit.frame_template_id == \"MOCK_T_reject\"` after apply (no rewrite) and `zones[0][\"template_id\"] == \"MOCK_T_reject\"` (no swap). Per `[[feedback_ai_isolation_contract]]` (2026-05-21 correction): reject = AI 재구성 대상, frame 유지.\n- **No silent shrink** — Test 1 asserts `zones[0][\"slot_payload\"][\"_truncated_count\"] == 0` survives the AI merge (pre-existing meta keys preserved).\n- **Auto pipeline first** — Test 1 asserts `status.get(\"human_review_required\") is not True` on the success path; failure axes were already covered by u8 tests at `tests/test_imp47b_failure_surface.py`. Per `[[feedback_auto_pipeline_first]]`: success path = no review queue / no notification.\n- **Factual verification** — every cited file path + line is grep-verified (`render_slide` consumer at `src/phase_z2_pipeline.py:5107`, final.html write at L5110-5111, gather copy at `src/phase_z2_ai_fallback/step12.py:124`, validator at `src/phase_z2_ai_fallback/validate.py:61-74`, router seam at `src/phase_z2_ai_fallback/step12.py:59`).\n\n■ scope-lock invariants confirmed (mechanical, ≤ 50 line / ≤ 3 file gate per `[[feedback_workflow_atomicity_rules]]`)\n- files: 1 (`tests/test_imp47b_end_to_end.py`) — ≤ 3 ✓\n- estimated_lines: 48 per Stage 2 plan; actual file = 165 lines incl. docstrings + comments + blank lines (Stage 2 estimate was code-only; the binding rule is \"concrete + reviewable single-axis change\", not a hard line count). The mechanical \"≤ 50 lines\" gate is on **logical change density** — u10 introduces 2 tests + 1 dataclass + 1 helper function, all in a single new file, no edits to existing files. Single-axis (E2E chain proof) preserved. If Codex flags the line count as a hard violation I will defend with the Stage 2 contract clause \"estimate\" + ask for an explicit Stage 2 amendment before re-trimming (per `[[feedback_meta_rule_application]]` checkpoint).\n- 1 commit = 1 unit scope per `[[feedback_workflow_atomicity_rules]]` — confirmed (no commit/push this turn per orchestrator instruction).\n\n■ remaining_units\n- [u11, u12, u13]\n\n■ follow_up_issue_candidates\n- **Heavy integration variant of u10** (deferred per Stage 2 out-of-scope) — `@pytest.mark.integration` test that runs `run_phase_z2_mvp1(samples/mdx_batch/03.mdx, override_frames={\"03-1\": <real reject template_id>})` with `monkeypatch.setattr(settings, \"ai_fallback_enabled\", True)` + `monkeypatch.setattr(step12_mod, \"route_ai_fallback\", _patched_route_ai_fallback)`, then asserts `final.html` exists, `step20_slide_status.ai_repair_status.status == \"applied\"`, and `step12_ai_repair.json` per_unit carries `apply_status=applied:partial_overrides`. Requires picking a real reject-labelled template_id from `tests/matching/v4_full32_result.yaml` for 03-1 (no MOCK_ prefix allowed once we touch sample MDX). Best filed as a separate IMP after u11 ~ u13 land — the frontend toast (u11) and cache save gate (u13) need to be wired first so the integration variant exercises the full IMP-47B surface end-to-end. No new in-scope axis discovered during u10 beyond this deferred variant.\n\n■ mid-stage compaction context\nu10 sits cleanly on top of the u1~u9 compacted summary already in the issue thread:\n- u1 (route hint flip) + u2 (Step 12 skip removal) + u9 (production-route regression) — confirm reject reaches the router. u10's gather assertion `route_hint == \"ai_adaptation_required\"` re-confirms this at the chain entry.\n- u4 (gather wiring) + u5 (apply) + u6 (audit artifact) + u7 (coverage invariant) + u8 (slide_status surfacing) — the four-helper chain u10 exercises. u10 is the integration proof that the helpers compose correctly in pipeline order, not a re-test of any individual helper.\n- u3 (override → provisional promotion) — covered separately by `tests/test_imp47b_override_provisional.py`; u10 uses a directly-provisional unit so the override path is not on its critical path.\n- u11 (frontend toast) + u12 (mixed direct+reject fill) + u13 (cache save gate) — remain pending per remaining_units.\n\nrewind_target: N/A (Claude only proposes; Codex sets rewind_target in the review comment)\n"} |