authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-04 13:33:19-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-04 20:33:19+00:00
log4701041eef6339bbda0d9d9b00a929c4e9e18662
tree89ae5f65a204ef219cc9d98277b85d168c9a5212
parentcc60753aa2279722149e182fb942ca87f70ff804
signature Signed by PGP key B5690EEEBB952194

test(server): skip posix executable fixtures on a Windows host (#9565)

Co-authored-by: Claude Code <noreply@anthropic.com>

4 files changed, 391 insertions(+), 347 deletions(-)

apps/server/src/process/externalLauncher.test.ts+314-282
...@@ -20,6 +20,12 @@ import { HostProcessPlatform } from "@t3tools/shared/hostProcess";...@@ -20,6 +20,12 @@ import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
20import { SpawnExecutableResolution } from "@t3tools/shared/shell";20import { SpawnExecutableResolution } from "@t3tools/shared/shell";
21import * as ExternalLauncher from "./externalLauncher.ts";21import * as ExternalLauncher from "./externalLauncher.ts";
2222
23// Tests below write `#!/bin/sh` stubs into a real temp dir and hand that
24// directory to a posix-mocked resolver as PATH. On a Windows host the temp
25// path carries a drive letter, so the posix `:` split shatters it; there is
26// no posix executable to find there anyway.
27const windowsHost = HostProcessPlatform.defaultValue() === "win32";
28
23interface MockSpawnResult {29interface MockSpawnResult {
24 readonly exitCode?: number;30 readonly exitCode?: number;
25 readonly stdout?: string;31 readonly stdout?: string;
...@@ -153,7 +159,7 @@ it.effect("launches an installed editor with platform-safe arguments", () =>...@@ -153,7 +159,7 @@ it.effect("launches an installed editor with platform-safe arguments", () =>
153 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),159 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
154);160);
155161
156it.effect("reveals a file in Finder with open -R on macOS", () =>162it.effect.skipIf(windowsHost)("reveals a file in Finder with open -R on macOS", () =>
157 Effect.gen(function* () {163 Effect.gen(function* () {
158 const fileSystem = yield* FileSystem.FileSystem;164 const fileSystem = yield* FileSystem.FileSystem;
159 const path = yield* Path.Path;165 const path = yield* Path.Path;
...@@ -336,196 +342,208 @@ it.effect("does not advertise reveal on Windows when PowerShell is missing", ()...@@ -336,196 +342,208 @@ it.effect("does not advertise reveal on Windows when PowerShell is missing", ()
336 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),342 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
337);343);
338344
339it.effect("reveals a WSL file in Windows File Explorer through its UNC path", () =>345it.effect.skipIf(windowsHost)(
340 Effect.gen(function* () {346 "reveals a WSL file in Windows File Explorer through its UNC path",
341 const fileSystem = yield* FileSystem.FileSystem;347 () =>
342 const path = yield* Path.Path;348 Effect.gen(function* () {
343 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });349 const fileSystem = yield* FileSystem.FileSystem;
344 for (const name of ["explorer.exe", "powershell.exe", "xdg-open"]) {350 const path = yield* Path.Path;
345 const filePath = path.join(binDir, name);351 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });
346 yield* fileSystem.writeFileString(filePath, "#!/bin/sh\n");352 for (const name of ["explorer.exe", "powershell.exe", "xdg-open"]) {
347 yield* fileSystem.chmod(filePath, 0o755);353 const filePath = path.join(binDir, name);
348 }354 yield* fileSystem.writeFileString(filePath, "#!/bin/sh\n");
355 yield* fileSystem.chmod(filePath, 0o755);
356 }
349357
350 let spawned: ChildProcess.StandardCommand | undefined;358 let spawned: ChildProcess.StandardCommand | undefined;
351 const result = yield* Effect.gen(function* () {359 const result = yield* Effect.gen(function* () {
352 const launcher = yield* ExternalLauncher.ExternalLauncher;360 const launcher = yield* ExternalLauncher.ExternalLauncher;
353 const kind = yield* launcher.resolveFileManagerRevealKind();361 const kind = yield* launcher.resolveFileManagerRevealKind();
354 const editors = yield* launcher.resolveAvailableEditors();362 const editors = yield* launcher.resolveAvailableEditors();
355 yield* launcher.launchEditor({363 yield* launcher.launchEditor({
356 editor: "file-manager",364 editor: "file-manager",
357 cwd: "/home/t3/workspace/media/clip.mp4",365 cwd: "/home/t3/workspace/media/clip.mp4",
358 reveal: true,366 reveal: true,
359 });367 });
360 return { kind, editors };368 return { kind, editors };
361 }).pipe(369 }).pipe(
362 Effect.provide(370 Effect.provide(
363 testLayer({371 testLayer({
364 platform: "linux",372 platform: "linux",
365 env: {373 env: {
366 PATH: binDir,374 PATH: binDir,
367 WSL_DISTRO_NAME: "Ubuntu-24.04",375 WSL_DISTRO_NAME: "Ubuntu-24.04",
368 WSL_INTEROP: "/run/WSL/1_interop",376 WSL_INTEROP: "/run/WSL/1_interop",
369 },377 },
370 onSpawn: (command) => {378 onSpawn: (command) => {
371 spawned = command;379 spawned = command;
372 },380 },
373 }),381 }),
374 ),382 ),
375 );383 );
376384
377 assert.equal(result.kind, "file-explorer");385 assert.equal(result.kind, "file-explorer");
378 assert.equal(result.editors.includes("file-manager"), true);386 assert.equal(result.editors.includes("file-manager"), true);
379 assert.ok(spawned);387 assert.ok(spawned);
380 // The reveal routes through interop PowerShell so Explorer receives its388 // The reveal routes through interop PowerShell so Explorer receives its
381 // raw `/select,"<path>"` switch even for spaced paths.389 // raw `/select,"<path>"` switch even for spaced paths.
382 assert.equal(spawned.command, "powershell.exe");390 assert.equal(spawned.command, "powershell.exe");
383 const encodedCommand = spawned.args[spawned.args.length - 1] ?? "";391 const encodedCommand = spawned.args[spawned.args.length - 1] ?? "";
384 const decodedCommand = Buffer.from(encodedCommand, "base64").toString("utf16le");392 const decodedCommand = Buffer.from(encodedCommand, "base64").toString("utf16le");
385 assert.equal(393 assert.equal(
386 decodedCommand,394 decodedCommand,
387 "$ProgressPreference = 'SilentlyContinue'; Start-Process 'explorer.exe' -ArgumentList ('/select,\"' + '\\\\wsl.localhost\\Ubuntu-24.04\\home\\t3\\workspace\\media\\clip.mp4' + '\"')",395 "$ProgressPreference = 'SilentlyContinue'; Start-Process 'explorer.exe' -ArgumentList ('/select,\"' + '\\\\wsl.localhost\\Ubuntu-24.04\\home\\t3\\workspace\\media\\clip.mp4' + '\"')",
388 );396 );
389 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),397 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
390);398);
391399
392it.effect("does not advertise reveal from WSL when interop PowerShell is missing", () =>400it.effect.skipIf(windowsHost)(
393 Effect.gen(function* () {401 "does not advertise reveal from WSL when interop PowerShell is missing",
394 const fileSystem = yield* FileSystem.FileSystem;402 () =>
395 const path = yield* Path.Path;403 Effect.gen(function* () {
396 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });404 const fileSystem = yield* FileSystem.FileSystem;
397 const explorerPath = path.join(binDir, "explorer.exe");405 const path = yield* Path.Path;
398 yield* fileSystem.writeFileString(explorerPath, "");406 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });
399 yield* fileSystem.chmod(explorerPath, 0o755);407 const explorerPath = path.join(binDir, "explorer.exe");
408 yield* fileSystem.writeFileString(explorerPath, "");
409 yield* fileSystem.chmod(explorerPath, 0o755);
400410
401 const result = yield* Effect.gen(function* () {411 const result = yield* Effect.gen(function* () {
402 const launcher = yield* ExternalLauncher.ExternalLauncher;412 const launcher = yield* ExternalLauncher.ExternalLauncher;
403 return {413 return {
404 kind: yield* launcher.resolveFileManagerRevealKind(),414 kind: yield* launcher.resolveFileManagerRevealKind(),
405 editors: yield* launcher.resolveAvailableEditors(),415 editors: yield* launcher.resolveAvailableEditors(),
406 };416 };
407 }).pipe(417 }).pipe(
408 Effect.provide(418 Effect.provide(
409 testLayer({419 testLayer({
410 platform: "linux",420 platform: "linux",
411 env: {421 env: {
412 PATH: binDir,422 PATH: binDir,
413 WSL_DISTRO_NAME: "Ubuntu-24.04",423 WSL_DISTRO_NAME: "Ubuntu-24.04",
414 WSL_INTEROP: "/run/WSL/1_interop",424 WSL_INTEROP: "/run/WSL/1_interop",
415 },425 },
416 }),426 }),
417 ),427 ),
418 );428 );
419429
420 assert.equal(result.editors.includes("file-manager"), true);430 assert.equal(result.editors.includes("file-manager"), true);
421 assert.isUndefined(result.kind);431 assert.isUndefined(result.kind);
422 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),432 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
423);433);
424434
425// When interop PowerShell is missing the capability advertises the Linux435// When interop PowerShell is missing the capability advertises the Linux
426// "files" kind (or nothing), so the reveal must open the Linux file manager436// "files" kind (or nothing), so the reveal must open the Linux file manager
427// the label promised even though plain open still prefers File Explorer.437// the label promised even though plain open still prefers File Explorer.
428it.effect("reveals through the Linux file manager when WSL lacks interop PowerShell", () =>438it.effect.skipIf(windowsHost)(
429 Effect.gen(function* () {439 "reveals through the Linux file manager when WSL lacks interop PowerShell",
430 const fileSystem = yield* FileSystem.FileSystem;440 () =>
431 const path = yield* Path.Path;441 Effect.gen(function* () {
432 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });442 const fileSystem = yield* FileSystem.FileSystem;
433 for (const name of ["explorer.exe", "xdg-open", "xdg-mime"]) {443 const path = yield* Path.Path;
434 const filePath = path.join(binDir, name);444 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });
435 yield* fileSystem.writeFileString(filePath, "#!/bin/sh\n");445 for (const name of ["explorer.exe", "xdg-open", "xdg-mime"]) {
436 yield* fileSystem.chmod(filePath, 0o755);446 const filePath = path.join(binDir, name);
437 }447 yield* fileSystem.writeFileString(filePath, "#!/bin/sh\n");
448 yield* fileSystem.chmod(filePath, 0o755);
449 }
438450
439 const spawnedCommands: ChildProcess.StandardCommand[] = [];451 const spawnedCommands: ChildProcess.StandardCommand[] = [];
440 const kind = yield* Effect.gen(function* () {452 const kind = yield* Effect.gen(function* () {
441 const launcher = yield* ExternalLauncher.ExternalLauncher;453 const launcher = yield* ExternalLauncher.ExternalLauncher;
442 const revealKind = yield* launcher.resolveFileManagerRevealKind();454 const revealKind = yield* launcher.resolveFileManagerRevealKind();
443 yield* launcher.launchEditor({455 yield* launcher.launchEditor({
444 editor: "file-manager",456 editor: "file-manager",
445 cwd: "/home/t3/workspace/media/clip.mp4",457 cwd: "/home/t3/workspace/media/clip.mp4",
446 reveal: true,458 reveal: true,
447 });459 });
448 return revealKind;460 return revealKind;
449 }).pipe(461 }).pipe(
450 Effect.provide(462 Effect.provide(
451 testLayer({463 testLayer({
452 platform: "linux",464 platform: "linux",
453 env: {465 env: {
454 PATH: binDir,466 PATH: binDir,
455 WSL_DISTRO_NAME: "Ubuntu-24.04",467 WSL_DISTRO_NAME: "Ubuntu-24.04",
456 WSL_INTEROP: "/run/WSL/1_interop",468 WSL_INTEROP: "/run/WSL/1_interop",
457 DISPLAY: ":0",469 DISPLAY: ":0",
458 },470 },
459 onSpawn: (command) => {471 onSpawn: (command) => {
460 spawnedCommands.push(command);472 spawnedCommands.push(command);
461 },473 },
462 spawnResult: (command) =>474 spawnResult: (command) =>
463 command.command === "xdg-mime" ? { stdout: "org.gnome.Nautilus.desktop\n" } : undefined,475 command.command === "xdg-mime"
464 }),476 ? { stdout: "org.gnome.Nautilus.desktop\n" }
465 ),477 : undefined,
466 );478 }),
479 ),
480 );
467481
468 assert.equal(kind, "files");482 assert.equal(kind, "files");
469 const launch = spawnedCommands.find((command) => command.command === "xdg-open");483 const launch = spawnedCommands.find((command) => command.command === "xdg-open");
470 assert.ok(launch);484 assert.ok(launch);
471 assert.deepEqual(launch.args, ["/home/t3/workspace/media"]);485 assert.deepEqual(launch.args, ["/home/t3/workspace/media"]);
472 assert.isUndefined(spawnedCommands.find((command) => command.command === "explorer.exe"));486 assert.isUndefined(spawnedCommands.find((command) => command.command === "explorer.exe"));
473 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),487 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
474);488);
475489
476// Interop can exist without `explorer.exe` on PATH (appendWindowsPath=false)490// Interop can exist without `explorer.exe` on PATH (appendWindowsPath=false)
477// while WSLg still provides a working Linux file manager; the host must keep491// while WSLg still provides a working Linux file manager; the host must keep
478// the Linux open/reveal path instead of losing the editor entirely.492// the Linux open/reveal path instead of losing the editor entirely.
479it.effect("falls back to the Linux file manager when WSL lacks the Explorer bridge", () =>493it.effect.skipIf(windowsHost)(
480 Effect.gen(function* () {494 "falls back to the Linux file manager when WSL lacks the Explorer bridge",
481 const fileSystem = yield* FileSystem.FileSystem;495 () =>
482 const path = yield* Path.Path;496 Effect.gen(function* () {
483 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });497 const fileSystem = yield* FileSystem.FileSystem;
484 for (const name of ["xdg-open", "xdg-mime"]) {498 const path = yield* Path.Path;
485 const filePath = path.join(binDir, name);499 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });
486 yield* fileSystem.writeFileString(filePath, "#!/bin/sh\n");500 for (const name of ["xdg-open", "xdg-mime"]) {
487 yield* fileSystem.chmod(filePath, 0o755);501 const filePath = path.join(binDir, name);
488 }502 yield* fileSystem.writeFileString(filePath, "#!/bin/sh\n");
503 yield* fileSystem.chmod(filePath, 0o755);
504 }
489505
490 const spawnedCommands: ChildProcess.StandardCommand[] = [];506 const spawnedCommands: ChildProcess.StandardCommand[] = [];
491 const result = yield* Effect.gen(function* () {507 const result = yield* Effect.gen(function* () {
492 const launcher = yield* ExternalLauncher.ExternalLauncher;508 const launcher = yield* ExternalLauncher.ExternalLauncher;
493 const editors = yield* launcher.resolveAvailableEditors();509 const editors = yield* launcher.resolveAvailableEditors();
494 const kind = yield* launcher.resolveFileManagerRevealKind();510 const kind = yield* launcher.resolveFileManagerRevealKind();
495 yield* launcher.launchEditor({511 yield* launcher.launchEditor({
496 editor: "file-manager",512 editor: "file-manager",
497 cwd: "/home/t3/workspace/media/clip.mp4",513 cwd: "/home/t3/workspace/media/clip.mp4",
498 reveal: true,514 reveal: true,
499 });515 });
500 return { editors, kind };516 return { editors, kind };
501 }).pipe(517 }).pipe(
502 Effect.provide(518 Effect.provide(
503 testLayer({519 testLayer({
504 platform: "linux",520 platform: "linux",
505 env: {521 env: {
506 PATH: binDir,522 PATH: binDir,
507 WSL_DISTRO_NAME: "Ubuntu-24.04",523 WSL_DISTRO_NAME: "Ubuntu-24.04",
508 WSL_INTEROP: "/run/WSL/1_interop",524 WSL_INTEROP: "/run/WSL/1_interop",
509 DISPLAY: ":0",525 DISPLAY: ":0",
510 },526 },
511 onSpawn: (command) => {527 onSpawn: (command) => {
512 spawnedCommands.push(command);528 spawnedCommands.push(command);
513 },529 },
514 spawnResult: (command) =>530 spawnResult: (command) =>
515 command.command === "xdg-mime" ? { stdout: "org.gnome.Nautilus.desktop\n" } : undefined,531 command.command === "xdg-mime"
516 }),532 ? { stdout: "org.gnome.Nautilus.desktop\n" }
517 ),533 : undefined,
518 );534 }),
535 ),
536 );
519537
520 assert.equal(result.editors.includes("file-manager"), true);538 assert.equal(result.editors.includes("file-manager"), true);
521 assert.equal(result.kind, "files");539 assert.equal(result.kind, "files");
522 const launch = spawnedCommands.find((command) => command.command === "xdg-open");540 const launch = spawnedCommands.find((command) => command.command === "xdg-open");
523 assert.ok(launch);541 assert.ok(launch);
524 assert.deepEqual(launch.args, ["/home/t3/workspace/media"]);542 assert.deepEqual(launch.args, ["/home/t3/workspace/media"]);
525 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),543 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
526);544);
527545
528it.effect(546it.effect.skipIf(windowsHost)(
529 "falls back to opening the containing directory for WSL paths Explorer cannot select",547 "falls back to opening the containing directory for WSL paths Explorer cannot select",
530 () =>548 () =>
531 Effect.gen(function* () {549 Effect.gen(function* () {
...@@ -570,7 +588,7 @@ it.effect(...@@ -570,7 +588,7 @@ it.effect(
570 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),588 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
571);589);
572590
573it.effect("reveals by opening the containing directory on Linux", () =>591it.effect.skipIf(windowsHost)("reveals by opening the containing directory on Linux", () =>
574 Effect.gen(function* () {592 Effect.gen(function* () {
575 const fileSystem = yield* FileSystem.FileSystem;593 const fileSystem = yield* FileSystem.FileSystem;
576 const path = yield* Path.Path;594 const path = yield* Path.Path;
...@@ -609,127 +627,137 @@ it.effect("reveals by opening the containing directory on Linux", () =>...@@ -609,127 +627,137 @@ it.effect("reveals by opening the containing directory on Linux", () =>
609 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),627 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
610);628);
611629
612it.effect("does not advertise a Linux file manager without a graphical session", () =>630it.effect.skipIf(windowsHost)(
613 Effect.gen(function* () {631 "does not advertise a Linux file manager without a graphical session",
614 const fileSystem = yield* FileSystem.FileSystem;632 () =>
615 const path = yield* Path.Path;633 Effect.gen(function* () {
616 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });634 const fileSystem = yield* FileSystem.FileSystem;
617 const xdgOpenPath = path.join(binDir, "xdg-open");635 const path = yield* Path.Path;
618 yield* fileSystem.writeFileString(xdgOpenPath, "#!/bin/sh\n");636 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });
619 yield* fileSystem.chmod(xdgOpenPath, 0o755);637 const xdgOpenPath = path.join(binDir, "xdg-open");
638 yield* fileSystem.writeFileString(xdgOpenPath, "#!/bin/sh\n");
639 yield* fileSystem.chmod(xdgOpenPath, 0o755);
620640
621 const editors = yield* Effect.gen(function* () {641 const editors = yield* Effect.gen(function* () {
622 const launcher = yield* ExternalLauncher.ExternalLauncher;642 const launcher = yield* ExternalLauncher.ExternalLauncher;
623 return yield* launcher.resolveAvailableEditors();643 return yield* launcher.resolveAvailableEditors();
624 }).pipe(Effect.provide(testLayer({ platform: "linux", env: { PATH: binDir } })));644 }).pipe(Effect.provide(testLayer({ platform: "linux", env: { PATH: binDir } })));
625645
626 assert.equal(editors.includes("file-manager"), false);646 assert.equal(editors.includes("file-manager"), false);
627 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),647 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
628);648);
629649
630it.effect("advertises a Linux file manager when a directory handler is installed", () =>650it.effect.skipIf(windowsHost)(
631 Effect.gen(function* () {651 "advertises a Linux file manager when a directory handler is installed",
632 const fileSystem = yield* FileSystem.FileSystem;652 () =>
633 const path = yield* Path.Path;653 Effect.gen(function* () {
634 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });654 const fileSystem = yield* FileSystem.FileSystem;
635 for (const name of ["xdg-open", "xdg-mime"]) {655 const path = yield* Path.Path;
636 const filePath = path.join(binDir, name);656 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });
637 yield* fileSystem.writeFileString(filePath, "#!/bin/sh\n");657 for (const name of ["xdg-open", "xdg-mime"]) {
638 yield* fileSystem.chmod(filePath, 0o755);658 const filePath = path.join(binDir, name);
639 }659 yield* fileSystem.writeFileString(filePath, "#!/bin/sh\n");
660 yield* fileSystem.chmod(filePath, 0o755);
661 }
640662
641 let probe: ChildProcess.StandardCommand | undefined;663 let probe: ChildProcess.StandardCommand | undefined;
642 const editors = yield* Effect.gen(function* () {664 const editors = yield* Effect.gen(function* () {
643 const launcher = yield* ExternalLauncher.ExternalLauncher;665 const launcher = yield* ExternalLauncher.ExternalLauncher;
644 return yield* launcher.resolveAvailableEditors();666 return yield* launcher.resolveAvailableEditors();
645 }).pipe(667 }).pipe(
646 Effect.provide(668 Effect.provide(
647 testLayer({669 testLayer({
648 platform: "linux",670 platform: "linux",
649 env: { PATH: binDir, DISPLAY: ":0" },671 env: { PATH: binDir, DISPLAY: ":0" },
650 onSpawn: (command) => {672 onSpawn: (command) => {
651 probe = command;673 probe = command;
652 },674 },
653 spawnResult: (command) =>675 spawnResult: (command) =>
654 command.command === "xdg-mime" ? { stdout: "org.gnome.Nautilus.desktop\n" } : undefined,676 command.command === "xdg-mime"
655 }),677 ? { stdout: "org.gnome.Nautilus.desktop\n" }
656 ),678 : undefined,
657 );679 }),
680 ),
681 );
658682
659 assert.equal(editors.includes("file-manager"), true);683 assert.equal(editors.includes("file-manager"), true);
660 assert.ok(probe);684 assert.ok(probe);
661 assert.equal(probe.command, "xdg-mime");685 assert.equal(probe.command, "xdg-mime");
662 assert.deepEqual(probe.args, ["query", "default", "inode/directory"]);686 assert.deepEqual(probe.args, ["query", "default", "inode/directory"]);
663 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),687 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
664);688);
665689
666// `xdg-open` with a display variable but no `inode/directory` handler exits690// `xdg-open` with a display variable but no `inode/directory` handler exits
667// nonzero after the launch has already detached: without this gate the server691// nonzero after the launch has already detached: without this gate the server
668// advertises a reveal that is a silent no-op.692// advertises a reveal that is a silent no-op.
669it.effect("does not advertise a Linux file manager without a directory handler", () =>693it.effect.skipIf(windowsHost)(
670 Effect.gen(function* () {694 "does not advertise a Linux file manager without a directory handler",
671 const fileSystem = yield* FileSystem.FileSystem;695 () =>
672 const path = yield* Path.Path;696 Effect.gen(function* () {
673 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });697 const fileSystem = yield* FileSystem.FileSystem;
674 for (const name of ["xdg-open", "xdg-mime"]) {698 const path = yield* Path.Path;
675 const filePath = path.join(binDir, name);699 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });
676 yield* fileSystem.writeFileString(filePath, "#!/bin/sh\n");700 for (const name of ["xdg-open", "xdg-mime"]) {
677 yield* fileSystem.chmod(filePath, 0o755);701 const filePath = path.join(binDir, name);
678 }702 yield* fileSystem.writeFileString(filePath, "#!/bin/sh\n");
703 yield* fileSystem.chmod(filePath, 0o755);
704 }
679705
680 const editors = yield* Effect.gen(function* () {706 const editors = yield* Effect.gen(function* () {
681 const launcher = yield* ExternalLauncher.ExternalLauncher;707 const launcher = yield* ExternalLauncher.ExternalLauncher;
682 return yield* launcher.resolveAvailableEditors();708 return yield* launcher.resolveAvailableEditors();
683 }).pipe(709 }).pipe(
684 Effect.provide(710 Effect.provide(
685 testLayer({711 testLayer({
686 platform: "linux",712 platform: "linux",
687 env: { PATH: binDir, DISPLAY: ":0" },713 env: { PATH: binDir, DISPLAY: ":0" },
688 spawnResult: (command) => (command.command === "xdg-mime" ? { stdout: "" } : undefined),714 spawnResult: (command) => (command.command === "xdg-mime" ? { stdout: "" } : undefined),
689 }),715 }),
690 ),716 ),
691 );717 );
692718
693 assert.equal(editors.includes("file-manager"), false);719 assert.equal(editors.includes("file-manager"), false);
694 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),720 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
695);721);
696722
697it.effect("does not advertise a Linux file manager when the handler query fails", () =>723it.effect.skipIf(windowsHost)(
698 Effect.gen(function* () {724 "does not advertise a Linux file manager when the handler query fails",
699 const fileSystem = yield* FileSystem.FileSystem;725 () =>
700 const path = yield* Path.Path;726 Effect.gen(function* () {
701 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });727 const fileSystem = yield* FileSystem.FileSystem;
702 for (const name of ["xdg-open", "xdg-mime"]) {728 const path = yield* Path.Path;
703 const filePath = path.join(binDir, name);729 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });
704 yield* fileSystem.writeFileString(filePath, "#!/bin/sh\n");730 for (const name of ["xdg-open", "xdg-mime"]) {
705 yield* fileSystem.chmod(filePath, 0o755);731 const filePath = path.join(binDir, name);
706 }732 yield* fileSystem.writeFileString(filePath, "#!/bin/sh\n");
733 yield* fileSystem.chmod(filePath, 0o755);
734 }
707735
708 const editors = yield* Effect.gen(function* () {736 const editors = yield* Effect.gen(function* () {
709 const launcher = yield* ExternalLauncher.ExternalLauncher;737 const launcher = yield* ExternalLauncher.ExternalLauncher;
710 return yield* launcher.resolveAvailableEditors();738 return yield* launcher.resolveAvailableEditors();
711 }).pipe(739 }).pipe(
712 Effect.provide(740 Effect.provide(
713 testLayer({741 testLayer({
714 platform: "linux",742 platform: "linux",
715 env: { PATH: binDir, DISPLAY: ":0" },743 env: { PATH: binDir, DISPLAY: ":0" },
716 spawnResult: (command) =>744 spawnResult: (command) =>
717 command.command === "xdg-mime"745 command.command === "xdg-mime"
718 ? { exitCode: 47, stdout: "org.gnome.Nautilus.desktop\n" }746 ? { exitCode: 47, stdout: "org.gnome.Nautilus.desktop\n" }
719 : undefined,747 : undefined,
720 }),748 }),
721 ),749 ),
722 );750 );
723751
724 assert.equal(editors.includes("file-manager"), false);752 assert.equal(editors.includes("file-manager"), false);
725 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),753 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
726);754);
727755
728// The handler probe carries its own timeout because the editor scan's outer756// The handler probe carries its own timeout because the editor scan's outer
729// timeout in server.getConfig degrades to an EMPTY editor list: a wedged757// timeout in server.getConfig degrades to an EMPTY editor list: a wedged
730// xdg-mime must cost only the file manager, never the other editors. Runs on758// xdg-mime must cost only the file manager, never the other editors. Runs on
731// the live clock so the probe's real timeout fires.759// the live clock so the probe's real timeout fires.
732it.live("a stalled handler probe drops only the file manager", () =>760it.live.skipIf(windowsHost)("a stalled handler probe drops only the file manager", () =>
733 Effect.gen(function* () {761 Effect.gen(function* () {
734 const fileSystem = yield* FileSystem.FileSystem;762 const fileSystem = yield* FileSystem.FileSystem;
735 const path = yield* Path.Path;763 const path = yield* Path.Path;
...@@ -758,22 +786,26 @@ it.live("a stalled handler probe drops only the file manager", () =>...@@ -758,22 +786,26 @@ it.live("a stalled handler probe drops only the file manager", () =>
758 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),786 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
759);787);
760788
761it.effect("does not advertise a Linux file manager when xdg-mime is missing", () =>789it.effect.skipIf(windowsHost)(
762 Effect.gen(function* () {790 "does not advertise a Linux file manager when xdg-mime is missing",
763 const fileSystem = yield* FileSystem.FileSystem;791 () =>
764 const path = yield* Path.Path;792 Effect.gen(function* () {
765 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });793 const fileSystem = yield* FileSystem.FileSystem;
766 const xdgOpenPath = path.join(binDir, "xdg-open");794 const path = yield* Path.Path;
767 yield* fileSystem.writeFileString(xdgOpenPath, "#!/bin/sh\n");795 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });
768 yield* fileSystem.chmod(xdgOpenPath, 0o755);796 const xdgOpenPath = path.join(binDir, "xdg-open");
797 yield* fileSystem.writeFileString(xdgOpenPath, "#!/bin/sh\n");
798 yield* fileSystem.chmod(xdgOpenPath, 0o755);
769799
770 const editors = yield* Effect.gen(function* () {800 const editors = yield* Effect.gen(function* () {
771 const launcher = yield* ExternalLauncher.ExternalLauncher;801 const launcher = yield* ExternalLauncher.ExternalLauncher;
772 return yield* launcher.resolveAvailableEditors();802 return yield* launcher.resolveAvailableEditors();
773 }).pipe(Effect.provide(testLayer({ platform: "linux", env: { PATH: binDir, DISPLAY: ":0" } })));803 }).pipe(
804 Effect.provide(testLayer({ platform: "linux", env: { PATH: binDir, DISPLAY: ":0" } })),
805 );
774806
775 assert.equal(editors.includes("file-manager"), false);807 assert.equal(editors.includes("file-manager"), false);
776 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),808 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
777);809);
778810
779it.effect("discovers editors through the service API", () =>811it.effect("discovers editors through the service API", () =>
apps/server/src/provider/AntigravityInstallation.test.ts+63-59
...@@ -681,65 +681,69 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => {...@@ -681,65 +681,69 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => {
681 }),681 }),
682 );682 );
683683
684 it.effect("honors explicit paths and reports invalid overrides without falling back", () =>684 // Real posix executables in a real temp dir, resolved by a linux-mocked
685 Effect.gen(function* () {685 // PATH walk; a Windows temp path cannot be split on `:`.
686 const fs = yield* FileSystem.FileSystem;686 it.effect.skipIf(HostProcessPlatform.defaultValue() === "win32")(
687 const path = yield* Path.Path;687 "honors explicit paths and reports invalid overrides without falling back",
688 const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-agy-path-test-" });688 () =>
689 const externalDirectory = path.join(baseDir, "external");689 Effect.gen(function* () {
690 const externalExecutable = path.join(externalDirectory, "agy_acp_server.par");690 const fs = yield* FileSystem.FileSystem;
691 const externalHarness = path.join(externalDirectory, "localharness_external");691 const path = yield* Path.Path;
692 yield* fs.makeDirectory(externalDirectory);692 const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-agy-path-test-" });
693 yield* fs.writeFileString(externalExecutable, "external server", { mode: 0o755 });693 const externalDirectory = path.join(baseDir, "external");
694 yield* fs.writeFileString(externalHarness, "external harness", { mode: 0o755 });694 const externalExecutable = path.join(externalDirectory, "agy_acp_server.par");
695 const { installation } = yield* makeHarness({695 const externalHarness = path.join(externalDirectory, "localharness_external");
696 baseDir,696 yield* fs.makeDirectory(externalDirectory);
697 path: externalDirectory,697 yield* fs.writeFileString(externalExecutable, "external server", { mode: 0o755 });
698 previous: true,698 yield* fs.writeFileString(externalHarness, "external harness", { mode: 0o755 });
699 });699 const { installation } = yield* makeHarness({
700 yield* expectPreviousRelease(installation);700 baseDir,
701 expect(yield* installation.resolve(undefined, { PATH: externalDirectory })).toMatchObject({701 path: externalDirectory,
702 source: "managed",702 previous: true,
703 version: previousVersion,703 });
704 });704 yield* expectPreviousRelease(installation);
705 expect(yield* installation.resolve(externalExecutable)).toMatchObject({705 expect(yield* installation.resolve(undefined, { PATH: externalDirectory })).toMatchObject({
706 executablePath: externalExecutable,706 source: "managed",
707 source: "override",707 version: previousVersion,
708 managedVersionDirectory: null,708 });
709 });709 expect(yield* installation.resolve(externalExecutable)).toMatchObject({
710 expect(yield* installation.resolve("agy_acp_server.par")).toMatchObject({710 executablePath: externalExecutable,
711 source: "override",711 source: "override",
712 });712 managedVersionDirectory: null,
713 yield* fs.remove(externalHarness);713 });
714 expect(yield* installation.resolve(externalExecutable).pipe(Effect.flip)).toMatchObject({714 expect(yield* installation.resolve("agy_acp_server.par")).toMatchObject({
715 operation: "resolve",715 source: "override",
716 });716 });
717 expect(717 yield* fs.remove(externalHarness);
718 yield* installation.resolve(path.join(baseDir, "missing")).pipe(Effect.flip),718 expect(yield* installation.resolve(externalExecutable).pipe(Effect.flip)).toMatchObject({
719 ).toMatchObject({719 operation: "resolve",
720 operation: "resolve",720 });
721 });721 expect(
722 yield* expectPreviousRelease(installation);722 yield* installation.resolve(path.join(baseDir, "missing")).pipe(Effect.flip),
723 yield* fs.writeFileString(externalHarness, "external harness", { mode: 0o755 });723 ).toMatchObject({
724 yield* installation.remove();724 operation: "resolve",
725 expect(yield* installation.resolve()).toMatchObject({725 });
726 source: "path",726 yield* expectPreviousRelease(installation);
727 executablePath: externalExecutable,727 yield* fs.writeFileString(externalHarness, "external harness", { mode: 0o755 });
728 });728 yield* installation.remove();
729 const isolated = yield* makeHarness({ baseDir });729 expect(yield* installation.resolve()).toMatchObject({
730 expect(yield* isolated.installation.resolve().pipe(Effect.flip)).toMatchObject({730 source: "path",
731 operation: "resolve",731 executablePath: externalExecutable,
732 });732 });
733 expect(733 const isolated = yield* makeHarness({ baseDir });
734 yield* isolated.installation.resolve(undefined, { PATH: externalDirectory }),734 expect(yield* isolated.installation.resolve().pipe(Effect.flip)).toMatchObject({
735 ).toMatchObject({735 operation: "resolve",
736 source: "path",736 });
737 executablePath: externalExecutable,737 expect(
738 });738 yield* isolated.installation.resolve(undefined, { PATH: externalDirectory }),
739 expect(739 ).toMatchObject({
740 yield* isolated.installation.resolve("agy_acp_server.par", { PATH: externalDirectory }),740 source: "path",
741 ).toMatchObject({ source: "override", executablePath: externalExecutable });741 executablePath: externalExecutable,
742 }),742 });
743 expect(
744 yield* isolated.installation.resolve("agy_acp_server.par", { PATH: externalDirectory }),
745 ).toMatchObject({ source: "override", executablePath: externalExecutable });
746 }),
743 );747 );
744748
745 it.effect("keeps leased releases available while new sessions resolve the new release", () =>749 it.effect("keeps leased releases available while new sessions resolve the new release", () =>
apps/server/src/provider/providerMaintenance.test.ts+8-4
...@@ -22,6 +22,10 @@ import {...@@ -22,6 +22,10 @@ import {
22} from "./providerMaintenance.ts";22} from "./providerMaintenance.ts";
2323
24const driver = (value: string) => ProviderDriverKind.make(value);24const driver = (value: string) => ProviderDriverKind.make(value);
25// These write `#!/bin/sh` stubs and resolve them through a darwin-mocked
26// PATH walk; a Windows temp path cannot be split on `:`.
27const windowsHost = HostProcessPlatform.defaultValue() === "win32";
28
25const makeTempDir = (name: string) =>29const makeTempDir = (name: string) =>
26 Crypto.Crypto.pipe(30 Crypto.Crypto.pipe(
27 Effect.flatMap((crypto) => crypto.randomUUIDv4),31 Effect.flatMap((crypto) => crypto.randomUUIDv4),
...@@ -199,7 +203,7 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => {...@@ -199,7 +203,7 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => {
199 });203 });
200 });204 });
201205
202 it.effect(206 it.effect.skipIf(windowsHost)(
203 "switches package-managed providers to vite-plus updates when the resolved binary lives in vite-plus global bin",207 "switches package-managed providers to vite-plus updates when the resolved binary lives in vite-plus global bin",
204 () =>208 () =>
205 Effect.gen(function* () {209 Effect.gen(function* () {
...@@ -272,7 +276,7 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => {...@@ -272,7 +276,7 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => {
272 }),276 }),
273 );277 );
274278
275 it.effect(279 it.effect.skipIf(windowsHost)(
276 "switches package-managed providers to pnpm updates when the resolved binary lives in pnpm's global bin",280 "switches package-managed providers to pnpm updates when the resolved binary lives in pnpm's global bin",
277 () =>281 () =>
278 Effect.gen(function* () {282 Effect.gen(function* () {
...@@ -332,7 +336,7 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => {...@@ -332,7 +336,7 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => {
332 });336 });
333 });337 });
334338
335 it.effect(339 it.effect.skipIf(windowsHost)(
336 "switches native-package-tool to native updates when the binary resolves through the native installer",340 "switches native-package-tool to native updates when the binary resolves through the native installer",
337 () =>341 () =>
338 Effect.gen(function* () {342 Effect.gen(function* () {
...@@ -369,7 +373,7 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => {...@@ -369,7 +373,7 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => {
369 }),373 }),
370 );374 );
371375
372 it.effect(376 it.effect.skipIf(windowsHost)(
373 "switches scoped-package-tool to native upgrades when the binary resolves through the standalone installer",377 "switches scoped-package-tool to native upgrades when the binary resolves through the standalone installer",
374 () =>378 () =>
375 Effect.gen(function* () {379 Effect.gen(function* () {
apps/server/src/resourceTelemetry/ResourceMonitorBinary.test.ts+6-2
...@@ -11,6 +11,10 @@ import * as FileSystem from "effect/FileSystem";...@@ -11,6 +11,10 @@ import * as FileSystem from "effect/FileSystem";
11import { ServerConfig } from "../config.ts";11import { ServerConfig } from "../config.ts";
12import * as ResourceMonitorBinary from "./ResourceMonitorBinary.ts";12import * as ResourceMonitorBinary from "./ResourceMonitorBinary.ts";
1313
14// The override checks POSIX exec bits on a real file under a linux platform
15// mock; NTFS never reports those bits, so the check cannot be satisfied there.
16const windowsHost = HostProcessPlatform.defaultValue() === "win32";
17
14describe("ResourceMonitorBinary", () => {18describe("ResourceMonitorBinary", () => {
15 afterEach(() => {19 afterEach(() => {
16 vi.restoreAllMocks();20 vi.restoreAllMocks();
...@@ -42,7 +46,7 @@ describe("ResourceMonitorBinary", () => {...@@ -42,7 +46,7 @@ describe("ResourceMonitorBinary", () => {
42 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),46 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
43 );47 );
4448
45 it.effect("resolves an executable override", () =>49 it.effect.skipIf(windowsHost)("resolves an executable override", () =>
46 Effect.gen(function* () {50 Effect.gen(function* () {
47 const fileSystem = yield* FileSystem.FileSystem;51 const fileSystem = yield* FileSystem.FileSystem;
48 const baseDir = yield* fileSystem.makeTempDirectoryScoped({52 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
...@@ -66,7 +70,7 @@ describe("ResourceMonitorBinary", () => {...@@ -66,7 +70,7 @@ describe("ResourceMonitorBinary", () => {
66 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),70 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
67 );71 );
6872
69 it.effect("resolves an executable override on an unsupported platform", () =>73 it.effect.skipIf(windowsHost)("resolves an executable override on an unsupported platform", () =>
70 Effect.gen(function* () {74 Effect.gen(function* () {
71 const fileSystem = yield* FileSystem.FileSystem;75 const fileSystem = yield* FileSystem.FileSystem;
72 const baseDir = yield* fileSystem.makeTempDirectoryScoped({76 const baseDir = yield* fileSystem.makeTempDirectoryScoped({