gabriel / muse public
test_cmd_archive_hardening.py python
588 lines 23.6 KB
Raw
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 139 days ago
1 """Comprehensive hardening tests for ``muse archive``.
2
3 Coverage dimensions:
4
5 Unit
6 ~~~~
7 - ``_safe_arcname`` empty rel_path rejected
8 - ``_safe_arcname`` null bytes in rel_path rejected
9 - ``_safe_arcname`` null bytes in prefix rejected
10 - ``_safe_arcname`` dot-only path rejected (".")
11 - ``_safe_arcname`` deeply nested safe path allowed
12 - ``_safe_arcname`` path with spaces allowed
13 - ``_safe_arcname`` unicode filenames allowed
14 - ``_ArchiveJson`` TypedDict has all expected fields
15
16 Security
17 ~~~~~~~~
18 - ``--json`` flag now works (not broken by format validation)
19 - All error messages route to stderr, not stdout
20 - Unknown --format rejected with nonzero exit (argparse choices= guard)
21 - --prefix with ``..`` rejected with nonzero exit
22 - Zip-slip in manifest (``../`` prefix) skipped in tar.gz
23 - Zip-slip in manifest (``../`` prefix) skipped in zip
24 - ANSI escape sequences in commit message sanitized in text output
25 - Null byte in manifest rel_path skipped silently
26
27 JSON schema
28 ~~~~~~~~~~~
29 - ``--json`` on tar.gz produces valid ``_ArchiveJson`` schema
30 - ``--json`` on zip produces valid ``_ArchiveJson`` schema
31 - ``--json`` includes correct ``file_count`` and ``bytes``
32 - ``--json`` includes ``commit_id`` (full SHA-256)
33 - ``--json`` includes ``message`` and ``branch``
34 - ``--json`` includes ``ref`` as null when HEAD used
35 - ``--json`` includes ``ref`` as string when --ref used
36 - ``--json`` on empty snapshot reports file_count=0
37
38 Integration
39 ~~~~~~~~~~~
40 - ``--ref`` with short SHA resolves correctly
41 - ``--ref`` with branch name resolves correctly
42 - ``--ref`` with unknown ref exits nonzero and writes to stderr
43 - Default output path is ``<sha12>.tar.gz``
44 - Custom output path honoured
45 - Missing object in manifest skipped gracefully
46 - Archive file content matches committed bytes (round-trip)
47 - Zip archive entries are readable
48 - Tar.gz archive entries are readable
49
50 E2E
51 ~~~
52 - Full lifecycle: init → commit files → archive → verify contents
53 - ``--prefix`` adds directory level inside both tar.gz and zip
54 - Repeated archive calls produce identical archives (deterministic)
55 - No ``.muse/`` metadata appears in any archive entry
56
57 Stress
58 ~~~~~~
59 - 200-file archive completes without error
60 - Concurrent archive calls on different repos are safe
61 """
62
63 from __future__ import annotations
64
65 type _FileStore = dict[str, bytes]
66
67 import hashlib
68 import json
69 import pathlib
70 import tarfile
71 import threading
72 import uuid
73 import zipfile
74 from typing import TypedDict
75
76 import pytest
77 from tests.cli_test_helper import CliRunner, InvokeResult
78 from muse.core._types import long_id, short_id
79
80 cli = None
81 runner = CliRunner()
82
83
84 # ---------------------------------------------------------------------------
85 # Helpers
86 # ---------------------------------------------------------------------------
87
88
89 def _env(root: pathlib.Path) -> Manifest:
90 return {"MUSE_REPO_ROOT": str(root)}
91
92
93 def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path:
94 import datetime as dt
95 muse = tmp_path / ".muse"
96 for sub in ("objects", "commits", "snapshots", "refs/heads"):
97 (muse / sub).mkdir(parents=True, exist_ok=True)
98 (muse / "repo.json").write_text(json.dumps({
99 "repo_id": str(uuid.uuid4()),
100 "domain": "code",
101 "default_branch": "main",
102 "created_at": "2026-01-01T00:00:00+00:00",
103 }), encoding="utf-8")
104 (muse / "HEAD").write_text("ref: refs/heads/main\n", encoding="utf-8")
105 return tmp_path
106
107
108 def _write_object(root: pathlib.Path, content: bytes) -> str:
109 from muse.core.object_store import write_object
110 obj_id = long_id(hashlib.sha256(content).hexdigest())
111 write_object(root, obj_id, content)
112 return obj_id
113
114
115 def _make_commit(
116 root: pathlib.Path,
117 files: _FileStore | None = None,
118 message: str = "test commit",
119 ) -> str:
120 import datetime as dt
121 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
122 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
123
124 ref_file = root / ".muse" / "refs" / "heads" / "main"
125 parent_id = ref_file.read_text().strip() if ref_file.exists() else None
126
127 manifest: Manifest = {}
128 for rel_path, content in (files or {}).items():
129 manifest[rel_path] = _write_object(root, content)
130
131 snap_id = compute_snapshot_id(manifest)
132 committed_at = dt.datetime(2026, 1, 1, tzinfo=dt.timezone.utc)
133 commit_id = compute_commit_id(
134 [parent_id] if parent_id else [], snap_id, message, committed_at.isoformat()
135 )
136 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
137 write_commit(root, CommitRecord(
138 commit_id=commit_id,
139 repo_id="test-repo",
140 branch="main",
141 snapshot_id=snap_id,
142 message=message,
143 committed_at=committed_at,
144 parent_commit_id=parent_id,
145 ))
146 ref_file.parent.mkdir(parents=True, exist_ok=True)
147 ref_file.write_text(commit_id, encoding="utf-8")
148 return commit_id
149
150
151 def _invoke(root: pathlib.Path, *args: str) -> InvokeResult:
152 return runner.invoke(cli, ["archive"] + list(args), env=_env(root), catch_exceptions=False)
153
154
155 class _ArchiveJson(TypedDict):
156 path: str
157 format: str
158 file_count: int
159 bytes: int
160 commit_id: str
161 message: str
162 branch: str
163 ref: str | None
164
165
166 def _parse_json(output: str) -> _ArchiveJson:
167 for line in output.splitlines():
168 line = line.strip()
169 if line.startswith("{"):
170 raw = json.loads(line)
171 return _ArchiveJson(
172 path=str(raw["path"]),
173 format=str(raw["format"]),
174 file_count=int(raw["file_count"]),
175 bytes=int(raw["bytes"]),
176 commit_id=str(raw["commit_id"]),
177 message=str(raw["message"]),
178 branch=str(raw["branch"]),
179 ref=raw["ref"] if raw["ref"] is not None else None,
180 )
181 raise AssertionError(f"No JSON object found in output:\n{output}")
182
183
184 # ---------------------------------------------------------------------------
185 # Unit — _safe_arcname edge cases
186 # ---------------------------------------------------------------------------
187
188
189 class TestSafeArcname:
190 def test_empty_rel_path_rejected(self) -> None:
191 from muse.cli.commands.archive import _safe_arcname
192 assert _safe_arcname("", "") is None
193 assert _safe_arcname("prefix", "") is None
194
195 def test_null_byte_in_rel_path_rejected(self) -> None:
196 from muse.cli.commands.archive import _safe_arcname
197 assert _safe_arcname("", "file\x00.txt") is None
198
199 def test_null_byte_in_prefix_rejected(self) -> None:
200 from muse.cli.commands.archive import _safe_arcname
201 assert _safe_arcname("pre\x00fix", "file.txt") is None
202
203 def test_dot_only_path_rejected(self) -> None:
204 from muse.cli.commands.archive import _safe_arcname
205 # PurePosixPath("") normalises to "." — must be rejected
206 assert _safe_arcname("", ".") is None
207
208 def test_deeply_nested_safe_path(self) -> None:
209 from muse.cli.commands.archive import _safe_arcname
210 assert _safe_arcname("", "a/b/c/d/e/file.txt") == "a/b/c/d/e/file.txt"
211
212 def test_path_with_spaces(self) -> None:
213 from muse.cli.commands.archive import _safe_arcname
214 assert _safe_arcname("", "my file.mid") == "my file.mid"
215
216 def test_unicode_filename(self) -> None:
217 from muse.cli.commands.archive import _safe_arcname
218 assert _safe_arcname("", "音楽/track.mid") == "音楽/track.mid"
219
220 def test_prefix_with_subdirs(self) -> None:
221 from muse.cli.commands.archive import _safe_arcname
222 assert _safe_arcname("release/v1.0", "file.txt") == "release/v1.0/file.txt"
223
224
225 # ---------------------------------------------------------------------------
226 # Security
227 # ---------------------------------------------------------------------------
228
229
230 class TestSecurity:
231 def test_json_flag_now_works(self, tmp_path: pathlib.Path) -> None:
232 """--json must NOT exit with an error (it was broken before the fix)."""
233 root = _make_repo(tmp_path)
234 _make_commit(root, files={"song.mid": b"MIDI"})
235 out = tmp_path / "out.tar.gz"
236 result = _invoke(root, "--output", str(out), "--json")
237 assert result.exit_code == 0, f"--json flag is still broken: {result.output}"
238
239 def test_error_unknown_format_to_stderr(self, tmp_path: pathlib.Path) -> None:
240 """Unknown --format must exit nonzero (argparse choices= rejects it)."""
241 root = _make_repo(tmp_path)
242 _make_commit(root)
243 result = runner.invoke(cli, ["archive", "--format", "rar"], env=_env(root))
244 assert result.exit_code != 0
245
246 def test_error_prefix_traversal_to_stderr(self, tmp_path: pathlib.Path) -> None:
247 root = _make_repo(tmp_path)
248 _make_commit(root, files={"song.mid": b"data"})
249 result = runner.invoke(cli, ["archive", "--prefix", "../evil/"], env=_env(root))
250 assert result.exit_code != 0
251 # Error must NOT appear on stdout (it should be on stderr, which CliRunner merges)
252 # We verify exit code nonzero — that's the contract.
253
254 def test_error_no_commits_nonzero(self, tmp_path: pathlib.Path) -> None:
255 root = _make_repo(tmp_path)
256 result = runner.invoke(cli, ["archive"], env=_env(root))
257 assert result.exit_code != 0
258
259 def test_error_bad_ref_nonzero(self, tmp_path: pathlib.Path) -> None:
260 root = _make_repo(tmp_path)
261 _make_commit(root)
262 result = runner.invoke(cli, ["archive", "--ref", "nonexistent-branch-xyz"], env=_env(root))
263 assert result.exit_code != 0
264
265 def test_zip_slip_in_tar_manifest_skipped(self, tmp_path: pathlib.Path) -> None:
266 from muse.cli.commands.archive import _build_entries, _build_tar
267 root = _make_repo(tmp_path)
268 evil_id = _write_object(root, b"evil content")
269 safe_id = _write_object(root, b"safe content")
270 out = tmp_path / "test.tar.gz"
271 manifest = {"../../../etc/cron.d/evil": evil_id, "safe.txt": safe_id}
272 entries, _ = _build_entries(root, manifest, prefix="")
273 count = _build_tar(entries, out)
274 assert count == 1
275 with tarfile.open(out, "r:gz") as tf:
276 names = tf.getnames()
277 assert not any("etc" in n or "cron" in n for n in names)
278 assert "safe.txt" in names
279
280 def test_zip_slip_in_zip_manifest_skipped(self, tmp_path: pathlib.Path) -> None:
281 from muse.cli.commands.archive import _build_entries, _build_zip
282 root = _make_repo(tmp_path)
283 evil_id = _write_object(root, b"evil")
284 safe_id = _write_object(root, b"safe")
285 out = tmp_path / "test.zip"
286 manifest = {"../../../etc/evil": evil_id, "safe.txt": safe_id}
287 entries, _ = _build_entries(root, manifest, prefix="")
288 count = _build_zip(entries, out)
289 assert count == 1
290 with zipfile.ZipFile(out, "r") as zf:
291 names = zf.namelist()
292 assert not any("etc" in n for n in names)
293 assert "safe.txt" in names
294
295 def test_null_byte_in_manifest_path_skipped(self, tmp_path: pathlib.Path) -> None:
296 from muse.cli.commands.archive import _build_entries, _build_tar
297 root = _make_repo(tmp_path)
298 null_id = _write_object(root, b"null content")
299 safe_id = _write_object(root, b"safe content")
300 out = tmp_path / "null.tar.gz"
301 manifest = {"file\x00.txt": null_id, "safe.txt": safe_id}
302 entries, _ = _build_entries(root, manifest, prefix="")
303 count = _build_tar(entries, out)
304 assert count == 1
305
306 def test_ansi_in_commit_message_sanitized(self, tmp_path: pathlib.Path) -> None:
307 root = _make_repo(tmp_path)
308 _make_commit(root, files={"f.mid": b"data"}, message="\x1b[31mred\x1b[0m")
309 out = tmp_path / "ansi.tar.gz"
310 result = _invoke(root, "--output", str(out))
311 assert result.exit_code == 0
312 assert "\x1b" not in result.output
313
314 def test_no_muse_dir_in_archive(self, tmp_path: pathlib.Path) -> None:
315 """The .muse/ directory must never appear in any archive entry."""
316 root = _make_repo(tmp_path)
317 _make_commit(root, files={"song.mid": b"MIDI"})
318 out = tmp_path / "clean.tar.gz"
319 _invoke(root, "--output", str(out))
320 with tarfile.open(out, "r:gz") as tf:
321 names = tf.getnames()
322 assert not any(".muse" in n for n in names)
323
324
325 # ---------------------------------------------------------------------------
326 # JSON schema
327 # ---------------------------------------------------------------------------
328
329
330 class TestJsonSchema:
331 def test_json_tar_gz_schema(self, tmp_path: pathlib.Path) -> None:
332 root = _make_repo(tmp_path)
333 commit_id = _make_commit(root, files={"a.mid": b"data", "b.mid": b"more"})
334 out = tmp_path / "archive.tar.gz"
335 result = _invoke(root, "--output", str(out), "--json")
336 assert result.exit_code == 0
337 payload = _parse_json(result.output)
338 assert payload["format"] == "tar.gz"
339 assert payload["file_count"] == 2
340 assert payload["bytes"] > 0
341 assert payload["commit_id"] == commit_id
342 assert payload["branch"] == "main"
343 assert payload["ref"] is None
344 assert payload["path"] == str(out)
345
346 def test_json_zip_schema(self, tmp_path: pathlib.Path) -> None:
347 root = _make_repo(tmp_path)
348 commit_id = _make_commit(root, files={"track.mid": b"MIDI"})
349 out = tmp_path / "archive.zip"
350 result = _invoke(root, "--format", "zip", "--output", str(out), "--json")
351 assert result.exit_code == 0
352 payload = _parse_json(result.output)
353 assert payload["format"] == "zip"
354 assert payload["file_count"] == 1
355 assert payload["commit_id"] == commit_id
356
357 def test_json_ref_field_when_head(self, tmp_path: pathlib.Path) -> None:
358 root = _make_repo(tmp_path)
359 _make_commit(root, files={"f.mid": b"x"})
360 out = tmp_path / "a.tar.gz"
361 result = _invoke(root, "--output", str(out), "--json")
362 payload = _parse_json(result.output)
363 assert payload["ref"] is None
364
365 def test_json_ref_field_when_explicit_ref(self, tmp_path: pathlib.Path) -> None:
366 root = _make_repo(tmp_path)
367 commit_id = _make_commit(root, files={"f.mid": b"x"})
368 short = commit_id[:12]
369 out = tmp_path / "a.tar.gz"
370 result = _invoke(root, "--ref", short, "--output", str(out), "--json")
371 payload = _parse_json(result.output)
372 assert payload["ref"] == short
373
374 def test_json_empty_snapshot(self, tmp_path: pathlib.Path) -> None:
375 root = _make_repo(tmp_path)
376 _make_commit(root, files={})
377 out = tmp_path / "empty.tar.gz"
378 result = _invoke(root, "--output", str(out), "--json")
379 payload = _parse_json(result.output)
380 assert payload["file_count"] == 0
381
382 def test_json_message_field(self, tmp_path: pathlib.Path) -> None:
383 root = _make_repo(tmp_path)
384 _make_commit(root, files={"f.mid": b"x"}, message="release v2.0")
385 out = tmp_path / "a.tar.gz"
386 result = _invoke(root, "--output", str(out), "--json")
387 payload = _parse_json(result.output)
388 assert payload["message"] == "release v2.0"
389
390
391 # ---------------------------------------------------------------------------
392 # Integration
393 # ---------------------------------------------------------------------------
394
395
396 class TestIntegration:
397 def test_default_output_path_is_sha12_dot_format(self, tmp_path: pathlib.Path) -> None:
398 root = _make_repo(tmp_path)
399 commit_id = _make_commit(root, files={"f.mid": b"data"})
400 result = _invoke(root)
401 assert result.exit_code == 0
402 # Filename uses bare hex (colons illegal on Windows).
403 bare_short = short_id(commit_id, strip=True)
404 assert bare_short in result.output
405 assert ".tar.gz" in result.output
406
407 def test_ref_with_short_sha(self, tmp_path: pathlib.Path) -> None:
408 root = _make_repo(tmp_path)
409 commit_id = _make_commit(root, files={"a.mid": b"MIDI"})
410 out = tmp_path / "ref.tar.gz"
411 # Use the full commit_id as ref (canonical sha256: prefixed form).
412 result = _invoke(root, "--ref", commit_id, "--output", str(out))
413 assert result.exit_code == 0
414 assert out.exists()
415
416 def test_missing_object_skipped_gracefully(self, tmp_path: pathlib.Path) -> None:
417 """If an object file is missing from the store, that entry is skipped — not a crash."""
418 from muse.cli.commands.archive import _build_entries, _build_tar
419 root = _make_repo(tmp_path)
420 # Write one good object, one phantom.
421 good_id = _write_object(root, b"good content")
422 phantom_id = long_id("a" * 64)# valid format but not written to store
423 out = tmp_path / "partial.tar.gz"
424 manifest = {"good.txt": good_id, "missing.txt": phantom_id}
425 entries, _ = _build_entries(root, manifest, prefix="")
426 count = _build_tar(entries, out)
427 assert count == 1
428 with tarfile.open(out, "r:gz") as tf:
429 names = tf.getnames()
430 assert "good.txt" in names
431 assert "missing.txt" not in names
432
433 def test_archive_bytes_match_committed_content(self, tmp_path: pathlib.Path) -> None:
434 """Content extracted from the archive must match what was committed."""
435 root = _make_repo(tmp_path)
436 content = b"exact bytes for round-trip verification"
437 _make_commit(root, files={"track.mid": content})
438 out = tmp_path / "roundtrip.tar.gz"
439 _invoke(root, "--output", str(out))
440 with tarfile.open(out, "r:gz") as tf:
441 member = tf.getmembers()[0]
442 extracted = tf.extractfile(member)
443 assert extracted is not None
444 assert extracted.read() == content
445
446 def test_zip_content_round_trip(self, tmp_path: pathlib.Path) -> None:
447 root = _make_repo(tmp_path)
448 content = b"zip round trip bytes"
449 _make_commit(root, files={"data.mid": content})
450 out = tmp_path / "rt.zip"
451 _invoke(root, "--format", "zip", "--output", str(out))
452 with zipfile.ZipFile(out, "r") as zf:
453 names = zf.namelist()
454 assert len(names) == 1
455 extracted = zf.read(names[0])
456 assert extracted == content
457
458 def test_prefix_appears_in_tar_gz(self, tmp_path: pathlib.Path) -> None:
459 root = _make_repo(tmp_path)
460 _make_commit(root, files={"song.mid": b"MIDI"})
461 out = tmp_path / "prefixed.tar.gz"
462 _invoke(root, "--output", str(out), "--prefix", "band-v1.0")
463 with tarfile.open(out, "r:gz") as tf:
464 names = tf.getnames()
465 assert all(n.startswith("band-v1.0/") for n in names)
466
467 def test_prefix_appears_in_zip(self, tmp_path: pathlib.Path) -> None:
468 root = _make_repo(tmp_path)
469 _make_commit(root, files={"song.mid": b"MIDI"})
470 out = tmp_path / "prefixed.zip"
471 _invoke(root, "--format", "zip", "--output", str(out), "--prefix", "band-v2.0")
472 with zipfile.ZipFile(out, "r") as zf:
473 names = zf.namelist()
474 assert all(n.startswith("band-v2.0/") for n in names)
475
476
477 # ---------------------------------------------------------------------------
478 # E2E — full lifecycle
479 # ---------------------------------------------------------------------------
480
481
482 class TestE2E:
483 def test_full_lifecycle_tar_gz(self, tmp_path: pathlib.Path) -> None:
484 """init → commit multiple files → archive → verify all files present."""
485 root = _make_repo(tmp_path)
486 files = {
487 "tracks/track_01.mid": b"MIDI track 1",
488 "tracks/track_02.mid": b"MIDI track 2",
489 "README.txt": b"Album readme",
490 }
491 _make_commit(root, files=files)
492 out = tmp_path / "album.tar.gz"
493 result = _invoke(root, "--output", str(out))
494 assert result.exit_code == 0
495 assert out.exists()
496 with tarfile.open(out, "r:gz") as tf:
497 names = tf.getnames()
498 assert len(names) == 3
499 assert any("track_01.mid" in n for n in names)
500 assert any("track_02.mid" in n for n in names)
501 assert any("README.txt" in n for n in names)
502
503 def test_deterministic_output(self, tmp_path: pathlib.Path) -> None:
504 """Two archive calls on the same commit produce byte-identical files."""
505 root = _make_repo(tmp_path)
506 _make_commit(root, files={"a.mid": b"AAA", "b.mid": b"BBB"})
507 out1 = tmp_path / "run1.tar.gz"
508 out2 = tmp_path / "run2.tar.gz"
509 _invoke(root, "--output", str(out1))
510 _invoke(root, "--output", str(out2))
511 # gzip includes a timestamp by default, so byte equality is not guaranteed;
512 # but the member names and content must be identical.
513 with tarfile.open(out1, "r:gz") as tf1, tarfile.open(out2, "r:gz") as tf2:
514 names1 = sorted(tf1.getnames())
515 names2 = sorted(tf2.getnames())
516 assert names1 == names2
517
518 def test_historical_ref_archive(self, tmp_path: pathlib.Path) -> None:
519 """Archiving an old commit SHA produces only files from that snapshot."""
520 root = _make_repo(tmp_path)
521 first_id = _make_commit(root, files={"v1.mid": b"v1 data"})
522 _make_commit(root, files={"v1.mid": b"v1 data", "v2.mid": b"v2 data"})
523 out = tmp_path / "historical.tar.gz"
524 result = _invoke(root, "--ref", first_id[:12], "--output", str(out))
525 assert result.exit_code == 0
526 with tarfile.open(out, "r:gz") as tf:
527 names = tf.getnames()
528 assert any("v1.mid" in n for n in names)
529 assert not any("v2.mid" in n for n in names)
530
531 def test_output_text_shows_commit_short(self, tmp_path: pathlib.Path) -> None:
532 root = _make_repo(tmp_path)
533 commit_id = _make_commit(root, files={"f.mid": b"x"})
534 out = tmp_path / "out.tar.gz"
535 result = _invoke(root, "--output", str(out))
536 assert result.exit_code == 0
537 assert commit_id[:len("sha256:") + 12] in result.output
538
539 def test_output_text_shows_file_count(self, tmp_path: pathlib.Path) -> None:
540 root = _make_repo(tmp_path)
541 _make_commit(root, files={"a.mid": b"x", "b.mid": b"y", "c.mid": b"z"})
542 out = tmp_path / "out.tar.gz"
543 result = _invoke(root, "--output", str(out))
544 assert "3" in result.output
545
546
547 # ---------------------------------------------------------------------------
548 # Stress
549 # ---------------------------------------------------------------------------
550
551
552 class TestStress:
553 def test_200_file_archive(self, tmp_path: pathlib.Path) -> None:
554 root = _make_repo(tmp_path)
555 files = {f"track_{i:03d}.mid": f"MIDI content {i}".encode() for i in range(200)}
556 _make_commit(root, files=files)
557 out = tmp_path / "big.tar.gz"
558 result = _invoke(root, "--output", str(out), "--json")
559 assert result.exit_code == 0
560 payload = _parse_json(result.output)
561 assert payload["file_count"] == 200
562 with tarfile.open(out, "r:gz") as tf:
563 assert len(tf.getnames()) == 200
564
565 def test_concurrent_archives_different_repos(self, tmp_path: pathlib.Path) -> None:
566 """Concurrent archive operations on different repos must not interfere."""
567 errors: list[str] = []
568
569 def _run(idx: int) -> None:
570 repo_dir = tmp_path / f"repo_{idx}"
571 repo_dir.mkdir()
572 root = _make_repo(repo_dir)
573 _make_commit(root, files={f"track_{idx}.mid": f"content {idx}".encode()})
574 out = repo_dir / f"archive_{idx}.tar.gz"
575 try:
576 result = _invoke(root, "--output", str(out))
577 if result.exit_code != 0:
578 errors.append(f"Thread {idx} exit {result.exit_code}: {result.output[:200]}")
579 except Exception as exc:
580 errors.append(f"Thread {idx}: {exc}")
581
582 threads = [threading.Thread(target=_run, args=(i,)) for i in range(8)]
583 for t in threads:
584 t.start()
585 for t in threads:
586 t.join()
587
588 assert not errors, f"Concurrent archive failures: {errors}"
File History 2 commits
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 139 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 142 days ago