Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -300,11 +300,7 @@ jobs:
run: bun run typecheck

- name: Web tests
# Explicit sorted file list: bun discovers test files in filesystem
# readdir order, which differs between Linux runners and local macOS,
# so an unpinned run exercises a file order no developer can
# reproduce. Sorted order makes CI failures replayable locally.
run: bun test $(ls tests/*.test.ts tests/*.test.tsx | sort)
run: bun run test

# Checks for in-app React webviews (currently the diff viewer; more cmux React
# surfaces will live alongside it).
Expand Down
64 changes: 64 additions & 0 deletions tests/test_ci_self_hosted_guard.sh
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,68 @@ check_web_db_behavior_tests() {
echo "PASS: web DB behavior tests run through the discovery runner"
}

check_web_test_runner_wiring() {
local runner="$ROOT_DIR/web/scripts/run-tests.sh"
if [[ ! -f "$runner" ]]; then
echo "FAIL: shared web test runner is missing"
exit 1
fi

if ! grep -Fq '"test": "bash scripts/run-tests.sh"' "$ROOT_DIR/web/package.json"; then
echo "FAIL: web/package.json must expose the shared web test runner"
exit 1
fi

if ! awk '
/^ web-typecheck:/ { in_job=1; next }
in_job && /^ [^[:space:]#][^:]*:[[:space:]]*(#.*)?$/ { in_job=0 }
in_job && /- name: Web tests/ { in_step=1; next }
in_step && /^[[:space:]]*- name:/ { in_step=0 }
in_step && /^[[:space:]]*run:[[:space:]]*bun run test([[:space:]]|$)/ { saw_shared_runner=1 }
END { exit !saw_shared_runner }
' "$CI_FILE"; then
echo "FAIL: web-typecheck must use the shared local web test runner"
exit 1
fi

echo "PASS: CI and local web tests use the shared runner"
}

check_web_test_runner_behavior() {
local fixture_dir fixture_runner args_log expected_args
fixture_dir="$(mktemp -d)"
fixture_runner="$fixture_dir/web/scripts/run-tests.sh"
args_log="$fixture_dir/bun-args.log"
mkdir -p "$fixture_dir/web/scripts" "$fixture_dir/bin"
cp "$ROOT_DIR/web/scripts/run-tests.sh" "$fixture_runner"

cat > "$fixture_dir/bin/bun" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' "$@" > "$CMUX_WEB_TEST_RUNNER_ARGS_LOG"
EOF
chmod +x "$fixture_dir/bin/bun"

if ! PATH="$fixture_dir/bin:/usr/bin:/bin" \
CMUX_WEB_TEST_RUNNER_ARGS_LOG="$args_log" \
/bin/bash "$fixture_runner"; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add a discovered test file to the guard fixture

This invokes the new runner with no arguments, but the fixture contains only web/scripts/run-tests.sh and the fake bun executable. Consequently, default_test_file_exists exits before the fake Bun is called; running bash tests/test_ci_self_hosted_guard.sh produces cmux web test runner found no test files followed by FAIL: shared web test runner default discovery should execute. Since .github/workflows/ci.yml runs this script in workflow-guard-tests, that required job now fails on every run; create a correctly named fixture test before invoking the runner.

Useful? React with 👍 / 👎.

rm -rf "$fixture_dir"
echo "FAIL: shared web test runner default discovery should execute"
exit 1
fi

expected_args=$'test\n--isolate'
if [[ "$(cat "$args_log")" != "$expected_args" ]]; then
echo "FAIL: shared web test runner must preserve Bun's default discovery with isolation"
cat "$args_log"
rm -rf "$fixture_dir"
exit 1
fi

rm -rf "$fixture_dir"
echo "PASS: shared web test runner delegates default discovery to Bun with isolation"
}

check_tmux_terminal_nightly_isolation() {
check_macos_runner "$TMUX_CORPUS_FILE" "terminal-nightly"

Expand Down Expand Up @@ -1027,4 +1089,6 @@ check_gui_smoke_unsupported_launch_handling
check_no_ci_xctest_skips
check_no_ci_swift_package_skips
check_web_db_behavior_tests
check_web_test_runner_wiring
check_web_test_runner_behavior
check_tmux_terminal_nightly_isolation
2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"db:up": "bash scripts/db-local.sh up",
"db:url": "bash scripts/db-local.sh url",
"start": "next start",
"test": "bun test",
"test": "bash scripts/run-tests.sh",
"test:db:behavior": "bash scripts/run-db-behavior-tests.sh",
"typecheck": "tsgo --noEmit",
"lint": "eslint"
Expand Down
29 changes: 29 additions & 0 deletions web/scripts/run-tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -euo pipefail

WEB_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$WEB_ROOT"

default_test_file_exists() {
while IFS= read -r -d '' file; do
case "$file" in
*.js | *.jsx | *.ts | *.tsx | *.mjs | *.cjs | *.mts | *.cts) ;;
*) continue ;;
esac
case "${file##*/}" in
*".test"* | *"_test_"* | *".spec"* | *"_spec_"*) return 0 ;;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Align pre-discovery with Bun

The basename substrings do not match Bun's default filename rules: *_test_* misses a valid name such as example_test.ts, while *.test* accepts a helper such as example.test-helper.ts. This can reject a valid default suite or delegate an actually empty suite back to Bun's version-dependent behavior.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

esac
done < <(
find . \
\( -path "./node_modules" -o -path "./.next" \) -prune \
-o -type f -print0
)
return 1
}

if (( $# == 0 )) && ! default_test_file_exists; then
echo "cmux web test runner found no test files" >&2
exit 1
fi

exec bun test --isolate "$@"
122 changes: 122 additions & 0 deletions web/tests/web-test-runner-isolation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { expect, test } from "bun:test";
import { spawnSync } from "node:child_process";
import {
copyFileSync,
mkdirSync,
mkdtempSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";

const webRoot = fileURLToPath(new URL("..", import.meta.url));
const runnerPath = fileURLToPath(
new URL("../scripts/run-tests.sh", import.meta.url),
);
const fixtureTestSource = `
import { test } from "bun:test";
test("fixture runs", () => {});
`;

test("shared web test runner isolates module mocks across files", () => {
const result = runChild(
process.execPath,
[
"run",
"test",
"--",
"--randomize",
"--seed=1",
"tests/feedback-route.test.ts",
"tests/founders-welcome-route.test.ts",
],
webRoot,
);

if (result.status !== 0) {
throw new Error(
`shared web test runner leaked module state:\n${result.output}`,
);
}
expect(result.output).toContain("tests/feedback-route.test.ts:");
expect(result.output).toContain("tests/founders-welcome-route.test.ts:");
expect(result.output).toContain("0 fail");
});

test("shared web test runner preserves recursive default discovery", () => {
const fixtureRoot = createRunnerFixture();
try {
mkdirSync(join(fixtureRoot, "tests", "nested"), { recursive: true });
writeFileSync(
join(fixtureRoot, "tests", "top-level.test.ts"),
fixtureTestSource,
);
writeFileSync(
join(fixtureRoot, "tests", "nested", "nested.test.ts"),
fixtureTestSource,
);

const result = runChild(
"/bin/bash",
["scripts/run-tests.sh"],
fixtureRoot,
);
if (result.status !== 0) {
throw new Error(
`shared web test runner skipped recursive discovery:\n${result.output}`,
);
}
expect(result.output).toContain("tests/top-level.test.ts:");
expect(result.output).toContain("tests/nested/nested.test.ts:");
} finally {
rmSync(fixtureRoot, { recursive: true, force: true });
}
});

test("shared web test runner fails when default discovery finds no tests", () => {
const fixtureRoot = createRunnerFixture();
try {
mkdirSync(join(fixtureRoot, "tests"));
const result = runChild(
"/bin/bash",
["scripts/run-tests.sh"],
fixtureRoot,
);
expect(result.status).not.toBe(0);
expect(result.output).toContain(
"cmux web test runner found no test files",
);
} finally {
rmSync(fixtureRoot, { recursive: true, force: true });
}
});

function createRunnerFixture(): string {
const fixtureRoot = mkdtempSync(join(tmpdir(), "cmux-web-test-runner-"));
mkdirSync(join(fixtureRoot, "scripts"));
copyFileSync(runnerPath, join(fixtureRoot, "scripts", "run-tests.sh"));
return fixtureRoot;
}

function runChild(
command: string,
args: string[],
cwd: string,
): { status: number | null; output: string } {
const result = spawnSync(command, args, {
cwd,
encoding: "utf8",
timeout: 30_000,
killSignal: "SIGKILL",
});
const output = [result.stdout, result.stderr].join("\n");

if (result.error) {
throw new Error(
`shared web test runner did not finish: ${result.error.message}\n${output}`,
);
}
return { status: result.status, output };
}