| ... | ... | @@ -24,6 +24,7 @@ import { |
| 24 | 24 | resolveSpawnCommand, |
| 25 | 25 | resolveWindowsEnvironment, |
| 26 | 26 | SpawnExecutableResolution, |
| 27 | type SpawnExecutableResolver, |
| 27 | 28 | WindowsShellEnvironment, |
| 28 | 29 | withPathDirectoryListings, |
| 29 | 30 | type WindowsShellEnvironmentReader, |
| ... | ... | @@ -515,7 +516,10 @@ effectIt.layer(NodeServices.layer)("resolveSpawnCommand", (it) => { |
| 515 | 516 | Effect.gen(function* () { |
| 516 | 517 | const command = yield* resolveSpawnCommand("node.exe", ["script.js", "hello & goodbye"], { |
| 517 | 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 | 524 | expect(command).toEqual({ |
| 521 | 525 | command: "node.exe", |
| ... | ... | @@ -533,6 +537,7 @@ effectIt.layer(NodeServices.layer)("resolveSpawnCommand", (it) => { |
| 533 | 537 | { env: { PATH: "", PATHEXT: ".COM;.EXE;.BAT;.CMD" } }, |
| 534 | 538 | ).pipe( |
| 535 | 539 | Effect.provideService(HostProcessPlatform, "win32"), |
| 540 | Effect.provideService(CommandResolutionCache, new Map()), |
| 536 | 541 | Effect.provideService( |
| 537 | 542 | SpawnExecutableResolution, |
| 538 | 543 | () => "C:\\Program Files\\npm & tools\\vp.cmd", |
| ... | ... | @@ -559,6 +564,7 @@ effectIt.layer(NodeServices.layer)("resolveSpawnCommand", (it) => { |
| 559 | 564 | extendEnv: true, |
| 560 | 565 | }).pipe( |
| 561 | 566 | Effect.provideService(HostProcessPlatform, "win32"), |
| 567 | Effect.provideService(CommandResolutionCache, new Map()), |
| 562 | 568 | Effect.provideService(HostProcessEnvironment, { |
| 563 | 569 | PATH: "C:\\Users\\tester\\AppData\\Roaming\\npm", |
| 564 | 570 | PATHEXT: ".COM;.EXE;.BAT;.CMD", |
| ... | ... | @@ -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 | 630 | it.effect("does not fall back to a shell for unresolved Windows commands", () => |
| 581 | 631 | Effect.gen(function* () { |
| 582 | 632 | const command = yield* resolveSpawnCommand("missing & calc", ["unsafe & value"], { |
| 583 | 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 | 639 | expect(command).toEqual({ |
| 587 | 640 | command: "missing & calc", |