33import base64
44import hashlib
55import json
6+ import shutil
67import subprocess
78from pathlib import Path
89
@@ -67,9 +68,13 @@ def _config(tmp_path: Path) -> Path:
6768 return config_path
6869
6970
70- def _capture (tmp_path : Path , monkeypatch : pytest .MonkeyPatch ):
71- source_checks : list [str ] = []
72- monkeypatch .setattr (present_module , "_verify_source_identity" , lambda expected : source_checks .append (expected ))
71+ def _capture (tmp_path : Path , monkeypatch : pytest .MonkeyPatch , * , output_dir : Path | None = None ):
72+ source_checks : list [Path | None ] = []
73+ monkeypatch .setattr (
74+ present_module ,
75+ "_verify_source_identity" ,
76+ lambda _expected , private_stage = None : source_checks .append (private_stage ),
77+ )
7378 calls : list [dict [str , object ]] = []
7479
7580 def run_once (config : dict [str , object ], mode : str ):
@@ -79,7 +84,7 @@ def run_once(config: dict[str, object], mode: str):
7984 monkeypatch .setattr (present_module , "run_market_regime_control_plugin" , run_once )
8085 package = present_module .capture_tqqq_market_regime_control_present (
8186 config_path = _config (tmp_path ),
82- output_dir = tmp_path / "packages" ,
87+ output_dir = output_dir or tmp_path / "packages" ,
8388 expected_qsp_commit_sha = EXPECTED_COMMIT ,
8489 as_of = AS_OF ,
8590 session_id = f"XNAS:{ AS_OF } " ,
@@ -95,7 +100,8 @@ def test_capture_publishes_one_canonical_package_after_one_current_producer_call
95100 payload_bytes = base64 .b64decode (decoded ["payload" ]["bytes_b64" ], validate = True )
96101
97102 assert len (calls ) == 1
98- assert source_checks == [EXPECTED_COMMIT , EXPECTED_COMMIT ]
103+ assert source_checks [0 ] is None
104+ assert source_checks [1 ] is not None
99105 assert package .path .parent == tmp_path / "packages"
100106 assert package .path .name == f"tqqq-market-regime-control-present-{ AS_OF } -{ package .sha256 } .json"
101107 assert package .sha256 == hashlib .sha256 (package_bytes ).hexdigest ()
@@ -113,6 +119,20 @@ def test_capture_publishes_one_canonical_package_after_one_current_producer_call
113119 assert list ((tmp_path / "packages" ).iterdir ()) == [package .path ]
114120
115121
122+ def test_capture_allows_unignored_in_repository_output_and_removes_private_stage (tmp_path , monkeypatch ) -> None :
123+ output_dir = Path .cwd () / f"tqqq-present-output-{ tmp_path .name } "
124+ try :
125+ package , _ , source_checks = _capture (tmp_path , monkeypatch , output_dir = output_dir )
126+
127+ assert package .path .parent == output_dir
128+ assert source_checks [0 ] is None
129+ assert source_checks [1 ] is not None
130+ assert source_checks [1 ].parent == output_dir
131+ assert not list (output_dir .glob (".tqqq-market-regime-control-present-*" ))
132+ finally :
133+ shutil .rmtree (output_dir , ignore_errors = True )
134+
135+
116136def test_capture_rejects_staged_input_mutation_without_a_final_package (tmp_path , monkeypatch ) -> None :
117137 monkeypatch .setattr (present_module , "_verify_source_identity" , lambda _expected : None )
118138
@@ -166,6 +186,71 @@ def test_source_identity_rejects_a_wrong_expected_commit(monkeypatch) -> None:
166186 present_module ._verify_source_identity ("0" * 40 )
167187
168188
189+ def _source_identity_git (monkeypatch , status : str ) -> tuple [Path , list [tuple [str , ...]]]:
190+ root = Path .cwd ().resolve ()
191+ relative_module = Path (present_module .__file__ ).resolve ().relative_to (root )
192+ calls : list [tuple [str , ...]] = []
193+
194+ def fake_git (* args : str ) -> str :
195+ calls .append (args )
196+ if args == ("rev-parse" , "--show-toplevel" ):
197+ return str (root )
198+ if args == ("ls-files" , "--error-unmatch" , str (relative_module )):
199+ return str (relative_module )
200+ if args and args [0 ] == "status" :
201+ return status
202+ if args == ("rev-parse" , "HEAD" ):
203+ return EXPECTED_COMMIT
204+ raise AssertionError (args )
205+
206+ monkeypatch .setattr (present_module , "_git" , fake_git )
207+ return root , calls
208+
209+
210+ def test_second_source_identity_check_allows_only_its_private_stage (monkeypatch ) -> None :
211+ root = Path .cwd ().resolve ()
212+ private_stage = root / "unignored-output" / ".tqqq-market-regime-control-present-test"
213+ relative_stage = private_stage .relative_to (root ).as_posix ()
214+ _ , calls = _source_identity_git (monkeypatch , f"?? { relative_stage } /\0 " )
215+
216+ present_module ._verify_source_identity (EXPECTED_COMMIT , private_stage = private_stage )
217+
218+ assert ("status" , "--porcelain=v1" , "-z" , "--untracked-files=all" ) in calls
219+
220+ with pytest .raises (present_module .PresentPackageError , match = "T2B2_PRODUCER_IDENTITY_INVALID" ):
221+ present_module ._verify_source_identity (EXPECTED_COMMIT )
222+
223+
224+ @pytest .mark .parametrize ("status" , [" M src/unrelated.py\0 " , "?? unrelated.txt\0 " ])
225+ def test_second_source_identity_check_rejects_unrelated_tracked_or_untracked_dirt (monkeypatch , status : str ) -> None :
226+ root = Path .cwd ().resolve ()
227+ private_stage = root / "unignored-output" / ".tqqq-market-regime-control-present-test"
228+ _source_identity_git (monkeypatch , f"?? { private_stage .relative_to (root ).as_posix ()} /\0 { status } " )
229+
230+ with pytest .raises (present_module .PresentPackageError , match = "T2B2_PRODUCER_IDENTITY_INVALID" ):
231+ present_module ._verify_source_identity (EXPECTED_COMMIT , private_stage = private_stage )
232+
233+
234+ def test_identity_failure_does_not_create_a_final_package (tmp_path , monkeypatch ) -> None :
235+ monkeypatch .setattr (
236+ present_module ,
237+ "_verify_source_identity" ,
238+ lambda * _args , ** _kwargs : (_ for _ in ()).throw (present_module .PresentPackageError ("T2B2_PRODUCER_IDENTITY_INVALID" , 2 )),
239+ )
240+ destination = tmp_path / "packages"
241+
242+ with pytest .raises (present_module .PresentPackageError , match = "T2B2_PRODUCER_IDENTITY_INVALID" ):
243+ present_module .capture_tqqq_market_regime_control_present (
244+ config_path = _config (tmp_path ),
245+ output_dir = destination ,
246+ expected_qsp_commit_sha = EXPECTED_COMMIT ,
247+ as_of = AS_OF ,
248+ session_id = f"XNAS:{ AS_OF } " ,
249+ )
250+
251+ assert not destination .exists ()
252+
253+
169254def test_strict_readback_rejects_an_unknown_package_field (tmp_path , monkeypatch ) -> None :
170255 package , _ , _ = _capture (tmp_path , monkeypatch )
171256 decoded = json .loads (package .path .read_bytes ())
0 commit comments