diff --git a/src/resource-loader.ts b/src/resource-loader.ts index 7ddc3b7ee..48a07fc35 100644 --- a/src/resource-loader.ts +++ b/src/resource-loader.ts @@ -393,7 +393,7 @@ function reconcileMergedNodeModules( // Skip the gsd-pi package itself and dotfiles if (entry.name === basename(packageRoot)) continue if (entry.name.startsWith('.')) continue - try { symlinkSync(join(hoisted, entry.name), join(agentNodeModules, entry.name)); linkedCount++ } catch { /* skip individual */ } + try { symlinkSync(join(hoisted, entry.name), join(agentNodeModules, entry.name), 'junction'); linkedCount++ } catch { /* skip individual */ } } } catch (err) { console.error(`[gsd] WARN: Failed to read hoisted node_modules at ${hoisted}: ${err instanceof Error ? err.message : err}`) @@ -408,7 +408,7 @@ function reconcileMergedNodeModules( const link = join(agentNodeModules, entry.name) // Replace hoisted symlink with internal version (internal takes precedence) try { lstatSync(link); unlinkSync(link) } catch { /* didn't exist — will create below */ } - try { symlinkSync(join(internal, entry.name), link); linkedCount++ } catch { /* skip individual */ } + try { symlinkSync(join(internal, entry.name), link, 'junction'); linkedCount++ } catch { /* skip individual */ } } } catch (err) { console.error(`[gsd] WARN: Failed to read internal node_modules at ${internal}: ${err instanceof Error ? err.message : err}`) diff --git a/src/tests/node-modules-symlink.test.ts b/src/tests/node-modules-symlink.test.ts index 56a9d4a03..fcf94e96e 100644 --- a/src/tests/node-modules-symlink.test.ts +++ b/src/tests/node-modules-symlink.test.ts @@ -5,8 +5,9 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, readdirSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; +import { dirname, join } from "node:path"; import { tmpdir } from "node:os"; +import { fileURLToPath } from "node:url"; // --- Integration tests via initResources (source/monorepo path) --- @@ -284,3 +285,19 @@ test("merged node_modules marker uses fingerprint including directory entries", const fingerprint2 = `${fakePackageRoot}\n${h2}\n${i}`; assert.notEqual(fingerprint, fingerprint2, "fingerprint should change when deps change"); }); + +test("reconcileMergedNodeModules uses junction symlinks for Windows compatibility", () => { + const testDir = dirname(fileURLToPath(import.meta.url)); + const source = readFileSync(join(testDir, "..", "resource-loader.ts"), "utf-8"); + + assert.match( + source, + /symlinkSync\(join\(hoisted,\s*entry\.name\),\s*join\(agentNodeModules,\s*entry\.name\),\s*'junction'\)/, + "hoisted merged symlink must use 'junction'", + ); + assert.match( + source, + /symlinkSync\(join\(internal,\s*entry\.name\),\s*link,\s*'junction'\)/, + "internal merged symlink must use 'junction'", + ); +});