Skip to content

fix(collections): re-download a member whose same-named archive is the wrong file - #24

Draft
doodlum wants to merge 3 commits into
masterfrom
fix/laz-1286-verify-reused-archive
Draft

doodlum wants to merge 3 commits into
masterfrom
fix/laz-1286-verify-reused-archive

Conversation

@doodlum

@doodlum doodlum commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

Paused, not ready for review. Work stopped at the user's request on 2026-10-05. The fix, the regression test and a real-collection A/B are done. Still owed: a clean pnpm run verify on this head, the E2E baseline, and the independent adversarial QA (none has reviewed this head yet). See "Evidence" for exactly where each gate stopped.

Problem

During a collection install, a member that pins one exact file (a non-fuzzy version with a file hash) is satisfied by any archive in the download folder that has the expected file name. Reported on Vortex 2.7.2 and still on master 34f3def. A truncated Skyland AIO-34179-4-32-1700281282.7z (hash 5428d2c4…; the rule wants a58b879f…) was installed, failed to extract, was requeued and failed again against the same file until the user deleted it. LAZ-1286.

findDownloadByRef correctly rejects the archive on MD5. But then downloadURL → IPCDownloadAdapter.#handleStartDownload reuses it by name (AlreadyDownloaded) without checking size or hash, and doDownload adds the rule's referenceTag to it. From then on the rule resolves to that archive by its tag (testRef's tag match, and the tag fallback in findDownloadByReferenceTag, which doesn't check the hash either). After an install failure the download is marked failed, and later resumes stall on it.

Change

  • mod_management/util/archiveMatchesReference.ts (new): decides whether a download can stand in for a reference. Fuzzy references, and references without a hash, always match. Otherwise the file's size on disk must equal the reference's fileSize, and its hash must match: the recorded hash, or, when none is recorded, the file hashed once on the hash worker and recorded. A file that can't be hashed is reused, as before.
  • InstallManager.ts (+83/−4):
    • downloadURL now takes the reference it downloads for. When the adapter reuses a file by name, reuseExistingArchive checks it if it is finished or failed. On a mismatch it removes that download (file and record, remove-download with {confirmed, silent}) and downloads again without the reference, so it cannot loop.
    • downloadDependencyAsync passes the requirement on both of its downloadURL calls. downloadMatching (fuzzy members) is unchanged.
    • doDownload also checks a member already resolved to a finished, non-bundled archive, for example one tagged by an earlier version.
    • findDownloadByReferenceTag no longer returns a download whose recorded hash contradicts the reference.
  • The download adapter is unchanged. It serves every start-download caller, and only the dependency path knows which file a member pins.

Size: 3 files, +578/−4. The test is about 405 of those lines; the non-test change is about 170.

Behaviour changes

  • A same-named archive whose size on disk or hash isn't what a non-fuzzy, hash-pinned collection member wants is deleted and the member's file downloaded instead, with a warning in the log ("archive on disk is not the file the rule pins, downloading it again"). This applies to required and optional members. Before, it was installed and tagged as the member.
  • An archive reused for such a member is checked with stat first. If it has no recorded hash, it is also hashed once and the hash recorded.
  • The requeue, phase-advance and skip scans no longer treat an archive with a contradicting hash as the member's.
  • Unchanged: fuzzy members, members without a hash, bundled members, every non-collection download, extensions, and reusing a matching same-named archive with no download (LAZ-972).

Evidence

  • In the app (A/B): source development builds of base 34f3def and head e520a62 (the later head 861de87 changes only the test's timeouts), Windows 11 on ARM under x64 emulation. Real Nexus collection stardewvalley/rywj0g rev 1 "EasyQoL" (5 required members, all exact), Premium account. Member under test: NPC Map Locations 3.5.2 (fileMD5 8f548631…, 122,700 B), using its real archive truncated to 61,350 B. One run per side per scenario.

    Scenario Base Head
    Truncated copy dropped into the downloads folder (the user's case) Adopted, no download found, then reused by name and tagged with the rule's tag, install failed "archive appears to be broken", requeued against the same download; next resume downloaded nothing and stalled at 4/5 Warning logged, copy removed, real nxm download, installs; 5/5
    Archive truncated in place (record still holds the good hash) Reused by tag, failed, requeued, stuck at 4/5 Caught by size on disk, re-downloaded, installs; 5/5
    Base's leftover failed, tagged copy, resumed on the head (stalled, row 1) Removed, re-downloaded, installs; 5/5
    Intact archive, tracked (LAZ-972) Reused, no download, 5/5 Reused, no download, 5/5
    Intact archive, untracked (LAZ-972) — Adopted, hash matches, reused, no download, 5/5
  • Regression test: InstallManager.reusedArchive.test.ts, 9 tests. With the non-test change reverted (pr-preflight revert check), 7 fail; the good-copy reuse and the fuzzy exemption pass on both sides, as they should. 9/9 pass on the head.

  • Scoped suites (on fd378f3): mod_management, collections, download_management, IPCDownloadAdapter and util: 1119 passed, 1 skipped, 1 failed — InstallDriver "churn" hit its 5 s timeout under load and passes alone (31/31). Renderer typecheck and lint pass.

  • pr-preflight (no PR number, Vortex down): 0 fail, 2 warn (size; callers outside the diff, each dispositioned: the requeue/phase/skip scans and both downloadDependencyAsync sites are the intended targets; no public signature changed), revert check pass.

  • pnpm run verify: incomplete. The first run, on e520a62, failed three tests under load: this PR's own test (a 1 s waitFor, fixed in 861de87) and two in src/main (flattenState 5 s timeout, manager integration 30 s timeout) that pass when run alone. The re-run on 861de87 was stopped when work was paused.

  • E2E: not started; work was paused before it.

  • CI: the fork's checks started when this draft was opened; see the Checks tab.

Review

No independent adversarial QA has reviewed this head yet; it was due after verify.

Not covered

  • In downloadMatching's fallback for members with no mod or file id (InstallManager.ts ~5591), downloadURL(api, lookupResult, wasCanceled, referenceTag, fileName, undefined, parentCollection) passes the file name as campaign and drops the real file name. Pre-existing and unrelated; needs its own issue.
  • The ticket's side note that setDownloadHashByFile doesn't persist fileMD5: in these runs an adopted file's hash was recorded, so it didn't reproduce.
  • LAZ-1287 (truncated 7z misclassified as file-in-use), the sibling draft. Both touch InstallManager.ts; a trial merge of the two branches is clean and the mod_management suites pass on it.

🤖 Generated with Claude Code


Written with doodlebot.

doodlum and others added 3 commits October 5, 2026 12:33
…he wrong hash

A collection member that pins one exact file (a non-fuzzy version with a
file hash) was satisfied by any archive in the download folder with the
expected file name. The download adapter reuses such a file by name alone,
and doDownload then tagged it as the member, so a truncated or different
file was installed, failed and was retried against the same file until the
user deleted it.

Verify an archive before reusing it for such a member: the recorded hash,
or when none is recorded the size and then the file's hash (recorded for
next time). On a mismatch remove that download and fetch the file again.
The tag fallback in findDownloadByReferenceTag no longer resolves the
member to an archive whose recorded hash contradicts the rule. Fuzzy
members are exempt, and a matching same-named archive is still reused
without a download.

Fixes LAZ-1286

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… too

A recorded hash describes the file as it was when it was hashed, so an
archive cut short afterwards still matched. Compare the member's file size
with the size on disk first. An archive whose install failed is marked as a
failed download, which the reuse check skipped, so a resumed collection
stalled on it; verify those as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…chine

The first dependency round in the file loads the manager's lazy modules and
outlasted waitFor's one-second default under pnpm run verify.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@doodlum
doodlum requested a balanced review from Copilot October 5, 2026 22:23

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Missing archives and mismatches without a source URL can still be treated as reusable.

Review effort: Balanced
Findings: 1 High severity · 1 Medium severity

Open (2)
What changed in this PR

Adds validation to prevent collection installs from reusing incorrect same-named archives.

Changes:

  • Validates exact references using archive size and MD5.
  • Removes mismatched archives and queues replacements.
  • Adds regression coverage for reuse and re-download scenarios.
File Description
archiveMatchesReference.ts Implements archive validation.
InstallManager.ts Integrates validation into dependency downloads.
InstallManager.reusedArchive.test.ts Tests archive reuse and replacement.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +6574 to +6576
if (matches || !hasSource) {
return dep.download;
}
Comment on lines +60 to +64
async function sizeOnDisk(api: IExtensionApi, download: IDownload): Promise<number | undefined> {
return stat(archivePath(api, download)).then(
(stats) => stats.size,
() => download.size,
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants