mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-11 17:32:22 -10:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e2eecdf53 | ||
|
|
a5359b891c | ||
|
|
caddbfd759 | ||
|
|
140e9ad4ca | ||
|
|
2296254b17 |
No files matched your search
+31
-122
@@ -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
|
||||
@@ -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`.",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user