| ... | @@ -24,6 +24,7 @@ import { | ... | @@ -24,6 +24,7 @@ import { |
| 24 | resolveSpawnCommand, | 24 | resolveSpawnCommand, |
| 25 | resolveWindowsEnvironment, | 25 | resolveWindowsEnvironment, |
| 26 | SpawnExecutableResolution, | 26 | SpawnExecutableResolution, |
| | 27 | type SpawnExecutableResolver, |
| 27 | WindowsShellEnvironment, | 28 | WindowsShellEnvironment, |
| 28 | withPathDirectoryListings, | 29 | withPathDirectoryListings, |
| 29 | type WindowsShellEnvironmentReader, | 30 | type WindowsShellEnvironmentReader, |
| ... | @@ -515,7 +516,10 @@ effectIt.layer(NodeServices.layer)("resolveSpawnCommand", (it) => { | ... | @@ -515,7 +516,10 @@ effectIt.layer(NodeServices.layer)("resolveSpawnCommand", (it) => { |
| 515 | Effect.gen(function* () { | 516 | Effect.gen(function* () { |
| 516 | const command = yield* resolveSpawnCommand("node.exe", ["script.js", "hello & goodbye"], { | 517 | const command = yield* resolveSpawnCommand("node.exe", ["script.js", "hello & goodbye"], { |
| 517 | env: { PATH: "", PATHEXT: ".COM;.EXE;.BAT;.CMD" }, | 518 | env: { PATH: "", PATHEXT: ".COM;.EXE;.BAT;.CMD" }, |
| 518 | }).pipe(Effect.provideService(HostProcessPlatform, "win32")); | 519 | }).pipe( |
| | 520 | Effect.provideService(HostProcessPlatform, "win32"), |
| | 521 | Effect.provideService(CommandResolutionCache, new Map()), |
| | 522 | ); |
| 519 | | 523 | |
| 520 | expect(command).toEqual({ | 524 | expect(command).toEqual({ |
| 521 | command: "node.exe", | 525 | command: "node.exe", |
| ... | @@ -533,6 +537,7 @@ effectIt.layer(NodeServices.layer)("resolveSpawnCommand", (it) => { | ... | @@ -533,6 +537,7 @@ effectIt.layer(NodeServices.layer)("resolveSpawnCommand", (it) => { |
| 533 | { env: { PATH: "", PATHEXT: ".COM;.EXE;.BAT;.CMD" } }, | 537 | { env: { PATH: "", PATHEXT: ".COM;.EXE;.BAT;.CMD" } }, |
| 534 | ).pipe( | 538 | ).pipe( |
| 535 | Effect.provideService(HostProcessPlatform, "win32"), | 539 | Effect.provideService(HostProcessPlatform, "win32"), |
| | 540 | Effect.provideService(CommandResolutionCache, new Map()), |
| 536 | Effect.provideService( | 541 | Effect.provideService( |
| 537 | SpawnExecutableResolution, | 542 | SpawnExecutableResolution, |
| 538 | () => "C:\\Program Files\\npm & tools\\vp.cmd", | 543 | () => "C:\\Program Files\\npm & tools\\vp.cmd", |
| ... | @@ -559,6 +564,7 @@ effectIt.layer(NodeServices.layer)("resolveSpawnCommand", (it) => { | ... | @@ -559,6 +564,7 @@ effectIt.layer(NodeServices.layer)("resolveSpawnCommand", (it) => { |
| 559 | extendEnv: true, | 564 | extendEnv: true, |
| 560 | }).pipe( | 565 | }).pipe( |
| 561 | Effect.provideService(HostProcessPlatform, "win32"), | 566 | Effect.provideService(HostProcessPlatform, "win32"), |
| | 567 | Effect.provideService(CommandResolutionCache, new Map()), |
| 562 | Effect.provideService(HostProcessEnvironment, { | 568 | Effect.provideService(HostProcessEnvironment, { |
| 563 | PATH: "C:\\Users\\tester\\AppData\\Roaming\\npm", | 569 | PATH: "C:\\Users\\tester\\AppData\\Roaming\\npm", |
| 564 | PATHEXT: ".COM;.EXE;.BAT;.CMD", | 570 | PATHEXT: ".COM;.EXE;.BAT;.CMD", |
| ... | @@ -577,11 +583,58 @@ effectIt.layer(NodeServices.layer)("resolveSpawnCommand", (it) => { | ... | @@ -577,11 +583,58 @@ effectIt.layer(NodeServices.layer)("resolveSpawnCommand", (it) => { |
| 577 | }), | 583 | }), |
| 578 | ); | 584 | ); |
| 579 | | 585 | |
| | 586 | it.effect("scans PATH once per command until the search environment changes", () => |
| | 587 | Effect.gen(function* () { |
| | 588 | const scans: Array<string> = []; |
| | 589 | const scan: SpawnExecutableResolver = (name, _platform, env) => { |
| | 590 | scans.push(`${name}@${env.PATH}`); |
| | 591 | return name === "missing" ? undefined : `${env.PATH}\\${name}.exe`; |
| | 592 | }; |
| | 593 | const resolve = (command: string, path: string, resolver = scan) => |
| | 594 | resolveSpawnCommand(command, [], { env: { PATH: path, PATHEXT: ".EXE" } }).pipe( |
| | 595 | Effect.provideService(HostProcessPlatform, "win32"), |
| | 596 | Effect.provideService(SpawnExecutableResolution, resolver), |
| | 597 | ); |
| | 598 | |
| | 599 | expect((yield* resolve("git", "C:\\one")).command).toBe("C:\\one\\git.exe"); |
| | 600 | expect((yield* resolve("git", "C:\\one")).command).toBe("C:\\one\\git.exe"); |
| | 601 | // A failed spawn is how a provider reports "not installed", so a miss |
| | 602 | // must clear the moment the binary appears. |
| | 603 | yield* resolve("missing", "C:\\one"); |
| | 604 | yield* resolve("missing", "C:\\one"); |
| | 605 | expect((yield* resolve("git", "C:\\two")).command).toBe("C:\\two\\git.exe"); |
| | 606 | // Callers probe explicit paths they may have just written. |
| | 607 | yield* resolve("C:\\tools\\git.exe", "C:\\one"); |
| | 608 | yield* resolve("C:\\tools\\git.exe", "C:\\one"); |
| | 609 | expect(scans).toEqual([ |
| | 610 | "git@C:\\one", |
| | 611 | "missing@C:\\one", |
| | 612 | "missing@C:\\one", |
| | 613 | "git@C:\\two", |
| | 614 | "C:\\tools\\git.exe@C:\\one", |
| | 615 | "C:\\tools\\git.exe@C:\\one", |
| | 616 | ]); |
| | 617 | |
| | 618 | yield* TestClock.adjust("31 seconds"); |
| | 619 | yield* resolve("git", "C:\\one"); |
| | 620 | expect(scans).toHaveLength(7); |
| | 621 | |
| | 622 | // Another resolver sharing the cache gets its own answer, not the cached one. |
| | 623 | const elsewhere = yield* resolve("git", "C:\\one", () => "D:\\elsewhere\\git.exe"); |
| | 624 | expect(elsewhere.command).toBe("D:\\elsewhere\\git.exe"); |
| | 625 | expect((yield* resolve("git", "C:\\one")).command).toBe("C:\\one\\git.exe"); |
| | 626 | expect(scans).toHaveLength(7); |
| | 627 | }).pipe(Effect.provideService(CommandResolutionCache, new Map())), |
| | 628 | ); |
| | 629 | |
| 580 | it.effect("does not fall back to a shell for unresolved Windows commands", () => | 630 | it.effect("does not fall back to a shell for unresolved Windows commands", () => |
| 581 | Effect.gen(function* () { | 631 | Effect.gen(function* () { |
| 582 | const command = yield* resolveSpawnCommand("missing & calc", ["unsafe & value"], { | 632 | const command = yield* resolveSpawnCommand("missing & calc", ["unsafe & value"], { |
| 583 | env: { PATH: "", PATHEXT: ".COM;.EXE;.BAT;.CMD" }, | 633 | env: { PATH: "", PATHEXT: ".COM;.EXE;.BAT;.CMD" }, |
| 584 | }).pipe(Effect.provideService(HostProcessPlatform, "win32")); | 634 | }).pipe( |
| | 635 | Effect.provideService(HostProcessPlatform, "win32"), |
| | 636 | Effect.provideService(CommandResolutionCache, new Map()), |
| | 637 | ); |
| 585 | | 638 | |
| 586 | expect(command).toEqual({ | 639 | expect(command).toEqual({ |
| 587 | command: "missing & calc", | 640 | command: "missing & calc", |