Compare commits

...
3 changed files with 84 additions and 135 deletions

No files matched your search

+31 -122
View File
@@ -1,91 +1,11 @@
name: Tests
# Builds, type-checks and runs every test suite on a clean GitHub runner, and records the file
# list each published package would contain. Each suite is its own job so one failure does not
# hide the others. No provider credentials or operator configuration are supplied.
# Package suites deny external networking; scenario runtime has no external route.
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
on: workflow_dispatch
permissions:
contents: read
concurrency:
group: tests-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-and-package:
doctor-r2-wording:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run build
- name: Record the published file list of each package
run: |
set -euo pipefail
mkdir -p pack-lists
for p in cli daemon tui; do
(cd packages/$p && npm pack --dry-run --json --ignore-scripts) \
| node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{for(const f of JSON.parse(s)[0].files)console.log(f.path)})' \
| sort > pack-lists/$p.txt
echo "$p: $(wc -l < pack-lists/$p.txt) files"
done
- uses: actions/upload-artifact@v4
with:
name: pack-lists
path: pack-lists/
typecheck:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run lint
repo-checks:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run test:repo
package-tests:
if: ${{ github.event_name != 'push' }}
# macOS, the platform OpenRig runs on: several daemon and cli tests read macOS ps columns,
# file-inode behaviour and release tags.
runs-on: macos-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
package: [daemon, cli, tui, ui]
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
@@ -95,48 +15,37 @@ jobs:
with:
node-version: 22
cache: npm
- run: brew install tmux
- run: npm ci
- name: Build the packages the tests import
run: npm run build
- name: Run the suite without ambient credentials or external network
- run: npm run build
- name: Measure the two new assertions against prior wording
run: |
suite_root=$(mktemp -d "$RUNNER_TEMP/opr-test.XXXXXX")
suite_root=$(cd "$suite_root" && pwd -P)
mkdir -p "$suite_root/home" "$suite_root/bin"
# Same system bytes, nonprivileged mode; no system file is changed.
/usr/bin/install -m 0755 /bin/ps "$suite_root/bin/ps"
# Only this job's temporary sockets (including tmux) are reachable.
env -i HOME="$suite_root/home" PATH="$PATH" TMPDIR="$suite_root" TMUX_TMPDIR="$suite_root" \
CI=true TERM=xterm-256color \
sandbox-exec -D IPC_ROOT="$suite_root" -p '(version 1)(allow default)(deny network-outbound)(allow network-outbound (remote ip "localhost:*"))(allow network-outbound (subpath (param "IPC_ROOT")))' \
/bin/bash -s "${{ matrix.package }}" <<'SUITE'
set -euo pipefail
node scripts/ci-macos-process-probe.mjs
# The production readers use bare ps. Select real system bytes only after
# self/owned-child observation and network/IPC confinement are proved.
export PATH="$TMPDIR/bin:$PATH"
test "$(command -v ps)" = "$TMPDIR/bin/ps"
exec npm run test -w "packages/$1"
SUITE
installed-scenario:
runs-on: ubuntu-latest
timeout-minutes: 35
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- name: Installed package, daemon restart, and seeded durability failure
run: bash scripts/run-pr-scenarios.sh
mkdir -p "$RUNNER_TEMP/doctor-r2"
cp packages/cli/src/commands/doctor.ts "$RUNNER_TEMP/doctor-r2/candidate-doctor.ts"
git show caddbfd75990f5b26ca961cb4fbcc967ba83c9e6:packages/cli/src/commands/doctor.ts > packages/cli/src/commands/doctor.ts
set +e
npx vitest run packages/cli/test/doctor.test.ts -t 'control unavailable \(default\)|check.s limited read' --reporter=json --outputFile="$RUNNER_TEMP/doctor-r2/prior.json" > "$RUNNER_TEMP/doctor-r2/prior.log" 2>&1
result=$?
set -e
test "$result" = 1
python3 - <<'PYCODE'
import json,os,pathlib
x=json.loads((pathlib.Path(os.environ['RUNNER_TEMP'])/'doctor-r2/prior.json').read_text())
assert x['numFailedTests']==2 and x['numPassedTests']==0,x
print('Two intended prior-wording assertion failures recorded')
PYCODE
- name: Verify candidate doctor suite
run: |
cp "$RUNNER_TEMP/doctor-r2/candidate-doctor.ts" packages/cli/src/commands/doctor.ts
npx vitest run packages/cli/test/doctor.test.ts --reporter=json --outputFile="$RUNNER_TEMP/doctor-r2/candidate.json" > "$RUNNER_TEMP/doctor-r2/candidate.log" 2>&1
python3 - <<'PYCODE'
import json,os,pathlib
x=json.loads((pathlib.Path(os.environ['RUNNER_TEMP'])/'doctor-r2/candidate.json').read_text())
print(json.dumps({k:x[k] for k in ['numPassedTests','numFailedTests','numPendingTests']}))
assert x['numFailedTests']==0
PYCODE
- uses: actions/upload-artifact@v4
if: always()
with:
name: installed-scenario-evidence
path: dist/pr-scenarios/
name: doctor-r2-wording
path: ${{ runner.temp }}/doctor-r2/
if-no-files-found: error
+8 -7
View File
@@ -54,6 +54,7 @@ export interface DoctorDeps {
}
const DEFAULT_PORT = 7433;
const CMUX_SOCKET_CONTROL_FIX = 'In cmux Settings > Automation, select "Automation mode" for Socket Control Mode, or set automation.socketControlMode to "automation" in ~/.config/cmux/cmux.json (older versions use ~/.config/cmux/settings.json; current cmux reads that legacy file only for keys absent from cmux.json). cmux reloads saved settings automatically; rerun `rig doctor` to re-probe without restarting the daemon.';
function defaultCheckPort(port: number, host: string): Promise<boolean> {
return new Promise((resolve) => {
@@ -173,9 +174,9 @@ export function runDoctorChecks(deps: DoctorDeps, providerChecks: DoctorCheck[]
const modeHint = socketMode?.error
? ` Likely cause on macOS: ${CMUX_SETTINGS_DISCLOSURE_PATH} is unreadable (${socketMode.error}).`
: socketMode && !isCmuxSocketControlCompatible(socketMode.mode) && socketMode.source === "default"
? ` Likely cause on macOS: cmux is still using its default automation.socketControlMode '${socketMode.mode}'.`
? ` ${CMUX_SETTINGS_DISCLOSURE_PATH} doesn't set automation.socketControlMode, so this check assumes cmux's default '${socketMode.mode}' (it doesn't read ~/.config/cmux/cmux.json). ${CMUX_SOCKET_CONTROL_FIX}`
: socketMode && !isCmuxSocketControlCompatible(socketMode.mode)
? ` Likely cause on macOS: automation.socketControlMode is '${socketMode.mode}' in ${CMUX_SETTINGS_DISCLOSURE_PATH}.`
? ` Likely cause on macOS: automation.socketControlMode is '${socketMode.mode}' in ${CMUX_SETTINGS_DISCLOSURE_PATH}. ${CMUX_SOCKET_CONTROL_FIX}`
: "";
checks.push({
name: "cmux_shell",
@@ -352,20 +353,20 @@ function buildCmuxDaemonWarning(deps: DoctorDeps, platform: NodeJS.Platform): Do
status: "warn",
message: "Shell cmux works, but the daemon cannot control cmux.",
reason: `${CMUX_SETTINGS_DISCLOSURE_PATH} is unreadable: ${socketMode.error}`,
fix: `Repair ${CMUX_SETTINGS_DISCLOSURE_PATH} or remove it so cmux can regenerate the template, then rerun \`rig setup\` or \`rig doctor\`.`,
fix: `Repair JSON in ${CMUX_SETTINGS_DISCLOSURE_PATH}; on current cmux, also check ~/.config/cmux/cmux.json (the primary settings file). Save and rerun \`rig doctor\`; cmux reloads saved settings automatically and this check re-probes without a daemon restart.`,
};
}
if (platform === "darwin" && socketMode && !isCmuxSocketControlCompatible(socketMode.mode)) {
const reason = socketMode.source === "default"
? `cmux is still using its default automation.socketControlMode '${socketMode.mode}', so the daemon cannot attach as an external cmux client yet.`
? `${CMUX_SETTINGS_DISCLOSURE_PATH} doesn't set automation.socketControlMode, so this check assumes cmux's default '${socketMode.mode}' (it doesn't read ~/.config/cmux/cmux.json).`
: `automation.socketControlMode is '${socketMode.mode}' in ${CMUX_SETTINGS_DISCLOSURE_PATH}, so the daemon cannot attach as an external cmux client yet.`;
return {
name: "cmux_daemon",
status: "warn",
message: "Shell cmux works, but the daemon cannot control cmux.",
reason,
fix: `Run \`rig setup\` to set automation.socketControlMode to "automation" in ${CMUX_SETTINGS_DISCLOSURE_PATH}, then restart the daemon with \`rig daemon start\`.`,
fix: CMUX_SOCKET_CONTROL_FIX,
};
}
@@ -373,8 +374,8 @@ function buildCmuxDaemonWarning(deps: DoctorDeps, platform: NodeJS.Platform): Do
name: "cmux_daemon",
status: "warn",
message: "Shell cmux works, but the daemon cannot control cmux.",
reason: "The daemon inherited a terminal/session environment that broke cmux adapter initialization.",
fix: "Restart the daemon with `rig daemon start` after setup. If it still fails, inspect the daemon log with `rig daemon logs`.",
reason: "The cmux control probe failed in the daemon, although it succeeded in this shell.",
fix: "Check that cmux is running and allows this user to control it, then rerun `rig doctor` to re-probe. If it still fails, inspect the daemon log with `rig daemon logs`.",
};
}
+45 -6
View File
@@ -181,9 +181,10 @@ describe("runDoctorChecks", () => {
expect(cmuxShell?.status).toBe("pass");
});
it("cmux_shell warn when shell cmux installed but control unavailable", () => {
it.each(["settings", "default"])("cmux_shell warn when shell cmux installed but control unavailable (%s)", (source) => {
const deps = makeDeps({
readFile: (filePath: string) => filePath === CMUX_SETTINGS_PATH ? "{\n \"automation\": {\n \"socketControlMode\": \"cmuxOnly\"\n }\n}\n" : null,
readFile: (filePath: string) => filePath === CMUX_SETTINGS_PATH && source === "settings"
? '{"automation":{"socketControlMode":"cmuxOnly"}}' : null,
exec: (cmd: string) => {
if (cmd === "tmux -V") return "tmux 3.4\n";
if (cmd === "cmux capabilities --json") {
@@ -199,6 +200,19 @@ describe("runDoctorChecks", () => {
expect(cmuxShell?.message).toContain("control unavailable");
expect(cmuxShell?.fix).toContain("cmuxOnly");
expect(cmuxShell?.fix).toContain("socketControlMode");
expect(cmuxShell?.fix).toContain("cmux Settings > Automation");
expect(cmuxShell?.fix).toContain("Automation mode");
expect(cmuxShell?.fix).toContain("cmux.json");
expect(cmuxShell?.fix).toContain("settings.json");
expect(cmuxShell?.fix).toContain("reloads saved settings automatically");
expect(cmuxShell?.fix).not.toContain("rig setup");
expect(cmuxShell?.fix).not.toContain("rig daemon start");
if (source === "default") {
expect(cmuxShell?.fix).toContain("~/.config/cmux/settings.json doesn't set automation.socketControlMode");
expect(cmuxShell?.fix).toContain("this check assumes cmux's default 'cmuxOnly'");
expect(cmuxShell?.fix).toContain("it doesn't read ~/.config/cmux/cmux.json");
expect(cmuxShell?.fix).not.toContain("cmux is still using");
}
});
it("cmux_shell warn when cmux missing", () => {
@@ -234,7 +248,9 @@ describe("runDoctorChecks", () => {
expect(cmuxDaemon).toBeDefined();
expect(cmuxDaemon?.status).toBe("warn");
expect(cmuxDaemon?.message).toContain("daemon cannot control");
expect(cmuxDaemon?.fix).toContain("rig daemon start");
expect(cmuxDaemon?.fix).toContain("rig doctor");
expect(cmuxDaemon?.fix).not.toContain("rig daemon start");
expect(cmuxDaemon?.fix).not.toContain("rig setup");
});
it("shell cmux pass + restrictive macOS socket control + daemon cmux unavailable -> cmux_daemon points at socket control mode", async () => {
@@ -257,11 +273,18 @@ describe("runDoctorChecks", () => {
expect(cmuxDaemon?.status).toBe("warn");
expect(cmuxDaemon?.reason).toContain("socketControlMode");
expect(cmuxDaemon?.reason).toContain("cmuxOnly");
expect(cmuxDaemon?.fix).toContain("rig setup");
expect(cmuxDaemon?.fix).toContain("cmux Settings > Automation");
expect(cmuxDaemon?.fix).toContain("Automation mode");
expect(cmuxDaemon?.fix).toContain("cmux.json");
expect(cmuxDaemon?.fix).toContain("settings.json");
expect(cmuxDaemon?.fix).toContain("reloads saved settings automatically");
expect(cmuxDaemon?.fix).toContain("rig doctor");
expect(cmuxDaemon?.fix).not.toContain("rig setup");
expect(cmuxDaemon?.fix).not.toContain("rig daemon start");
expect(cmuxDaemon?.fix).toContain("automation");
});
it("shell cmux pass + implicit default cmuxOnly + daemon cmux unavailable -> cmux_daemon points at effective default, not a fake file value", async () => {
it("shell cmux pass + implicit default cmuxOnly + daemon cmux unavailable -> cmux_daemon discloses the check's limited read", async () => {
const deps = makeDeps({
readFile: (filePath: string) => filePath === CMUX_SETTINGS_PATH ? null : null,
fetch: async (url: string) => {
@@ -281,6 +304,13 @@ describe("runDoctorChecks", () => {
expect(cmuxDaemon?.reason).toContain("default");
expect(cmuxDaemon?.reason).toContain("cmuxOnly");
expect(cmuxDaemon?.reason).not.toContain("is 'cmuxOnly' in");
expect(cmuxDaemon?.reason).toContain("~/.config/cmux/settings.json doesn't set automation.socketControlMode");
expect(cmuxDaemon?.reason).toContain("this check assumes cmux's default 'cmuxOnly'");
expect(cmuxDaemon?.reason).toContain("it doesn't read ~/.config/cmux/cmux.json");
expect(cmuxDaemon?.reason).not.toContain("cmux is still using");
expect(cmuxDaemon?.fix).toContain("cmux Settings > Automation");
expect(cmuxDaemon?.fix).not.toContain("rig setup");
expect(cmuxDaemon?.fix).not.toContain("rig daemon start");
});
it("shell cmux pass + unreadable cmux settings -> cmux_daemon warns about parse failure", async () => {
@@ -301,7 +331,13 @@ describe("runDoctorChecks", () => {
const cmuxDaemon = allChecks.find((c) => c.name === "cmux_daemon");
expect(cmuxDaemon?.status).toBe("warn");
expect(cmuxDaemon?.reason).toContain("unreadable");
expect(cmuxDaemon?.fix).toContain("Repair JSON");
expect(cmuxDaemon?.fix).toContain("~/.config/cmux/settings.json");
expect(cmuxDaemon?.fix).toContain("cmux.json");
expect(cmuxDaemon?.fix).toContain("rig doctor");
expect(cmuxDaemon?.fix).not.toContain("rig setup");
expect(cmuxDaemon?.fix).not.toContain("remove it");
expect(cmuxDaemon?.fix).not.toContain("rig daemon start");
});
it("shell cmux pass + compatible password mode + daemon cmux unavailable -> falls back to generic daemon warning", async () => {
@@ -321,7 +357,10 @@ describe("runDoctorChecks", () => {
const cmuxDaemon = allChecks.find((c) => c.name === "cmux_daemon");
expect(cmuxDaemon?.status).toBe("warn");
expect(cmuxDaemon?.reason).toContain("inherited a terminal/session environment");
expect(cmuxDaemon?.reason).toContain("probe failed in the daemon");
expect(cmuxDaemon?.fix).toContain("rig daemon logs");
expect(cmuxDaemon?.fix).toContain("rig doctor");
expect(cmuxDaemon?.fix).not.toContain("rig daemon start");
expect(cmuxDaemon?.reason).not.toContain("socketControlMode");
});