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";
2020import { SpawnExecutableResolution } from "@t3tools/shared/shell";
2121import * 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
2329interface MockSpawnResult {
2430 readonly exitCode?: number;
2531 readonly stdout?: string;
......@@ -153,7 +159,7 @@ it.effect("launches an installed editor with platform-safe arguments", () =>
153159 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
154160);
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", () =>
157163 Effect.gen(function* () {
158164 const fileSystem = yield* FileSystem.FileSystem;
159165 const path = yield* Path.Path;
......@@ -336,196 +342,208 @@ it.effect("does not advertise reveal on Windows when PowerShell is missing", ()
336342 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
337343);
338344
339it.effect("reveals a WSL file in Windows File Explorer through its UNC path", () =>
340 Effect.gen(function* () {
341 const fileSystem = yield* FileSystem.FileSystem;
342 const path = yield* Path.Path;
343 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });
344 for (const name of ["explorer.exe", "powershell.exe", "xdg-open"]) {
345 const filePath = path.join(binDir, name);
346 yield* fileSystem.writeFileString(filePath, "#!/bin/sh\n");
347 yield* fileSystem.chmod(filePath, 0o755);
348 }
345it.effect.skipIf(windowsHost)(
346 "reveals a WSL file in Windows File Explorer through its UNC path",
347 () =>
348 Effect.gen(function* () {
349 const fileSystem = yield* FileSystem.FileSystem;
350 const path = yield* Path.Path;
351 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });
352 for (const name of ["explorer.exe", "powershell.exe", "xdg-open"]) {
353 const filePath = path.join(binDir, name);
354 yield* fileSystem.writeFileString(filePath, "#!/bin/sh\n");
355 yield* fileSystem.chmod(filePath, 0o755);
356 }
349357
350 let spawned: ChildProcess.StandardCommand | undefined;
351 const result = yield* Effect.gen(function* () {
352 const launcher = yield* ExternalLauncher.ExternalLauncher;
353 const kind = yield* launcher.resolveFileManagerRevealKind();
354 const editors = yield* launcher.resolveAvailableEditors();
355 yield* launcher.launchEditor({
356 editor: "file-manager",
357 cwd: "/home/t3/workspace/media/clip.mp4",
358 reveal: true,
359 });
360 return { kind, editors };
361 }).pipe(
362 Effect.provide(
363 testLayer({
364 platform: "linux",
365 env: {
366 PATH: binDir,
367 WSL_DISTRO_NAME: "Ubuntu-24.04",
368 WSL_INTEROP: "/run/WSL/1_interop",
369 },
370 onSpawn: (command) => {
371 spawned = command;
372 },
373 }),
374 ),
375 );
358 let spawned: ChildProcess.StandardCommand | undefined;
359 const result = yield* Effect.gen(function* () {
360 const launcher = yield* ExternalLauncher.ExternalLauncher;
361 const kind = yield* launcher.resolveFileManagerRevealKind();
362 const editors = yield* launcher.resolveAvailableEditors();
363 yield* launcher.launchEditor({
364 editor: "file-manager",
365 cwd: "/home/t3/workspace/media/clip.mp4",
366 reveal: true,
367 });
368 return { kind, editors };
369 }).pipe(
370 Effect.provide(
371 testLayer({
372 platform: "linux",
373 env: {
374 PATH: binDir,
375 WSL_DISTRO_NAME: "Ubuntu-24.04",
376 WSL_INTEROP: "/run/WSL/1_interop",
377 },
378 onSpawn: (command) => {
379 spawned = command;
380 },
381 }),
382 ),
383 );
376384
377 assert.equal(result.kind, "file-explorer");
378 assert.equal(result.editors.includes("file-manager"), true);
379 assert.ok(spawned);
380 // The reveal routes through interop PowerShell so Explorer receives its
381 // raw `/select,"<path>"` switch even for spaced paths.
382 assert.equal(spawned.command, "powershell.exe");
383 const encodedCommand = spawned.args[spawned.args.length - 1] ?? "";
384 const decodedCommand = Buffer.from(encodedCommand, "base64").toString("utf16le");
385 assert.equal(
386 decodedCommand,
387 "$ProgressPreference = 'SilentlyContinue'; Start-Process 'explorer.exe' -ArgumentList ('/select,\"' + '\\\\wsl.localhost\\Ubuntu-24.04\\home\\t3\\workspace\\media\\clip.mp4' + '\"')",
388 );
389 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
385 assert.equal(result.kind, "file-explorer");
386 assert.equal(result.editors.includes("file-manager"), true);
387 assert.ok(spawned);
388 // The reveal routes through interop PowerShell so Explorer receives its
389 // raw `/select,"<path>"` switch even for spaced paths.
390 assert.equal(spawned.command, "powershell.exe");
391 const encodedCommand = spawned.args[spawned.args.length - 1] ?? "";
392 const decodedCommand = Buffer.from(encodedCommand, "base64").toString("utf16le");
393 assert.equal(
394 decodedCommand,
395 "$ProgressPreference = 'SilentlyContinue'; Start-Process 'explorer.exe' -ArgumentList ('/select,\"' + '\\\\wsl.localhost\\Ubuntu-24.04\\home\\t3\\workspace\\media\\clip.mp4' + '\"')",
396 );
397 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
390398);
391399
392it.effect("does not advertise reveal from WSL when interop PowerShell is missing", () =>
393 Effect.gen(function* () {
394 const fileSystem = yield* FileSystem.FileSystem;
395 const path = yield* Path.Path;
396 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });
397 const explorerPath = path.join(binDir, "explorer.exe");
398 yield* fileSystem.writeFileString(explorerPath, "");
399 yield* fileSystem.chmod(explorerPath, 0o755);
400it.effect.skipIf(windowsHost)(
401 "does not advertise reveal from WSL when interop PowerShell is missing",
402 () =>
403 Effect.gen(function* () {
404 const fileSystem = yield* FileSystem.FileSystem;
405 const path = yield* Path.Path;
406 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });
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* () {
402 const launcher = yield* ExternalLauncher.ExternalLauncher;
403 return {
404 kind: yield* launcher.resolveFileManagerRevealKind(),
405 editors: yield* launcher.resolveAvailableEditors(),
406 };
407 }).pipe(
408 Effect.provide(
409 testLayer({
410 platform: "linux",
411 env: {
412 PATH: binDir,
413 WSL_DISTRO_NAME: "Ubuntu-24.04",
414 WSL_INTEROP: "/run/WSL/1_interop",
415 },
416 }),
417 ),
418 );
411 const result = yield* Effect.gen(function* () {
412 const launcher = yield* ExternalLauncher.ExternalLauncher;
413 return {
414 kind: yield* launcher.resolveFileManagerRevealKind(),
415 editors: yield* launcher.resolveAvailableEditors(),
416 };
417 }).pipe(
418 Effect.provide(
419 testLayer({
420 platform: "linux",
421 env: {
422 PATH: binDir,
423 WSL_DISTRO_NAME: "Ubuntu-24.04",
424 WSL_INTEROP: "/run/WSL/1_interop",
425 },
426 }),
427 ),
428 );
419429
420 assert.equal(result.editors.includes("file-manager"), true);
421 assert.isUndefined(result.kind);
422 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
430 assert.equal(result.editors.includes("file-manager"), true);
431 assert.isUndefined(result.kind);
432 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
423433);
424434
425435// When interop PowerShell is missing the capability advertises the Linux
426436// "files" kind (or nothing), so the reveal must open the Linux file manager
427437// the label promised even though plain open still prefers File Explorer.
428it.effect("reveals through the Linux file manager when WSL lacks interop PowerShell", () =>
429 Effect.gen(function* () {
430 const fileSystem = yield* FileSystem.FileSystem;
431 const path = yield* Path.Path;
432 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });
433 for (const name of ["explorer.exe", "xdg-open", "xdg-mime"]) {
434 const filePath = path.join(binDir, name);
435 yield* fileSystem.writeFileString(filePath, "#!/bin/sh\n");
436 yield* fileSystem.chmod(filePath, 0o755);
437 }
438it.effect.skipIf(windowsHost)(
439 "reveals through the Linux file manager when WSL lacks interop PowerShell",
440 () =>
441 Effect.gen(function* () {
442 const fileSystem = yield* FileSystem.FileSystem;
443 const path = yield* Path.Path;
444 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });
445 for (const name of ["explorer.exe", "xdg-open", "xdg-mime"]) {
446 const filePath = path.join(binDir, name);
447 yield* fileSystem.writeFileString(filePath, "#!/bin/sh\n");
448 yield* fileSystem.chmod(filePath, 0o755);
449 }
438450
439 const spawnedCommands: ChildProcess.StandardCommand[] = [];
440 const kind = yield* Effect.gen(function* () {
441 const launcher = yield* ExternalLauncher.ExternalLauncher;
442 const revealKind = yield* launcher.resolveFileManagerRevealKind();
443 yield* launcher.launchEditor({
444 editor: "file-manager",
445 cwd: "/home/t3/workspace/media/clip.mp4",
446 reveal: true,
447 });
448 return revealKind;
449 }).pipe(
450 Effect.provide(
451 testLayer({
452 platform: "linux",
453 env: {
454 PATH: binDir,
455 WSL_DISTRO_NAME: "Ubuntu-24.04",
456 WSL_INTEROP: "/run/WSL/1_interop",
457 DISPLAY: ":0",
458 },
459 onSpawn: (command) => {
460 spawnedCommands.push(command);
461 },
462 spawnResult: (command) =>
463 command.command === "xdg-mime" ? { stdout: "org.gnome.Nautilus.desktop\n" } : undefined,
464 }),
465 ),
466 );
451 const spawnedCommands: ChildProcess.StandardCommand[] = [];
452 const kind = yield* Effect.gen(function* () {
453 const launcher = yield* ExternalLauncher.ExternalLauncher;
454 const revealKind = yield* launcher.resolveFileManagerRevealKind();
455 yield* launcher.launchEditor({
456 editor: "file-manager",
457 cwd: "/home/t3/workspace/media/clip.mp4",
458 reveal: true,
459 });
460 return revealKind;
461 }).pipe(
462 Effect.provide(
463 testLayer({
464 platform: "linux",
465 env: {
466 PATH: binDir,
467 WSL_DISTRO_NAME: "Ubuntu-24.04",
468 WSL_INTEROP: "/run/WSL/1_interop",
469 DISPLAY: ":0",
470 },
471 onSpawn: (command) => {
472 spawnedCommands.push(command);
473 },
474 spawnResult: (command) =>
475 command.command === "xdg-mime"
476 ? { stdout: "org.gnome.Nautilus.desktop\n" }
477 : undefined,
478 }),
479 ),
480 );
467481
468 assert.equal(kind, "files");
469 const launch = spawnedCommands.find((command) => command.command === "xdg-open");
470 assert.ok(launch);
471 assert.deepEqual(launch.args, ["/home/t3/workspace/media"]);
472 assert.isUndefined(spawnedCommands.find((command) => command.command === "explorer.exe"));
473 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
482 assert.equal(kind, "files");
483 const launch = spawnedCommands.find((command) => command.command === "xdg-open");
484 assert.ok(launch);
485 assert.deepEqual(launch.args, ["/home/t3/workspace/media"]);
486 assert.isUndefined(spawnedCommands.find((command) => command.command === "explorer.exe"));
487 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
474488);
475489
476490// Interop can exist without `explorer.exe` on PATH (appendWindowsPath=false)
477491// while WSLg still provides a working Linux file manager; the host must keep
478492// 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", () =>
480 Effect.gen(function* () {
481 const fileSystem = yield* FileSystem.FileSystem;
482 const path = yield* Path.Path;
483 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });
484 for (const name of ["xdg-open", "xdg-mime"]) {
485 const filePath = path.join(binDir, name);
486 yield* fileSystem.writeFileString(filePath, "#!/bin/sh\n");
487 yield* fileSystem.chmod(filePath, 0o755);
488 }
493it.effect.skipIf(windowsHost)(
494 "falls back to the Linux file manager when WSL lacks the Explorer bridge",
495 () =>
496 Effect.gen(function* () {
497 const fileSystem = yield* FileSystem.FileSystem;
498 const path = yield* Path.Path;
499 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });
500 for (const name of ["xdg-open", "xdg-mime"]) {
501 const filePath = path.join(binDir, name);
502 yield* fileSystem.writeFileString(filePath, "#!/bin/sh\n");
503 yield* fileSystem.chmod(filePath, 0o755);
504 }
489505
490 const spawnedCommands: ChildProcess.StandardCommand[] = [];
491 const result = yield* Effect.gen(function* () {
492 const launcher = yield* ExternalLauncher.ExternalLauncher;
493 const editors = yield* launcher.resolveAvailableEditors();
494 const kind = yield* launcher.resolveFileManagerRevealKind();
495 yield* launcher.launchEditor({
496 editor: "file-manager",
497 cwd: "/home/t3/workspace/media/clip.mp4",
498 reveal: true,
499 });
500 return { editors, kind };
501 }).pipe(
502 Effect.provide(
503 testLayer({
504 platform: "linux",
505 env: {
506 PATH: binDir,
507 WSL_DISTRO_NAME: "Ubuntu-24.04",
508 WSL_INTEROP: "/run/WSL/1_interop",
509 DISPLAY: ":0",
510 },
511 onSpawn: (command) => {
512 spawnedCommands.push(command);
513 },
514 spawnResult: (command) =>
515 command.command === "xdg-mime" ? { stdout: "org.gnome.Nautilus.desktop\n" } : undefined,
516 }),
517 ),
518 );
506 const spawnedCommands: ChildProcess.StandardCommand[] = [];
507 const result = yield* Effect.gen(function* () {
508 const launcher = yield* ExternalLauncher.ExternalLauncher;
509 const editors = yield* launcher.resolveAvailableEditors();
510 const kind = yield* launcher.resolveFileManagerRevealKind();
511 yield* launcher.launchEditor({
512 editor: "file-manager",
513 cwd: "/home/t3/workspace/media/clip.mp4",
514 reveal: true,
515 });
516 return { editors, kind };
517 }).pipe(
518 Effect.provide(
519 testLayer({
520 platform: "linux",
521 env: {
522 PATH: binDir,
523 WSL_DISTRO_NAME: "Ubuntu-24.04",
524 WSL_INTEROP: "/run/WSL/1_interop",
525 DISPLAY: ":0",
526 },
527 onSpawn: (command) => {
528 spawnedCommands.push(command);
529 },
530 spawnResult: (command) =>
531 command.command === "xdg-mime"
532 ? { stdout: "org.gnome.Nautilus.desktop\n" }
533 : undefined,
534 }),
535 ),
536 );
519537
520 assert.equal(result.editors.includes("file-manager"), true);
521 assert.equal(result.kind, "files");
522 const launch = spawnedCommands.find((command) => command.command === "xdg-open");
523 assert.ok(launch);
524 assert.deepEqual(launch.args, ["/home/t3/workspace/media"]);
525 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
538 assert.equal(result.editors.includes("file-manager"), true);
539 assert.equal(result.kind, "files");
540 const launch = spawnedCommands.find((command) => command.command === "xdg-open");
541 assert.ok(launch);
542 assert.deepEqual(launch.args, ["/home/t3/workspace/media"]);
543 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
526544);
527545
528it.effect(
546it.effect.skipIf(windowsHost)(
529547 "falls back to opening the containing directory for WSL paths Explorer cannot select",
530548 () =>
531549 Effect.gen(function* () {
......@@ -570,7 +588,7 @@ it.effect(
570588 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
571589);
572590
573it.effect("reveals by opening the containing directory on Linux", () =>
591it.effect.skipIf(windowsHost)("reveals by opening the containing directory on Linux", () =>
574592 Effect.gen(function* () {
575593 const fileSystem = yield* FileSystem.FileSystem;
576594 const path = yield* Path.Path;
......@@ -609,127 +627,137 @@ it.effect("reveals by opening the containing directory on Linux", () =>
609627 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
610628);
611629
612it.effect("does not advertise a Linux file manager without a graphical session", () =>
613 Effect.gen(function* () {
614 const fileSystem = yield* FileSystem.FileSystem;
615 const path = yield* Path.Path;
616 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });
617 const xdgOpenPath = path.join(binDir, "xdg-open");
618 yield* fileSystem.writeFileString(xdgOpenPath, "#!/bin/sh\n");
619 yield* fileSystem.chmod(xdgOpenPath, 0o755);
630it.effect.skipIf(windowsHost)(
631 "does not advertise a Linux file manager without a graphical session",
632 () =>
633 Effect.gen(function* () {
634 const fileSystem = yield* FileSystem.FileSystem;
635 const path = yield* Path.Path;
636 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });
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* () {
622 const launcher = yield* ExternalLauncher.ExternalLauncher;
623 return yield* launcher.resolveAvailableEditors();
624 }).pipe(Effect.provide(testLayer({ platform: "linux", env: { PATH: binDir } })));
641 const editors = yield* Effect.gen(function* () {
642 const launcher = yield* ExternalLauncher.ExternalLauncher;
643 return yield* launcher.resolveAvailableEditors();
644 }).pipe(Effect.provide(testLayer({ platform: "linux", env: { PATH: binDir } })));
625645
626 assert.equal(editors.includes("file-manager"), false);
627 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
646 assert.equal(editors.includes("file-manager"), false);
647 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
628648);
629649
630it.effect("advertises a Linux file manager when a directory handler is installed", () =>
631 Effect.gen(function* () {
632 const fileSystem = yield* FileSystem.FileSystem;
633 const path = yield* Path.Path;
634 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });
635 for (const name of ["xdg-open", "xdg-mime"]) {
636 const filePath = path.join(binDir, name);
637 yield* fileSystem.writeFileString(filePath, "#!/bin/sh\n");
638 yield* fileSystem.chmod(filePath, 0o755);
639 }
650it.effect.skipIf(windowsHost)(
651 "advertises a Linux file manager when a directory handler is installed",
652 () =>
653 Effect.gen(function* () {
654 const fileSystem = yield* FileSystem.FileSystem;
655 const path = yield* Path.Path;
656 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });
657 for (const name of ["xdg-open", "xdg-mime"]) {
658 const filePath = path.join(binDir, name);
659 yield* fileSystem.writeFileString(filePath, "#!/bin/sh\n");
660 yield* fileSystem.chmod(filePath, 0o755);
661 }
640662
641 let probe: ChildProcess.StandardCommand | undefined;
642 const editors = yield* Effect.gen(function* () {
643 const launcher = yield* ExternalLauncher.ExternalLauncher;
644 return yield* launcher.resolveAvailableEditors();
645 }).pipe(
646 Effect.provide(
647 testLayer({
648 platform: "linux",
649 env: { PATH: binDir, DISPLAY: ":0" },
650 onSpawn: (command) => {
651 probe = command;
652 },
653 spawnResult: (command) =>
654 command.command === "xdg-mime" ? { stdout: "org.gnome.Nautilus.desktop\n" } : undefined,
655 }),
656 ),
657 );
663 let probe: ChildProcess.StandardCommand | undefined;
664 const editors = yield* Effect.gen(function* () {
665 const launcher = yield* ExternalLauncher.ExternalLauncher;
666 return yield* launcher.resolveAvailableEditors();
667 }).pipe(
668 Effect.provide(
669 testLayer({
670 platform: "linux",
671 env: { PATH: binDir, DISPLAY: ":0" },
672 onSpawn: (command) => {
673 probe = command;
674 },
675 spawnResult: (command) =>
676 command.command === "xdg-mime"
677 ? { stdout: "org.gnome.Nautilus.desktop\n" }
678 : undefined,
679 }),
680 ),
681 );
658682
659 assert.equal(editors.includes("file-manager"), true);
660 assert.ok(probe);
661 assert.equal(probe.command, "xdg-mime");
662 assert.deepEqual(probe.args, ["query", "default", "inode/directory"]);
663 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
683 assert.equal(editors.includes("file-manager"), true);
684 assert.ok(probe);
685 assert.equal(probe.command, "xdg-mime");
686 assert.deepEqual(probe.args, ["query", "default", "inode/directory"]);
687 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
664688);
665689
666690// `xdg-open` with a display variable but no `inode/directory` handler exits
667691// nonzero after the launch has already detached: without this gate the server
668692// advertises a reveal that is a silent no-op.
669it.effect("does not advertise a Linux file manager without a directory handler", () =>
670 Effect.gen(function* () {
671 const fileSystem = yield* FileSystem.FileSystem;
672 const path = yield* Path.Path;
673 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });
674 for (const name of ["xdg-open", "xdg-mime"]) {
675 const filePath = path.join(binDir, name);
676 yield* fileSystem.writeFileString(filePath, "#!/bin/sh\n");
677 yield* fileSystem.chmod(filePath, 0o755);
678 }
693it.effect.skipIf(windowsHost)(
694 "does not advertise a Linux file manager without a directory handler",
695 () =>
696 Effect.gen(function* () {
697 const fileSystem = yield* FileSystem.FileSystem;
698 const path = yield* Path.Path;
699 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });
700 for (const name of ["xdg-open", "xdg-mime"]) {
701 const filePath = path.join(binDir, name);
702 yield* fileSystem.writeFileString(filePath, "#!/bin/sh\n");
703 yield* fileSystem.chmod(filePath, 0o755);
704 }
679705
680 const editors = yield* Effect.gen(function* () {
681 const launcher = yield* ExternalLauncher.ExternalLauncher;
682 return yield* launcher.resolveAvailableEditors();
683 }).pipe(
684 Effect.provide(
685 testLayer({
686 platform: "linux",
687 env: { PATH: binDir, DISPLAY: ":0" },
688 spawnResult: (command) => (command.command === "xdg-mime" ? { stdout: "" } : undefined),
689 }),
690 ),
691 );
706 const editors = yield* Effect.gen(function* () {
707 const launcher = yield* ExternalLauncher.ExternalLauncher;
708 return yield* launcher.resolveAvailableEditors();
709 }).pipe(
710 Effect.provide(
711 testLayer({
712 platform: "linux",
713 env: { PATH: binDir, DISPLAY: ":0" },
714 spawnResult: (command) => (command.command === "xdg-mime" ? { stdout: "" } : undefined),
715 }),
716 ),
717 );
692718
693 assert.equal(editors.includes("file-manager"), false);
694 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
719 assert.equal(editors.includes("file-manager"), false);
720 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
695721);
696722
697it.effect("does not advertise a Linux file manager when the handler query fails", () =>
698 Effect.gen(function* () {
699 const fileSystem = yield* FileSystem.FileSystem;
700 const path = yield* Path.Path;
701 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });
702 for (const name of ["xdg-open", "xdg-mime"]) {
703 const filePath = path.join(binDir, name);
704 yield* fileSystem.writeFileString(filePath, "#!/bin/sh\n");
705 yield* fileSystem.chmod(filePath, 0o755);
706 }
723it.effect.skipIf(windowsHost)(
724 "does not advertise a Linux file manager when the handler query fails",
725 () =>
726 Effect.gen(function* () {
727 const fileSystem = yield* FileSystem.FileSystem;
728 const path = yield* Path.Path;
729 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });
730 for (const name of ["xdg-open", "xdg-mime"]) {
731 const filePath = path.join(binDir, name);
732 yield* fileSystem.writeFileString(filePath, "#!/bin/sh\n");
733 yield* fileSystem.chmod(filePath, 0o755);
734 }
707735
708 const editors = yield* Effect.gen(function* () {
709 const launcher = yield* ExternalLauncher.ExternalLauncher;
710 return yield* launcher.resolveAvailableEditors();
711 }).pipe(
712 Effect.provide(
713 testLayer({
714 platform: "linux",
715 env: { PATH: binDir, DISPLAY: ":0" },
716 spawnResult: (command) =>
717 command.command === "xdg-mime"
718 ? { exitCode: 47, stdout: "org.gnome.Nautilus.desktop\n" }
719 : undefined,
720 }),
721 ),
722 );
736 const editors = yield* Effect.gen(function* () {
737 const launcher = yield* ExternalLauncher.ExternalLauncher;
738 return yield* launcher.resolveAvailableEditors();
739 }).pipe(
740 Effect.provide(
741 testLayer({
742 platform: "linux",
743 env: { PATH: binDir, DISPLAY: ":0" },
744 spawnResult: (command) =>
745 command.command === "xdg-mime"
746 ? { exitCode: 47, stdout: "org.gnome.Nautilus.desktop\n" }
747 : undefined,
748 }),
749 ),
750 );
723751
724 assert.equal(editors.includes("file-manager"), false);
725 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
752 assert.equal(editors.includes("file-manager"), false);
753 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
726754);
727755
728756// The handler probe carries its own timeout because the editor scan's outer
729757// timeout in server.getConfig degrades to an EMPTY editor list: a wedged
730758// xdg-mime must cost only the file manager, never the other editors. Runs on
731759// 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", () =>
733761 Effect.gen(function* () {
734762 const fileSystem = yield* FileSystem.FileSystem;
735763 const path = yield* Path.Path;
......@@ -758,22 +786,26 @@ it.live("a stalled handler probe drops only the file manager", () =>
758786 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
759787);
760788
761it.effect("does not advertise a Linux file manager when xdg-mime is missing", () =>
762 Effect.gen(function* () {
763 const fileSystem = yield* FileSystem.FileSystem;
764 const path = yield* Path.Path;
765 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });
766 const xdgOpenPath = path.join(binDir, "xdg-open");
767 yield* fileSystem.writeFileString(xdgOpenPath, "#!/bin/sh\n");
768 yield* fileSystem.chmod(xdgOpenPath, 0o755);
789it.effect.skipIf(windowsHost)(
790 "does not advertise a Linux file manager when xdg-mime is missing",
791 () =>
792 Effect.gen(function* () {
793 const fileSystem = yield* FileSystem.FileSystem;
794 const path = yield* Path.Path;
795 const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" });
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* () {
771 const launcher = yield* ExternalLauncher.ExternalLauncher;
772 return yield* launcher.resolveAvailableEditors();
773 }).pipe(Effect.provide(testLayer({ platform: "linux", env: { PATH: binDir, DISPLAY: ":0" } })));
800 const editors = yield* Effect.gen(function* () {
801 const launcher = yield* ExternalLauncher.ExternalLauncher;
802 return yield* launcher.resolveAvailableEditors();
803 }).pipe(
804 Effect.provide(testLayer({ platform: "linux", env: { PATH: binDir, DISPLAY: ":0" } })),
805 );
774806
775 assert.equal(editors.includes("file-manager"), false);
776 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
807 assert.equal(editors.includes("file-manager"), false);
808 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
777809);
778810
779811it.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) => {
681681 }),
682682 );
683683
684 it.effect("honors explicit paths and reports invalid overrides without falling back", () =>
685 Effect.gen(function* () {
686 const fs = yield* FileSystem.FileSystem;
687 const path = yield* Path.Path;
688 const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-agy-path-test-" });
689 const externalDirectory = path.join(baseDir, "external");
690 const externalExecutable = path.join(externalDirectory, "agy_acp_server.par");
691 const externalHarness = path.join(externalDirectory, "localharness_external");
692 yield* fs.makeDirectory(externalDirectory);
693 yield* fs.writeFileString(externalExecutable, "external server", { mode: 0o755 });
694 yield* fs.writeFileString(externalHarness, "external harness", { mode: 0o755 });
695 const { installation } = yield* makeHarness({
696 baseDir,
697 path: externalDirectory,
698 previous: true,
699 });
700 yield* expectPreviousRelease(installation);
701 expect(yield* installation.resolve(undefined, { PATH: externalDirectory })).toMatchObject({
702 source: "managed",
703 version: previousVersion,
704 });
705 expect(yield* installation.resolve(externalExecutable)).toMatchObject({
706 executablePath: externalExecutable,
707 source: "override",
708 managedVersionDirectory: null,
709 });
710 expect(yield* installation.resolve("agy_acp_server.par")).toMatchObject({
711 source: "override",
712 });
713 yield* fs.remove(externalHarness);
714 expect(yield* installation.resolve(externalExecutable).pipe(Effect.flip)).toMatchObject({
715 operation: "resolve",
716 });
717 expect(
718 yield* installation.resolve(path.join(baseDir, "missing")).pipe(Effect.flip),
719 ).toMatchObject({
720 operation: "resolve",
721 });
722 yield* expectPreviousRelease(installation);
723 yield* fs.writeFileString(externalHarness, "external harness", { mode: 0o755 });
724 yield* installation.remove();
725 expect(yield* installation.resolve()).toMatchObject({
726 source: "path",
727 executablePath: externalExecutable,
728 });
729 const isolated = yield* makeHarness({ baseDir });
730 expect(yield* isolated.installation.resolve().pipe(Effect.flip)).toMatchObject({
731 operation: "resolve",
732 });
733 expect(
734 yield* isolated.installation.resolve(undefined, { PATH: externalDirectory }),
735 ).toMatchObject({
736 source: "path",
737 executablePath: externalExecutable,
738 });
739 expect(
740 yield* isolated.installation.resolve("agy_acp_server.par", { PATH: externalDirectory }),
741 ).toMatchObject({ source: "override", executablePath: externalExecutable });
742 }),
684 // Real posix executables in a real temp dir, resolved by a linux-mocked
685 // PATH walk; a Windows temp path cannot be split on `:`.
686 it.effect.skipIf(HostProcessPlatform.defaultValue() === "win32")(
687 "honors explicit paths and reports invalid overrides without falling back",
688 () =>
689 Effect.gen(function* () {
690 const fs = yield* FileSystem.FileSystem;
691 const path = yield* Path.Path;
692 const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-agy-path-test-" });
693 const externalDirectory = path.join(baseDir, "external");
694 const externalExecutable = path.join(externalDirectory, "agy_acp_server.par");
695 const externalHarness = path.join(externalDirectory, "localharness_external");
696 yield* fs.makeDirectory(externalDirectory);
697 yield* fs.writeFileString(externalExecutable, "external server", { mode: 0o755 });
698 yield* fs.writeFileString(externalHarness, "external harness", { mode: 0o755 });
699 const { installation } = yield* makeHarness({
700 baseDir,
701 path: externalDirectory,
702 previous: true,
703 });
704 yield* expectPreviousRelease(installation);
705 expect(yield* installation.resolve(undefined, { PATH: externalDirectory })).toMatchObject({
706 source: "managed",
707 version: previousVersion,
708 });
709 expect(yield* installation.resolve(externalExecutable)).toMatchObject({
710 executablePath: externalExecutable,
711 source: "override",
712 managedVersionDirectory: null,
713 });
714 expect(yield* installation.resolve("agy_acp_server.par")).toMatchObject({
715 source: "override",
716 });
717 yield* fs.remove(externalHarness);
718 expect(yield* installation.resolve(externalExecutable).pipe(Effect.flip)).toMatchObject({
719 operation: "resolve",
720 });
721 expect(
722 yield* installation.resolve(path.join(baseDir, "missing")).pipe(Effect.flip),
723 ).toMatchObject({
724 operation: "resolve",
725 });
726 yield* expectPreviousRelease(installation);
727 yield* fs.writeFileString(externalHarness, "external harness", { mode: 0o755 });
728 yield* installation.remove();
729 expect(yield* installation.resolve()).toMatchObject({
730 source: "path",
731 executablePath: externalExecutable,
732 });
733 const isolated = yield* makeHarness({ baseDir });
734 expect(yield* isolated.installation.resolve().pipe(Effect.flip)).toMatchObject({
735 operation: "resolve",
736 });
737 expect(
738 yield* isolated.installation.resolve(undefined, { PATH: externalDirectory }),
739 ).toMatchObject({
740 source: "path",
741 executablePath: externalExecutable,
742 });
743 expect(
744 yield* isolated.installation.resolve("agy_acp_server.par", { PATH: externalDirectory }),
745 ).toMatchObject({ source: "override", executablePath: externalExecutable });
746 }),
743747 );
744748
745749 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 {
2222} from "./providerMaintenance.ts";
2323
2424const 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
2529const makeTempDir = (name: string) =>
2630 Crypto.Crypto.pipe(
2731 Effect.flatMap((crypto) => crypto.randomUUIDv4),
......@@ -199,7 +203,7 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => {
199203 });
200204 });
201205
202 it.effect(
206 it.effect.skipIf(windowsHost)(
203207 "switches package-managed providers to vite-plus updates when the resolved binary lives in vite-plus global bin",
204208 () =>
205209 Effect.gen(function* () {
......@@ -272,7 +276,7 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => {
272276 }),
273277 );
274278
275 it.effect(
279 it.effect.skipIf(windowsHost)(
276280 "switches package-managed providers to pnpm updates when the resolved binary lives in pnpm's global bin",
277281 () =>
278282 Effect.gen(function* () {
......@@ -332,7 +336,7 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => {
332336 });
333337 });
334338
335 it.effect(
339 it.effect.skipIf(windowsHost)(
336340 "switches native-package-tool to native updates when the binary resolves through the native installer",
337341 () =>
338342 Effect.gen(function* () {
......@@ -369,7 +373,7 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => {
369373 }),
370374 );
371375
372 it.effect(
376 it.effect.skipIf(windowsHost)(
373377 "switches scoped-package-tool to native upgrades when the binary resolves through the standalone installer",
374378 () =>
375379 Effect.gen(function* () {
apps/server/src/resourceTelemetry/ResourceMonitorBinary.test.ts+6-2
......@@ -11,6 +11,10 @@ import * as FileSystem from "effect/FileSystem";
1111import { ServerConfig } from "../config.ts";
1212import * 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
1418describe("ResourceMonitorBinary", () => {
1519 afterEach(() => {
1620 vi.restoreAllMocks();
......@@ -42,7 +46,7 @@ describe("ResourceMonitorBinary", () => {
4246 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
4347 );
4448
45 it.effect("resolves an executable override", () =>
49 it.effect.skipIf(windowsHost)("resolves an executable override", () =>
4650 Effect.gen(function* () {
4751 const fileSystem = yield* FileSystem.FileSystem;
4852 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
......@@ -66,7 +70,7 @@ describe("ResourceMonitorBinary", () => {
6670 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
6771 );
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", () =>
7074 Effect.gen(function* () {
7175 const fileSystem = yield* FileSystem.FileSystem;
7276 const baseDir = yield* fileSystem.makeTempDirectoryScoped({