Add post-deploy WSM conflict scanning and notification (Unit G) - #19
Merged
Merged
Conversation
…nit G)
After a Vortex deployment finishes for Witcher 3, spawn a short-lived WsmMcpClient,
run scan_conflicts, and show a dashboard notification when the set of unresolved
conflicts has changed since the last check this session. Registered via
context.api.onAsync('did-deploy', ...), matching the real, verified event contract
(async, fired via emitAndAwait) and the built-in game-witcher3 extension's own
onDidDeploy wiring - not context.api.events.on, contrary to a plausible-looking guess.
Uses a notification id distinct from game-witcher3's own "witcher3-merge" so both
extensions can coexist without colliding on the same slot.
Skips the notification while Vortex reports mod-install or dependency-install activity
in progress, per state.session.base.activity. Found and fixed a real bug during
development: @nexusmods/vortex-api's published types describe that field as
{[group: string]: string}, but the actual Vortex reducer stores a string[] per group,
and never deletes the group key on stopActivity - it leaves an empty array behind. A
naive truthiness check would have seen that leftover [] as "still active" and
permanently suppressed every future notification after the first install ever ran.
Fixed with a shape-tolerant activityEntries() helper, regression-tested against the
empty-array case.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXAuGMLB44T5Zv5o5ZzKah
…w-up)
A code-review pass surfaced 6 issues; all verified against the real code and fixed:
1. isWsmToolAcquired swallowed every fs.access error, not just ENOENT - a locked file
(AV scan, a running WSM process) would silently look identical to "nothing
installed". Now mirrors toolAcquisition.ts's pathExists exactly: re-throws anything
that isn't ENOENT.
2. notifyConflictsIfChanged committed lastNotifiedSignature before the
sendNotification/dismissNotification call actually succeeded. A failure there would
permanently suppress the real notification for that conflict set, since the next
check would see the same signature and skip. Now wrapped in try/catch, with the
signature only committed on success, and the error swallowed (never thrown) per
onAsync's contract.
3. scanWsmConflicts had no in-flight coalescing, unlike toolAcquisition.ts's
inFlightAcquisitions. Overlapping did-deploy events could run two concurrent WSM
processes and resolve out of order, feeding a stale result to
notifyConflictsIfChanged after a fresher one already landed. Added the same
single-slot coalescing pattern.
4. The post-deploy scan used mcpClient.ts's full 30s-per-request default, but this path
runs inside Vortex's own emitAndAwait('did-deploy', ...) await window - a slow WSM
process would extend Vortex's own reported deployment-completion time. Added a
tighter 15s requestTimeoutMs specific to this call site (mcpClient.ts itself
untouched - this uses its existing public per-call override).
5. checkForConflictsAfterDeploy gated on isWitcher3Active(context.api) - whichever game
is active when the async handler happens to run - rather than the deployed
profile's own game (did-deploy's own profileId argument). A user switching games
between did-deploy firing and this handler's turn coming up could cause a real
Witcher 3 deployment's scan to be silently skipped. Now resolves profileId's own
gameId via selectors.profileById and gates on that instead - time-invariant, so
immune to this race. (Verified against game-witcher3's own validateProfile as
precedent for the general profileId-driven approach, but it is not a verbatim copy:
that function still ultimately keys off the active profile, with an added
same-profile guard - not needed for this extension's narrower job of surfacing
conflicts from a deployment that genuinely happened.)
6. lastNotifiedSignature initialized to undefined instead of '', so the very first
post-deploy check of a session with zero conflicts always called dismissNotification
for an id that was never sent. Now initialized to '', matching
computeConflictSignature([])'s own value.
Also fixes a regression introduced while addressing #1: isWsmToolAcquired's new
non-ENOENT throw must stay inside checkForConflictsAfterDeploy's try/catch, not ahead
of it, or it would reject the onAsync('did-deploy', ...) handler's promise straight
into Vortex's own dispatch.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXAuGMLB44T5Zv5o5ZzKah
…low-up 2) The code-review skill invocation from earlier in this unit's work finally delivered its results (delayed by a background-agent naming collision). It had reviewed the already-fixed commit and confirmed those 6 fixes, but surfaced 7 further findings. Verified each against the real code; fixed 6 of them: - conflictScan.ts's and gating.ts's own doc comments still described conflict scanning as gated on isWitcher3Active(api) - stale since the profileId-based gating fix. Corrected both to describe the actual, deliberate exception and why. - computeConflictSignature keyed only on relativePath, so a conflict whose contributing mod set changed (e.g. a third mod starts touching an already-conflicting file) produced the same signature as before and was silently never re-notified. Now includes each conflict's sorted mod names in its signature entry. - checkForConflictsAfterDeploy spawned a full WSM process unconditionally whenever a tool was acquired, checking install-activity only afterward (inside notifyConflictsIfChanged, which would then discard the result). Exported isModOrDependencyInstallActive from conflictNotifications.ts and added a pre-check before scanning, avoiding wasted process spawns during e.g. a Collection install's deploy-per-mod bursts. The later check inside notifyConflictsIfChanged stays as defense-in-depth, since activity can start during the scan itself. - The outer selectors.profileById(...) gate sat outside checkForConflictsAfterDeploy's try/catch - a narrower version of the same class of bug just fixed for isWsmToolAcquired. Widened the try/catch to cover the entire handler body. Two findings were verified as real but left as documented, not code changes: - A TOCTOU gap between isWsmToolAcquired's check and scanWsmConflicts's own connect() - already safely contained by the existing try/catch (a caught warning, not a crash), so just documented rather than adding synchronization machinery for an already-handled race. - The dotnet build race between integration test files - already disclosed in this PR's own description as a known, pre-existing test-infra property; not restructured here since sibling units may add their own integration test files with the same pattern. 89 unit tests (up from 82), 95 total with integration. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GXAuGMLB44T5Zv5o5ZzKah
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Unit G of the Vortex extension plan: after a Vortex deployment finishes for Witcher 3,
scan for WSM script conflicts and show a dashboard notification when the unresolved
conflict set has changed since the last check this session. Builds on Unit E's
WsmMcpClient(PR #15) and Unit F's tool acquisition/env-var config (PR #17).vortex-extension/src/conflictScan.ts(new) —scanWsmConflicts(api)spawns ashort-lived
WsmMcpClientpointed at the acquired WSM exe and Witcher 3's discoveredgame directory (via
wsmEnv.ts'sWSM_<KeyName>mechanism), runsscan_conflicts,and closes the client in a
finally— matchingmcpClient.ts's documentedprocess-lifecycle policy exactly.
isWsmToolAcquired(api)is a cheap, localexistence check so a deploy before any WSM tool is acquired doesn't spawn a doomed
process. Coalesces overlapping calls onto a single in-flight scan (see "Post-review
fixes" below) and requests a tighter MCP timeout than the client's own default.
vortex-extension/src/conflictNotifications.ts(new) —notifyConflictsIfChanged(api, conflicts)takes an already-obtained scan result (deliberately, for direct unit-testability) and
shows/updates/dismisses a dashboard notification via
context.api.sendNotification.vortex-extension/src/index.ts— one additivecontext.api.onAsync('did-deploy', ...)registration inside the existing
context.once(...)block, gated on the deployedprofile's own game (see "Post-review fixes" below for why this isn't
isWitcher3Active(context.api), contrary to this unit's own original taskdescription).
This PR went through three rounds of review: an initial implementation pass, a second
pass (prompted by a coordinator-relayed code review) that found and fixed 6 real issues,
and a third pass (the
code-reviewskill invocation this unit's own instructionsrequire, whose results were delayed by an unrelated background-agent naming collision
and arrived after round 2 had already shipped) that found 7 more, 6 of which were real
and fixed — see "Post-review fixes" and "Round 3" below. All three rounds prioritized
verifying against real source over trusting a plausible-looking guess, including
catching two of my own overclaimed citations along the way.
Verification methodology — everything below is either directly run, or confirmed against real source I fetched and read (never guessed)
Per the task's own instruction to verify against real types/source rather than trust
anything secondhand (same discipline Unit E used for
IRunOptions), I fetched and readthe actual
Nexus-Mods/Vortexmonorepo source viagh apiwherever the published@nexusmods/vortex-apinpm package'slib/api.d.tstypings and docs weren't enough ontheir own. Exact citations below.
did-deployis async —onAsync, notevents.onThe task's own suggested snippet used
context.api.events.on('did-deploy', ...). That'swrong.
@nexusmods/vortex-api's bundleddocs/EVENTS.mdlistsdid-deployunder"Async" events (fired via
emitAndAwait), and its ownREADME.md#### Event hookssection gives the exact pattern:
Confirmed a second, independent way: Vortex's own built-in
game-witcher3extensionregisters its analogous handler the same way —
extensions/games/game-witcher3/src/index.ts:313:context.api.onAsync("did-deploy", onDidDeploy(context.api) as any)(commitf68defdd71c9a9a43673fe15550fdde53cd73ca2at time of fetch). This repo's owncheckForConflictsAfterDeployfollows the sameonAsynccontract documented inlib/api.d.ts: "listeners should report all errors themselves" — it never lets anerror propagate out, matching
tryRegisterWsmTool's existing catch-and-log shape.Distinct notification id, verified against the real collision risk
game-witcher3's owneventHandlers.ts(queryScriptMerge) sends its "you may need torun the script merger" notification with
id: "witcher3-merge". This unit useswitcherscriptmerger-vortex-conflictsinstead, so a user with both extensions installedsees both rather than one silently overwriting the other's notification slot.
sendNotification/INotificationshape verified directly againstlib/api.d.tssendNotification?: (notification: INotification) => stringandINotification's realfields (
id?,type: NotificationType,message,allowSuppress?,actions?) wereread directly out of the installed package's typings before writing any notification
code — not assumed from the task description's summary of
scriptmerger.ts's shape.Suppression logic — a real bug found and fixed mid-implementation
The task pointed at
state.session.base.activityfor the mod-install/dependency-installgate.
@nexusmods/vortex-api's publishedlib/api.d.tstypesISession.activityas{[group: string]: string}. That type is wrong; confirmed against the real Vortexreducer, not just the shipped
.d.ts.Fetched
src/renderer/src/reducers/session.ts(commit8c3793aed7063a43bf400d3d6919491b9897b2e2):The real runtime value is a
string[]per group, andstopActivityneverdeletes the group key — it leaves an empty array behind. My first implementation used
Boolean(activity.installing_dependencies), which would have seen that leftover[](truthy in JS) as "still active" forever after the first dependency install of the
session completed, permanently suppressing every future notification. Fixed with a
shape-tolerant
activityEntries()helper (conflictNotifications.ts) that handles boththe real array shape and the documented-but-stale string shape, plus a regression test
(
{ installing_dependencies: [] }must NOT suppress).The two specific group/activityId checks:
installing_dependenciesnon-empty — the exact guardgame-witcher3's ownqueryScriptMergeuses before its own conflict-adjacent notification:if ((state.session.base.activity?.installing_dependencies ?? []).length > 0) { return; }(eventHandlers.ts, same commit as above) — this real call site alreadyassumes the array shape, independently confirming the reducer finding. Populated by
startActivity("installing_dependencies", <modId>)inmod_management/InstallManager.ts(commit0c087e87af09873956e6b0e48a613dab416a57d7).modsgroup contains'installing'— the plainer single-mod-install case,populated by
startActivity("mods", "installing")inmod_management/InstallContext.ts(commit
f840765cddca7277725e6e4ae04dfd2f333f155c). Deliberately not "themodsgroup is non-empty": that same key is also used for
startActivity("mods", "deployment")inmod_management/index.ts(commit331157b72c0324eafd64dfdb0ecb197daf0a9bdb), andstopActivity("mods", "deployment")only fires after
emitAndAwait("did-deploy", ...)resolves — after everydid-deployhandler, including this extension's own, has run. A blanket "modsnon-empty" check would always see this extension's own deployment window as "install
in progress" and permanently suppress every notification. Unlike the
installing_dependenciescheck, this one has no direct precedent ingame-witcher3'sown code — included because the task explicitly names "mod-install" alongside
"dependency-install," flagged here as the less battle-tested of the two.
Post-review fixes
A coordinator-relayed code review (a separate pass, Angle D — language/framework
pitfalls) found 6 issues in the first draft of
conflictScan.ts/conflictNotifications.ts.All 6 were re-verified directly against the real code before fixing (not taken on
faith), and all 6 held up:
isWsmToolAcquiredswallowed everyfs.accesserror, not justENOENT—unlike
toolAcquisition.ts's ownpathExists, which it claimed to mirror. Alocked/permission-denied exe path (an antivirus scan, a concurrently running WSM
process) would look identical to "nothing installed yet" and silently skip scanning
for the rest of the session with only a misleading
'debug'log. Fixed: nowre-throws anything that isn't
ENOENT, matchingpathExistsin substance.lastNotifiedSignaturewas committed beforesendNotification/dismissNotificationactually succeeded. If that call threw (or the resultingpromise-adjacent path failed for any reason), the signature was already marked
"shown" — so the user never actually saw the notification, yet every later
did-deploywith the identical conflict set would silently skip re-attempting it forthe rest of the session. Fixed: the send/dismiss call is now wrapped in its own
try/catch inside
notifyConflictsIfChanged, the signature is only committed onsuccess, and a failure is logged and swallowed locally (never thrown — this function
is reachable from the
onAsynchandler, which must never reject). Regression-testedfor both the send-failure and dismiss-failure paths, including a test that
specifically distinguishes "failure correctly left retryable" from "failure silently
treated as if it had succeeded" (my first draft of that particular test was itself
wrong in a way that wouldn't have caught the bug — corrected before committing; see
commit history for the honest trail).
scanWsmConflictshad no in-flight coalescing, unliketoolAcquisition.ts'sinFlightAcquisitionsmap foracquireWsmTool. Overlappingdid-deployeventscould spawn two concurrent WSM processes against the same mods folder and — worse —
resolve out of order, letting a stale scan's result reach
notifyConflictsIfChangedafter a fresher one already had, showing a notification that no longer matched the
real conflict set and recording that stale signature as "already seen." Fixed: added
the same single-slot coalescing pattern
acquireWsmToolalready uses (a bare moduleslot here, not a
Map, since this extension only ever scans one thing).The post-deploy scan used
mcpClient.ts's general-purpose 30s-per-requestdefault (up to ~60s worst case across the handshake +
scan_conflictscall), butthis path runs inside Vortex's own
emitAndAwait('did-deploy', ...)await window —confirmed by reading
mod_management/index.ts:stopActivity('mods', 'deployment')doesn't fire until every
did-deployhandler resolves. A slow/hung WSM process wouldextend Vortex's own reported deployment-completion time for that long, from a hook
the user never explicitly asked to wait on. Fixed:
scanWsmConflictsnow passes atighter
requestTimeoutMs: 15_000specific to this call site, viaWsmMcpClientOptions' already-public per-call override —mcpClient.tsitself isuntouched, per this unit's own "don't touch" list.
checkForConflictsAfterDeploygated onisWitcher3Active(context.api)—whichever game is active at the moment this async handler happens to run — rather
than the deployed profile's own game (
did-deploy's ownprofileIdargument). Sincethis handler is one of potentially several
emitAndAwait('did-deploy', ...)listeners, and the user can act on Vortex's UI while earlier listeners are still
resolving, a real Witcher 3 deployment's scan could be silently skipped if the user
switched to a different game before this handler's own turn came up — a false
negative that misses genuine conflicts, not just an ordering nicety. Fixed: now
resolves
profileIdto that specific profile's owngameIdviaselectors.profileById(state, profileId)and gates on that instead — time-invariant,so immune to this race regardless of when the handler actually executes.
Deliberately deviates from this unit's own original task description ("Gate
everything on
isWitcher3Active(context.api)");isWitcher3Activeis retainedexactly as before in
tryRegisterWsmTool, where "what's active right now" genuinelyis the correct question (it's driven by
gamemode-activated, not a specificdeployment). ANDing both checks together in
checkForConflictsAfterDeploywould havereintroduced the exact false-negative this fix removes, so it isn't used there.
Honesty note on a citation I initially overstated: I first described this as "the
exact pattern
game-witcher3already uses" for its ownonDidDeploy/onWillDeploy.That's not accurate — I verified
game-witcher3's actualvalidateProfile(profileId, state)(extensions/games/game-witcher3/src/util.ts)directly, and it still ultimately keys off
selectors.activeProfile(state).gameId(the same "what's active now" read this fix moves away from), just with an added
guard that
profileIdmatches the currently-active profile's own id. That's theright call for what
game-witcher3uses it for (INI/load-order bookkeeping that onlymakes sense for the actively-displayed profile) — it is not the right call for this
extension's narrower job (telling the user about conflicts from a deployment that
genuinely happened, which is still true information even if they've since switched
games), so this PR deliberately drops that same-profile cross-check rather than
copying it verbatim. Caught and corrected this overclaim before finalizing, per the
same "verify, don't assert" standard the rest of this description holds itself to.
lastNotifiedSignatureinitialized toundefinedinstead of''(the samevalue
computeConflictSignature([])produces for "no conflicts"), so the very firstpost-deploy check of a session with zero conflicts always called
dismissNotificationfor a notification id that was never sent. Harmless against thereal Vortex API (a no-op on an unknown id) but pointless noise. Fixed: initialized to
''instead, so the first zero-conflict check matches the same-signatureearly-return.
A regression introduced while fixing #1, caught before committing: making
isWsmToolAcquiredthrow on non-ENOENTerrors meant that call needed to move insidecheckForConflictsAfterDeploy's own try/catch rather than gating ahead of it — otherwisea real
EBUSY/EPERMwould reject theonAsync('did-deploy', ...)handler's promisestraight into Vortex's own dispatch, exactly the failure mode
onAsync's contractforbids. Fixed and regression-tested (
isWsmToolAcquiredrejecting must still resolvethe handler, never throw).
Round 3: the delayed
code-reviewskill findingsThis unit's own instructions require invoking the
code-reviewskill and fixing itsfindings before opening the PR. That invocation happened early, but its background
result got stuck behind a same-name collision with other agents already active in this
session and only arrived after round 2 above had already shipped. It re-verified round
2's fixes directly against the committed code (confirmed all 6) and surfaced 7 more.
Each was independently re-checked against the real code rather than taken on faith; 6
were real and fixed, 1 (the integration-test build race) was already known and
disclosed in this description, so left as-is:
Two doc comments (
conflictScan.ts,gating.ts) still described conflictscanning as gated on
isWitcher3Active(api)- stale since round 2's profileId-basedgating fix (Split into WitcherScriptMerger.Core + host GUI/CLI/MCP project #5 above). Both corrected to describe the actual, deliberate exception.
computeConflictSignaturekeyed only onrelativePath, so a conflict whosecontributing mod set changed between two scans (e.g. a third mod starts touching an
already-conflicting file that was never merged) produced an identical signature to
before, and the user was never re-notified about a real, relevant change. Fixed:
each conflict's entry in the signature now also includes its sorted contributing mod
names (
relativePath:sortedModNames, entries newline-joined -|,:, and newlineare all part of Windows' reserved/control-character set, so none can appear in a
real file/directory name and collide two genuinely different conflict sets onto the
same signature).
checkForConflictsAfterDeployspawned a full WSM process unconditionallywhenever a tool was acquired, checking
isModOrDependencyInstallActiveonlyafterward, inside
notifyConflictsIfChanged- which would then just discard thatscan's result. During a dependency-install burst (e.g. installing a Collection
triggers several deploy-per-mod cycles), each cycle would spawn and tear down a WSM
process for a result that was thrown away. Fixed: exported
isModOrDependencyInstallActivefromconflictNotifications.tsand added apre-check before ever calling
scanWsmConflicts- a pure optimization, not acorrectness fix, since the later check inside
notifyConflictsIfChangedstays inplace as defense-in-depth (activity can start during the scan itself, after a clean
pre-check).
The outer
selectors.profileById(...)gate sat outsidecheckForConflictsAfterDeploy's try/catch - a narrower instance of the exact classof bug already fixed for
isWsmToolAcquiredin round 2 (a synchronous throw wouldreject the
onAsynchandler's promise, violating its documented "never reject"contract). Low likelihood (a plain state lookup, not a filesystem call with a real
trigger like
EBUSY), but free to close. Fixed: the try/catch now wraps the entirehandler body, including this gate.
A real TOCTOU gap between
isWsmToolAcquired's existence check andscanWsmConflictsUncoordinated's own independentconnect()call a few lines laterbut left as a documented comment, not a code change: the failure mode is already
fully contained (
WsmMcpClient.connectrejects, and the surrounding try/catchalready logs it as a warning rather than crashing), so this is an inherent
check-then-act gap already handled safely, not something worth adding
synchronization machinery for.
The
dotnet buildrace between integration test files - re-confirmed real bythe reviewer, already disclosed in this PR's own "known, pre-existing test-infra
flake" section below (written during round 2, before this round's review even ran).
No new action - left as documented for the same reasons already given there.
89 unit tests (up from 82 after round 2), 95 total with integration.
What was actually verified by running vs. by reading source
Run and green:
npm run typecheck && npm run build && npm run lint && npm test— 89 fast unit tests(
src/conflictScan.test.ts,src/conflictNotifications.test.ts, updatedsrc/index.test.ts), including every regression test named above (both rounds).npm run test:integration—test/conflictScan.integration.test.tsbuilds and spawnsthe real
WitcherScriptMerger.Headlessmcpserver against a scratch mods folder withtwo real mods both placing
conflicting.wsundercontent\scripts\, runs a realscan_conflicts, confirms it reports exactly that one genuine conflict, and feeds thereal result into
notifyConflictsIfChangedend-to-end (sends once, doesn't re-send onan identical second call). A second scenario confirms an empty mods folder produces no
notification. 95 tests total across the full suite.
notifyConflictsIfChangedagainst fabricated conflict arrays for allthree suppression scenarios named in this unit's instructions: same-signature twice →
no second notification; different signature → notifies; activity-in-progress → skips
(including the real array-shape case and the stale-empty-array trap above).
Not run — confirmed only by reading real, current source (see citations above), or
genuinely unverifiable without a live Vortex install:
allowSuppress's in-app suppressbehavior, and directly observing a real
did-deployfire (with a realprofileId)from a live Vortex + Witcher 3 deployment. Needs a manual pass with real Vortex.
checkForConflictsAfterDeploystill spawns and awaits a WSM process insideemitAndAwait('did-deploy', ...)'s await window even with the tightened 15s timeout(fix Add Vortex extension design doc (Unit 4, design only) #4 bounds the worst case, it doesn't eliminate the underlying design tradeoff).
game-witcher3's ownonDidDeploydoes async work in the same window, so this isn'ta novel problem, but a live tester should watch for it.
logic-tested against fabricated/mocked scenarios, but neither has been exercised
against a real overlapping-deployment or real-mid-deploy-game-switch scenario in a live
Vortex — both are inherently hard to trigger reliably even with a real install.
A known, pre-existing test-infra flake — not a regression
Cold-cache
npm run test:integrationcan fail: it runs three*.integration.test.tsfiles in parallel by default, and my new file is the second one that does
dotnet build -c Debuginto the exact sameWitcherScriptMerger.Core/.Headlessoutput paths
test/mcpClient.integration.test.tsalready builds into — concurrentdotnet buildprocesses racing the sameobj//bin/output can hit file locks(
CSC : error CS2012 ... file may be locked by 'Microsoft Defender Antivirus Service')or a
NETSDK1047restore-target mismatch from another file's concurrent restoreclobbering the shared
obj/project.assets.jsonmid-build. Reproduced, then confirmednon-fatal two ways:
vitest run test --no-file-parallelismpasses cleanly, and awarm-cache
npm run test:integration(build outputs already present, so every file'sown
if (!fs.existsSync(...))guard skips rebuilding) also passes cleanly in parallel.This is a pre-existing property of each integration test file independently
guarding+building with no cross-file coordination, not something this unit's own code
broke — I deliberately didn't restructure the existing files' build orchestration, since
sibling units (H/I/J) may add their own integration test files with the same pattern in
parallel branches.
AI-assisted development
This PR was substantially produced by Claude Code (Sonnet 5), per this repo's
CONTRIBUTING.mddisclosure requirement. All source-verification claims above wereconfirmed against real, current
@nexusmods/vortex-apitypings andNexus-Mods/Vortexmonorepo source fetched via
gh api, not asserted from training-data memory. This PR'shistory is a genuine example of the verification discipline
CONTRIBUTING.md'sAI-assisted-development section asks for in practice, not just in principle, across all
three rounds: the initial implementation had a real, severe bug (the
activityarray-shape issue) caught before first opening this PR; a coordinator-relayed second
review then found 6 more real issues, all independently re-verified rather than fixed on
faith (and during that pass, I caught and corrected my own overclaimed citation - fix
#5's
game-witcher3comparison - before it shipped); and a third round (this unit's ownrequired
code-reviewskill invocation, whose result was delayed by an unrelatedbackground-agent naming collision) found 6 more genuine issues after round 2 had already
shipped, again each independently re-verified rather than accepted at face value.
Flagged explicitly because it's exactly the "looks plausible, compiles, silently wrong
(or overstated)" failure mode this repo's own AI-assisted-development guidance warns
about — the fix isn't "trust the agent less," it's "the agent re-verified its own claims
before submitting," which is what happened here, three times over.
Test plan
npm run typechecknpm run buildnpm run lintnpm test(89 unit tests)npm run test:integration(95 tests total, verified both warm-cache-parallel and--no-file-parallelism)allowSuppressbehavior against alive Vortex + Witcher 3 install
did-deployactually fires (with a realprofileId) as expectedfrom a real deployment
(Research spike: WolvenKit as a QuickBMS/wcc_lite replacement #3) against real, hard-to-synthesize scenarios (a mid-deploy game switch;
overlapping deployments) if practical