"""Phase 2 regression-guard tests for cruft_elimination_20260627. Per SPEC_CORRECTION_phase_2.md acceptance criteria: - VC8 (corrected): flat_config returns typed ProjectContext - VC8 (corrected): All 6 sub-dataclasses exist - VC8 (corrected): Consumers unchanged (Option A) - existing tests pass - VC8 (corrected): Dict-compat works (ctx.get() / ctx[]) - VC8 (corrected): output_dir REQUIRED field works (zero default is OK) """ from __future__ import annotations import pytest from src.project_manager import flat_config from src.project import ( ProjectContext, ProjectMeta, ProjectOutput, ProjectFiles, ProjectScreenshots, ProjectDiscussion, EMPTY_PROJECT_CONTEXT, ) def test_all_six_sub_dataclasses_importable() -> None: """All 6 sub-dataclasses are importable from src.models.""" assert isinstance(ProjectMeta, type) assert isinstance(ProjectOutput, type) assert isinstance(ProjectFiles, type) assert isinstance(ProjectScreenshots, type) assert isinstance(ProjectDiscussion, type) assert isinstance(ProjectContext, type) assert isinstance(EMPTY_PROJECT_CONTEXT, ProjectContext) def test_flat_config_returns_project_context() -> None: """VC8 (corrected): flat_config returns ProjectContext (typed).""" ctx = flat_config({}) assert isinstance(ctx, ProjectContext) assert isinstance(ctx.project, ProjectMeta) assert isinstance(ctx.output, ProjectOutput) assert isinstance(ctx.files, ProjectFiles) assert isinstance(ctx.screenshots, ProjectScreenshots) assert isinstance(ctx.discussion, ProjectDiscussion) def test_flat_config_empty_dict_yields_zero_defaults() -> None: """Empty dict input -> all sub-dataclass fields are zero-initialized.""" ctx = flat_config({}) assert ctx.project.name == "" assert ctx.project.summary_only is False assert ctx.project.execution_mode == "standard" assert ctx.output.namespace == "project" assert ctx.output.output_dir == "" # REQUIRED field, empty by default assert ctx.files.base_dir == "" assert ctx.files.paths == () assert ctx.screenshots.base_dir == "." assert ctx.screenshots.paths == () assert ctx.discussion.roles == () assert ctx.discussion.history == () def test_flat_config_full_dict_input() -> None: """Full dict input -> correct dataclass fields populated.""" proj = { "project": {"name": "test", "summary_only": True, "execution_mode": "fast"}, "output": {"namespace": "ns1", "output_dir": "/tmp/out"}, "files": {"base_dir": "/src", "paths": ["a.py", "b.py"]}, "screenshots":{"base_dir": "/scr", "paths": ["s1.png"]}, "discussion":{ "active": "main", "roles": ["User", "AI"], "discussions": {"main": {"history": ["msg1", "msg2"]}}, }, } ctx = flat_config(proj, disc_name="main") assert ctx.project.name == "test" assert ctx.project.summary_only is True assert ctx.project.execution_mode == "fast" assert ctx.output.namespace == "ns1" assert ctx.output.output_dir == "/tmp/out" assert ctx.files.base_dir == "/src" assert ctx.files.paths == ("a.py", "b.py") assert ctx.screenshots.base_dir == "/scr" assert ctx.screenshots.paths == ("s1.png",) assert ctx.discussion.roles == ("User", "AI") assert ctx.discussion.history == ("msg1", "msg2") def test_flat_config_dict_compat_getitem() -> None: """ctx[\"key\"] returns the same shape as the legacy dict.""" proj = {"output": {"output_dir": "/out", "namespace": "ns1"}} ctx = flat_config(proj) assert ctx["output"] == {"namespace": "ns1", "output_dir": "/out"} assert ctx["files"] == {"base_dir": "", "paths": []} assert ctx["project"] == {"name": "", "summary_only": False, "execution_mode": "standard"} def test_flat_config_dict_compat_get() -> None: """ctx.get(\"key\", default) returns dict value or default.""" ctx = flat_config({}) assert ctx.get("output") == {"namespace": "project", "output_dir": ""} assert ctx.get("missing_key") is None assert ctx.get("missing_key", "fallback") == "fallback" def test_flat_config_to_dict_round_trip() -> None: """to_dict() returns the same shape as the legacy flat_config() dict.""" proj = { "project": {"name": "p", "summary_only": False, "execution_mode": "std"}, "output": {"namespace": "ns", "output_dir": "/o"}, "files": {"base_dir": "/s", "paths": ["x"]}, "screenshots":{"base_dir": "/sc", "paths": ["i"]}, "context_presets": {"preset_a": {"name": "preset_a"}}, "discussion": { "roles": ["User"], "active": "main", "discussions": {"main": {"history": ["h1"]}}, }, } ctx = flat_config(proj, disc_name="main") d = ctx.to_dict() assert d["project"] == {"name": "p", "summary_only": False, "execution_mode": "std"} assert d["output"] == {"namespace": "ns", "output_dir": "/o"} assert d["files"] == {"base_dir": "/s", "paths": ["x"]} assert d["screenshots"] == {"base_dir": "/sc", "paths": ["i"]} assert d["context_presets"] == {"preset_a": {"name": "preset_a"}} assert d["discussion"] == {"roles": ["User"], "history": ["h1"]} def test_empty_project_context_sentinel() -> None: """EMPTY_PROJECT_CONTEXT is a zero-init ProjectContext.""" assert isinstance(EMPTY_PROJECT_CONTEXT, ProjectContext) assert EMPTY_PROJECT_CONTEXT.project.name == "" assert EMPTY_PROJECT_CONTEXT.output.output_dir == "" assert EMPTY_PROJECT_CONTEXT.files.paths == () assert EMPTY_PROJECT_CONTEXT.screenshots.base_dir == "." assert EMPTY_PROJECT_CONTEXT.discussion.roles == () def test_output_dir_required_field_zero_default() -> None: """output_dir is a REQUIRED field per spec; zero default (empty str) is acceptable. aggregate.run would fail with a clear error when output_dir is empty (existing behavior, not a regression).""" ctx = flat_config({}) assert ctx.output.output_dir == "" # Verify it's still a valid string field (not None) assert isinstance(ctx.output.output_dir, str) def test_flat_config_consumers_unchanged() -> None: """VC8 Option A: existing consumer code patterns continue to work via dict-compat methods (ctx.get(\"key\"), ctx[\"key\"]).""" ctx = flat_config({}) # Mimic consumer patterns from src/aggregate.py:484-525: assert ctx.get("output", {}).get("namespace", "project") == "project" assert ctx.get("files", {}).get("paths", []) == [] # list, not tuple (legacy compat) assert ctx.get("discussion", {}).get("history", []) == [] # list, not tuple (legacy compat) # Mimic src/app_controller.py:4026 pattern: flat[\"files\"] returns dict flat_files = ctx["files"] assert isinstance(flat_files, dict) assert "base_dir" in flat_files assert "paths" in flat_files