mirror of
https://github.com/github/spec-kit.git
synced 2026-07-07 06:35:06 +08:00
fix(shared-infra): record skipped files in speckit.manifest.json
`install_shared_infra` skipped files that already existed on disk when `force=False`, but the skip branches in both the scripts loop and the templates loop only appended to `skipped_files` without calling `manifest.record_existing`. So when the function ran with a fresh manifest against an already-populated `.specify/` tree (e.g. after the manifest was deleted, corrupted, or extracted out of band), every file went down the skip path, `planned_copies` / `planned_templates` stayed empty, and `manifest.save()` wrote an empty `files` field — leaving the integration believing nothing was installed. Record every skipped file in the manifest, but only when it is not already tracked. This preserves the original hash for files that were previously recorded so `check_modified()` (used by `integration use` to decide whether a user has customized a template) keeps working correctly. Add `TestSpeckitManifestRecordsSkippedFiles` in `tests/integrations/test_integration_claude.py` covering both the fresh-skip path and the recover-after-lost-manifest path. Fixes #2107
This commit is contained in:
@@ -359,6 +359,23 @@ def install_shared_infra(
|
||||
preserved_user_files.append(rel)
|
||||
else:
|
||||
skipped_files.append(rel)
|
||||
# Record the existing-on-disk file in the manifest so a
|
||||
# fresh manifest run against an already-populated
|
||||
# ``.specify/`` tree does not silently drop it (#2107).
|
||||
# ``prior_hashes`` is the function-scope snapshot taken
|
||||
# at entry, so this membership check is O(1) and avoids
|
||||
# the repeated ``dict(self._files)`` copy that
|
||||
# ``manifest.files`` performs on every access.
|
||||
if dst_path.is_file() and rel not in prior_hashes:
|
||||
try:
|
||||
manifest.record_existing(rel)
|
||||
except (OSError, ValueError) as exc:
|
||||
# Tolerate races / permission issues / non-file
|
||||
# collisions so one weird path does not abort
|
||||
# the whole install.
|
||||
console.print(
|
||||
f"[yellow]⚠[/yellow] could not record {rel} in manifest: {exc}"
|
||||
)
|
||||
continue
|
||||
|
||||
if not _ensure_or_bucket_dir(dst_path.parent):
|
||||
@@ -383,6 +400,23 @@ def install_shared_infra(
|
||||
preserved_user_files.append(rel)
|
||||
else:
|
||||
skipped_files.append(rel)
|
||||
# Record the existing-on-disk template in the manifest so a
|
||||
# fresh manifest run against an already-populated
|
||||
# ``.specify/`` tree does not silently drop it (#2107).
|
||||
# ``prior_hashes`` is the function-scope snapshot taken at
|
||||
# entry, so this membership check is O(1) and avoids the
|
||||
# repeated ``dict(self._files)`` copy that ``manifest.files``
|
||||
# performs on every access.
|
||||
if dst.is_file() and rel not in prior_hashes:
|
||||
try:
|
||||
manifest.record_existing(rel)
|
||||
except (OSError, ValueError) as exc:
|
||||
# Tolerate races / permission issues / non-file
|
||||
# collisions so one weird path does not abort
|
||||
# the whole install.
|
||||
console.print(
|
||||
f"[yellow]⚠[/yellow] could not record {rel} in manifest: {exc}"
|
||||
)
|
||||
continue
|
||||
|
||||
content = src.read_text(encoding="utf-8")
|
||||
|
||||
Reference in New Issue
Block a user