From b306a51687c094fecdb2c99285d0aac30b24750b Mon Sep 17 00:00:00 2001 From: Oliver Drobnik Date: Sat, 9 May 2026 14:26:07 +0200 Subject: [PATCH 1/2] BashSwiftScriptTests: end-to-end verify Subprocess.run hits bash builtins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the verification loop on the polyglot subprocess unification: - ShellKit#1 — `Shell.processLauncher` primitive - SwiftBash#19 — `BashProcessLauncher` resolves against the bash command registry - SwiftScript#5 — `import Subprocess` bridge routes through `Shell.current.processLauncher` A SwiftScript script under SwiftBash that does `Subprocess.run(Executable.name("echo"), ...)` should NOT reach `posix_spawn`; it should hit `EchoCommand` (the pure-Swift bash builtin). Two tests confirm this in practice — they're the script-author-visible payoff for the layered work above. * `subprocessRunRoutesThroughBashCommandRegistry` — script calls `Subprocess.run(Executable.name("echo"), arguments: ["hello-..."], ...)`, the bridge calls `Shell.current.processLauncher.launch(...)`, `BashProcessLauncher` resolves `"echo"` to the registered `EchoCommand`, the captured `standardOutput` round-trips back to the script and prints. No `posix_spawn`, no host `/bin/echo` involvement. * `subprocessRunWithCanonicalPathHitsBashBuiltin` — same flow with `Executable.path("/bin/echo")`. Verifies the canonical-bin-path resolution rule (PR #19's P2 fix): `/bin/echo` matches `BinCatalog.knownPaths["echo"]`, so the launcher dispatches to the registered builtin. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../BashSwiftScriptTests.swift | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/Tests/BashSwiftScriptTests/BashSwiftScriptTests.swift b/Tests/BashSwiftScriptTests/BashSwiftScriptTests.swift index 3671b8d..b74283d 100644 --- a/Tests/BashSwiftScriptTests/BashSwiftScriptTests.swift +++ b/Tests/BashSwiftScriptTests/BashSwiftScriptTests.swift @@ -248,6 +248,65 @@ import ShellKit #expect(cap.stdout == "denied\n") } + // MARK: subprocess routing + + @Test func subprocessRunRoutesThroughBashCommandRegistry() async throws { + // The full polyglot story end-to-end: a SwiftScript script + // running under SwiftBash uses `import Subprocess` to call out + // to `echo`. Because the SwiftBash `Shell`'s default + // `processLauncher` is `BashProcessLauncher` (not the + // `DefaultProcessLauncher` that ShellKit installs), the call + // resolves against the bash command registry — `echo` is the + // pure-Swift `EchoCommand` builtin, NOT `/bin/echo` from the + // host filesystem. This proves a script-side Subprocess call + // never reaches `posix_spawn` when bound to a SwiftBash shell, + // matching SwiftBash's "no Process / fork / exec" model. + let (shell, cap) = makeShell() + let s = try writeScript(""" + #!/usr/bin/env swift-script + import Subprocess + let r = try await Subprocess.run( + Executable.name("echo"), + arguments: ["hello-from-bash-builtin"], + output: Output.string(limit: 4096)) + if let out = r.standardOutput, r.terminationStatus.isSuccess { + print("got=\\(out)", terminator: "") + } else { + print("bridge dispatch failed") + } + """) + defer { try? FileManager.default.removeItem(atPath: s.dir) } + + let status = try await shell.run(Self.bashQuote(s.path)) + #expect(status.code == 0) + // `EchoCommand` writes "hello-from-bash-builtin\n", the script + // captures that and prints `got=hello-from-bash-builtin\n`. + #expect(cap.stdout == "got=hello-from-bash-builtin\n") + } + + @Test func subprocessRunWithCanonicalPathHitsBashBuiltin() async throws { + // `Executable.path("/bin/echo")` is resolved by + // `BashProcessLauncher` against `BinCatalog.knownPaths` — the + // canonical path matches, so it dispatches to the registered + // `echo` builtin (not the host's `/bin/echo`). Mirrors the + // `VirtualBinFileSystem` synthesized-file rule from the bash + // side. + let (shell, cap) = makeShell() + let s = try writeScript(""" + #!/usr/bin/env swift-script + import Subprocess + let r = try await Subprocess.run( + Executable.path("/bin/echo"), + arguments: ["via-canonical-path"], + output: Output.string(limit: 4096)) + print(r.standardOutput ?? "", terminator: "") + """) + defer { try? FileManager.default.removeItem(atPath: s.dir) } + + try await shell.run(Self.bashQuote(s.path)) + #expect(cap.stdout == "via-canonical-path\n") + } + // MARK: identity @Test func swiftScriptSeesSandboxIdentity() async throws { From 73c0e16b6ae9061b7c0ee76c5e8a6da2b0aa2496 Mon Sep 17 00:00:00 2001 From: Oliver Drobnik Date: Sat, 9 May 2026 14:31:18 +0200 Subject: [PATCH 2/2] Address Codex review on PR #22: differentiate builtin from host echo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P2 from the automated review on https://github.com/Cocoanetics/SwiftBash/pull/22: The original tests asserted on `echo` output, which is identical whether produced by `EchoCommand` (the bash builtin) or `/bin/echo` (the host). A regression that routed `Subprocess.run` through real exec instead of `BashProcessLauncher` would still pass on Unix- likes — the test didn't actually verify the routing claim. Fix: use a signal that differs between dispatch paths. - `subprocessRunRoutesThroughBashCommandRegistry` registers a sentinel command name (`swiftbash-only-marker`) that exists only in the shell's command table — nothing matches on the host's PATH. A regression that fell through to real exec would fail with "executable not found." The test asserts on a `REGISTRY-OK:` prefix produced by the registered closure. - `subprocessRunWithCanonicalPathHitsBashBuiltin` overrides the registry's `echo` with a closure that prepends `BUILTIN:`. The canonical-path resolution rule (`BinCatalog.knownPaths["echo"] == "/bin/echo"`) routes the call through `BashProcessLauncher` to the override; a regression that invoked real `/bin/echo` would miss the prefix. Both pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../BashSwiftScriptTests.swift | 44 ++++++++++++++----- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/Tests/BashSwiftScriptTests/BashSwiftScriptTests.swift b/Tests/BashSwiftScriptTests/BashSwiftScriptTests.swift index b74283d..36f1a1a 100644 --- a/Tests/BashSwiftScriptTests/BashSwiftScriptTests.swift +++ b/Tests/BashSwiftScriptTests/BashSwiftScriptTests.swift @@ -252,21 +252,30 @@ import ShellKit @Test func subprocessRunRoutesThroughBashCommandRegistry() async throws { // The full polyglot story end-to-end: a SwiftScript script - // running under SwiftBash uses `import Subprocess` to call out - // to `echo`. Because the SwiftBash `Shell`'s default + // running under SwiftBash uses `import Subprocess` to call a + // command name that **only exists in the bash registry, not + // on the host PATH**. Because the SwiftBash `Shell`'s default // `processLauncher` is `BashProcessLauncher` (not the // `DefaultProcessLauncher` that ShellKit installs), the call - // resolves against the bash command registry — `echo` is the - // pure-Swift `EchoCommand` builtin, NOT `/bin/echo` from the - // host filesystem. This proves a script-side Subprocess call - // never reaches `posix_spawn` when bound to a SwiftBash shell, - // matching SwiftBash's "no Process / fork / exec" model. + // resolves to the registered closure. A regression that + // routed through real exec would fail with "executable not + // found" because no such binary exists anywhere — that's what + // makes this test load-bearing on the routing claim, vs. a + // generic `echo` test where bash-builtin and host `/bin/echo` + // produce identical bytes. let (shell, cap) = makeShell() + // Sentinel registered only in this Shell's command table. + // Prints a marker that no real binary anywhere would emit. + shell.register(name: "swiftbash-only-marker") { argv in + let payload = argv.dropFirst().joined(separator: " ") + Shell.bashCurrent.stdout("REGISTRY-OK: \(payload)\n") + return .success + } let s = try writeScript(""" #!/usr/bin/env swift-script import Subprocess let r = try await Subprocess.run( - Executable.name("echo"), + Executable.name("swiftbash-only-marker"), arguments: ["hello-from-bash-builtin"], output: Output.string(limit: 4096)) if let out = r.standardOutput, r.terminationStatus.isSuccess { @@ -279,9 +288,9 @@ import ShellKit let status = try await shell.run(Self.bashQuote(s.path)) #expect(status.code == 0) - // `EchoCommand` writes "hello-from-bash-builtin\n", the script - // captures that and prints `got=hello-from-bash-builtin\n`. - #expect(cap.stdout == "got=hello-from-bash-builtin\n") + // The sentinel prefix proves the call landed on the registered + // closure, not on a host binary that happens to share the name. + #expect(cap.stdout == "got=REGISTRY-OK: hello-from-bash-builtin\n") } @Test func subprocessRunWithCanonicalPathHitsBashBuiltin() async throws { @@ -291,7 +300,18 @@ import ShellKit // `echo` builtin (not the host's `/bin/echo`). Mirrors the // `VirtualBinFileSystem` synthesized-file rule from the bash // side. + // + // To make the routing visible (host `/bin/echo` produces + // identical output for the same argv), override the registry's + // `echo` with a sentinel-prepending closure. A regression that + // bypassed `BashProcessLauncher` for canonical-path resolution + // would invoke the real `/bin/echo` and miss the prefix. let (shell, cap) = makeShell() + shell.register(name: "echo") { argv in + let payload = argv.dropFirst().joined(separator: " ") + Shell.bashCurrent.stdout("BUILTIN: \(payload)\n") + return .success + } let s = try writeScript(""" #!/usr/bin/env swift-script import Subprocess @@ -304,7 +324,7 @@ import ShellKit defer { try? FileManager.default.removeItem(atPath: s.dir) } try await shell.run(Self.bashQuote(s.path)) - #expect(cap.stdout == "via-canonical-path\n") + #expect(cap.stdout == "BUILTIN: via-canonical-path\n") } // MARK: identity