|
| 1 | +import { existsSync } from "node:fs"; |
| 2 | + |
1 | 3 | import { Database, initializeSchema, WorkspaceFilesystem } from "@cloudflare/dofs"; |
2 | 4 | import { SQLiteTestStorage } from "@cloudflare/dofs/testing"; |
3 | 5 | import { expect, test } from "vitest"; |
@@ -517,3 +519,74 @@ test("a spawned command sees the allowlisted environment, not the daemon's", asy |
517 | 519 | dispose(); |
518 | 520 | } |
519 | 521 | }); |
| 522 | + |
| 523 | +// The interpreter is a per-consumer choice, not a property of the image. |
| 524 | +// |
| 525 | +// A caller that redacts credentials through a pipe -- `git push "$URL" 2>&1 | |
| 526 | +// sed -E 's#//[^@]*@#//***@#'` -- gets the pipeline's last exit status, so a |
| 527 | +// failed push reads as success. PIPESTATUS is the usual recovery, and under |
| 528 | +// dash it is a parse error that aborts the command rather than a missing |
| 529 | +// feature, which is worse than the problem it was reached for. |
| 530 | +const hasBash = existsSync("/usr/bin/bash"); |
| 531 | + |
| 532 | +test("defaults to /bin/sh when no shell is given", async () => { |
| 533 | + const { runner, dispose } = fixture(); |
| 534 | + try { |
| 535 | + const handle = runner.exec("printf '%s' \"$0\""); |
| 536 | + const events = await drain(handle.events); |
| 537 | + const stdout = events |
| 538 | + .filter((event) => event.name === "stdout") |
| 539 | + .map((event) => decode(event.value as Uint8Array)) |
| 540 | + .join(""); |
| 541 | + expect(stdout).toBe("/bin/sh"); |
| 542 | + } finally { |
| 543 | + dispose(); |
| 544 | + } |
| 545 | +}); |
| 546 | + |
| 547 | +test.skipIf(!hasBash)("runs commands under an explicitly chosen shell", async () => { |
| 548 | + const { runner, dispose } = fixture({ shell: "/usr/bin/bash" }); |
| 549 | + try { |
| 550 | + const handle = runner.exec("printf '%s' \"$0\""); |
| 551 | + const events = await drain(handle.events); |
| 552 | + const stdout = events |
| 553 | + .filter((event) => event.name === "stdout") |
| 554 | + .map((event) => decode(event.value as Uint8Array)) |
| 555 | + .join(""); |
| 556 | + expect(stdout).toBe("/usr/bin/bash"); |
| 557 | + } finally { |
| 558 | + dispose(); |
| 559 | + } |
| 560 | +}); |
| 561 | + |
| 562 | +test.skipIf(!hasBash)("a chosen shell resolves PIPESTATUS instead of aborting", async () => { |
| 563 | + const { runner, dispose } = fixture({ shell: "/usr/bin/bash" }); |
| 564 | + try { |
| 565 | + // false | true leaves $? as true's 0 while the first pipeline stage's real |
| 566 | + // failure survives in the PIPESTATUS array. The trailing marker proves the |
| 567 | + // command was not aborted: under dash the expansion is fatal and "after" |
| 568 | + // never prints. The expansion is assembled from parts so its braces are |
| 569 | + // not linted as a JavaScript template placeholder. |
| 570 | + const first = ['"$', "{PIPESTATUS[0]}", '"'].join(""); |
| 571 | + const handle = runner.exec(`false | true; printf '[%s]' ${first}; printf "after"`); |
| 572 | + const events = await drain(handle.events); |
| 573 | + const stdout = events |
| 574 | + .filter((event) => event.name === "stdout") |
| 575 | + .map((event) => decode(event.value as Uint8Array)) |
| 576 | + .join(""); |
| 577 | + expect(stdout).toBe("[1]after"); |
| 578 | + } finally { |
| 579 | + dispose(); |
| 580 | + } |
| 581 | +}); |
| 582 | + |
| 583 | +test("rejects a shell that is not an absolute path", () => { |
| 584 | + const storage = new SQLiteTestStorage(); |
| 585 | + const db = new Database(storage); |
| 586 | + initializeSchema(db, () => Date.now()); |
| 587 | + try { |
| 588 | + expect(() => new Runner({ db, shell: "bash" })).toThrow(/absolute path/); |
| 589 | + } finally { |
| 590 | + storage.close?.(); |
| 591 | + } |
| 592 | +}); |
0 commit comments