Option C Tools
Loading...
Searching...
No Matches
test_option_c_dir.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# tests/tests_core/test_option_c_dir.py
4
5"""
6Purpose
7-------
8FS-539 regression tests — :mod:`oct.core.option_c_dir` resolves paths
9to the canonical ``.option_c/`` project dotdir with a legacy fallback
10so in-flight migrations keep working for one release.
11
12Responsibilities
13----------------
14- ``option_c_dir(root)`` returns ``root/.option_c``.
15- ``resolve_octrc`` prefers ``.option_c/octrc.json``, falling back to
16 ``root/.octrc.json``.
17- ``resolve_debug_config`` prefers ``.option_c/debug_config.json``,
18 then ``oc_diagnostics/debug_config.json``, then legacy
19 ``diagnostics/debug_config.json``.
20- ``resolve_logs_dir`` prefers ``.option_c/logs``, falls back to
21 ``root/logs``.
22- ``load_oc_status`` returns ``None`` when the file is missing and an
23 :class:`OcStatus` dataclass when present.
24
25Diagnostics
26-----------
27Domain: CORE-TESTS
28 Levels:
29 L2 — test lifecycle
30 L3 — assertion details
31 L4 — deep tracing
32
33Contracts
34---------
35- All path-resolution tests use ``tmp_path`` as the project root.
36"""
37
38from __future__ import annotations
39
40import json
41from pathlib import Path
42
43import pytest
44
45from oct.core.option_c_dir import (
46 OPTION_C_DIRNAME,
47 OC_STATUS_FILENAME,
48 OcStatus,
49 load_oc_status,
50 option_c_dir,
51 resolve_debug_config,
52 resolve_logs_dir,
53 resolve_octrc,
54 write_oc_status,
55)
56
57
58# ---------------------------------------------------------------------
59# Constants
60# ---------------------------------------------------------------------
61
62
64 assert OPTION_C_DIRNAME == ".option_c"
65
66
68 assert OC_STATUS_FILENAME == "oc_status.json"
69
70
71def test_option_c_dir_joins_root(tmp_path: Path):
72 assert option_c_dir(tmp_path) == tmp_path / ".option_c"
73
74
75# ---------------------------------------------------------------------
76# resolve_octrc
77# ---------------------------------------------------------------------
78
79
81 def test_prefers_option_c_when_present(self, tmp_path: Path):
82 (tmp_path / ".option_c").mkdir()
83 modern = tmp_path / ".option_c" / "octrc.json"
84 modern.write_text("{}", encoding="utf-8")
85 legacy = tmp_path / ".octrc.json"
86 legacy.write_text("{}", encoding="utf-8")
87
88 assert resolve_octrc(tmp_path) == modern
89
91 legacy = tmp_path / ".octrc.json"
92 legacy.write_text("{}", encoding="utf-8")
93
94 assert resolve_octrc(tmp_path) == legacy
95
97 # The resolver must not raise when neither path exists; the loader
98 # handles the missing-file case. It returns the legacy path so the
99 # "file not found" error points the user at the expected location
100 # rather than a brand-new .option_c/ they haven't migrated to.
101 result = resolve_octrc(tmp_path)
102 assert result == tmp_path / ".octrc.json"
103
104
105# ---------------------------------------------------------------------
106# resolve_debug_config
107# ---------------------------------------------------------------------
108
109
111 def test_prefers_option_c_dir(self, tmp_path: Path):
112 (tmp_path / ".option_c").mkdir()
113 modern = tmp_path / ".option_c" / "debug_config.json"
114 modern.write_text("{}", encoding="utf-8")
115 (tmp_path / "oc_diagnostics").mkdir()
116 (tmp_path / "oc_diagnostics" / "debug_config.json").write_text(
117 "{}", encoding="utf-8",
118 )
119
120 assert resolve_debug_config(tmp_path) == modern
121
122 def test_falls_back_to_oc_diagnostics(self, tmp_path: Path):
123 (tmp_path / "oc_diagnostics").mkdir()
124 legacy = tmp_path / "oc_diagnostics" / "debug_config.json"
125 legacy.write_text("{}", encoding="utf-8")
126
127 assert resolve_debug_config(tmp_path) == legacy
128
130 (tmp_path / "diagnostics").mkdir()
131 very_legacy = tmp_path / "diagnostics" / "debug_config.json"
132 very_legacy.write_text("{}", encoding="utf-8")
133
134 assert resolve_debug_config(tmp_path) == very_legacy
135
136
137# ---------------------------------------------------------------------
138# resolve_logs_dir
139# ---------------------------------------------------------------------
140
141
143 def test_prefers_option_c_logs(self, tmp_path: Path):
144 modern = tmp_path / ".option_c" / "logs"
145 modern.mkdir(parents=True)
146 (tmp_path / "logs").mkdir()
147
148 assert resolve_logs_dir(tmp_path) == modern
149
150 def test_falls_back_to_root_logs(self, tmp_path: Path):
151 legacy = tmp_path / "logs"
152 legacy.mkdir()
153
154 assert resolve_logs_dir(tmp_path) == legacy
155
157 # No .option_c/ and no logs/ — an unmigrated working tree should
158 # get the legacy path so legacy callers don't see surprise
159 # .option_c/ creation. Fresh scaffolds materialise .option_c/
160 # up-front, so the resolver then switches to the modern path.
161 result = resolve_logs_dir(tmp_path)
162 assert result == tmp_path / "logs"
163
165 # .option_c/ present (scaffold / migrate created it), logs subdir
166 # not yet created — the resolver returns the modern path so the
167 # first write lands under .option_c/logs/.
168 (tmp_path / ".option_c").mkdir()
169 result = resolve_logs_dir(tmp_path)
170 assert result == tmp_path / ".option_c" / "logs"
171
172
173# ---------------------------------------------------------------------
174# OcStatus + read/write
175# ---------------------------------------------------------------------
176
177
179 def test_load_returns_none_when_missing(self, tmp_path: Path):
180 assert load_oc_status(tmp_path) is None
181
183 (tmp_path / ".option_c").mkdir()
184 (tmp_path / ".option_c" / "oc_status.json").write_text(
185 "{ not json", encoding="utf-8",
186 )
187 assert load_oc_status(tmp_path) is None
188
189 def test_write_and_round_trip(self, tmp_path: Path):
190 status = OcStatus(
191 stage="compliant",
192 pillars={
193 "linter": {"enabled": True, "since": "2026-04-16"},
194 "diagnostics": {"enabled": True, "since": "2026-04-16"},
195 "git": {"enabled": False, "since": None},
196 "docs": {"enabled": True, "since": "2026-04-16"},
197 "tests": {"enabled": True, "since": "2026-04-16"},
198 "scaffold": {"enabled": True, "since": "2026-04-16"},
199 },
200 migrated_from={},
201 oct_version_at_migration="0.19.0",
202 )
203 write_oc_status(tmp_path, status)
204
205 path = tmp_path / ".option_c" / "oc_status.json"
206 assert path.is_file()
207
208 loaded = load_oc_status(tmp_path)
209 assert loaded is not None
210 assert loaded.stage == "compliant"
211 assert loaded.pillars["git"]["enabled"] is False
212 assert loaded.oct_version_at_migration == "0.19.0"
213
215 status = OcStatus(
216 stage="bootstrap",
217 pillars={},
218 migrated_from={},
219 oct_version_at_migration="0.19.0",
220 )
221 write_oc_status(tmp_path, status)
222
223 assert (tmp_path / ".option_c").is_dir()
224 assert (tmp_path / ".option_c" / "oc_status.json").is_file()
225
226 def test_write_emits_schema_version(self, tmp_path: Path):
227 status = OcStatus(
228 stage="compliant",
229 pillars={},
230 migrated_from={},
231 oct_version_at_migration="0.19.0",
232 )
233 write_oc_status(tmp_path, status)
234
235 raw = json.loads(
236 (tmp_path / ".option_c" / "oc_status.json").read_text(
237 encoding="utf-8",
238 )
239 )
240 assert raw["_schema_version"] == "1.0"
241
243 with pytest.raises(ValueError):
244 OcStatus(
245 stage="banana", # type: ignore[arg-type]
246 pillars={},
247 migrated_from={},
248 oct_version_at_migration="0.19.0",
249 )
test_write_creates_option_c_dir_if_missing(self, Path tmp_path)
test_load_returns_none_when_invalid_json(self, Path tmp_path)
test_returns_legacy_path_when_nothing_exists(self, Path tmp_path)
test_falls_back_to_legacy_when_modern_absent(self, Path tmp_path)
test_option_c_dir_joins_root(Path tmp_path)