Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
326 lines
11 KiB
TypeScript
326 lines
11 KiB
TypeScript
// E2E Shell Tempfiles tests cover e2e shell tempfiles script behavior.
|
|
import { spawnSync } from "node:child_process";
|
|
import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
async function listShellScripts(dir: string): Promise<string[]> {
|
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
const scripts: string[] = [];
|
|
|
|
for (const entry of entries.toSorted((a, b) => a.name.localeCompare(b.name))) {
|
|
const entryPath = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
scripts.push(...(await listShellScripts(entryPath)));
|
|
} else if (entry.isFile() && entry.name.endsWith(".sh")) {
|
|
scripts.push(entryPath);
|
|
}
|
|
}
|
|
|
|
return scripts;
|
|
}
|
|
|
|
async function extractClawhubSkillInstallVerifier(): Promise<string> {
|
|
const script = await readFile("scripts/e2e/lib/skills/clawhub-install-proof.sh", "utf8");
|
|
const marker =
|
|
'node --input-type=module - "$OPENCLAW_CONFIG_PATH" "$skill_dir" "$origin_json" "$lock_json" "$info_json" "$slug" <<\'NODE\'\n';
|
|
const start = script.indexOf(marker);
|
|
if (start === -1) {
|
|
throw new Error("ClawHub skill install verifier heredoc was not found");
|
|
}
|
|
const verifierStart = start + marker.length;
|
|
const verifierEnd = script.indexOf("\nNODE", verifierStart);
|
|
if (verifierEnd === -1) {
|
|
throw new Error("ClawHub skill install verifier heredoc was not terminated");
|
|
}
|
|
return script.slice(verifierStart, verifierEnd);
|
|
}
|
|
|
|
describe("e2e shell tempfile hygiene", () => {
|
|
it("does not allocate FIFO paths with mktemp -u", async () => {
|
|
const offenders: string[] = [];
|
|
|
|
for (const scriptPath of await listShellScripts("scripts/e2e")) {
|
|
const contents = await readFile(path.resolve(scriptPath), "utf8");
|
|
if (contents.includes("mktemp -u")) {
|
|
offenders.push(scriptPath);
|
|
}
|
|
}
|
|
|
|
expect(offenders).toEqual([]);
|
|
});
|
|
|
|
it("preserves wizard exit status when reporting failures", async () => {
|
|
const tempRoot = await mkdtemp(path.join(tmpdir(), "openclaw-onboard-status-test-"));
|
|
const fixturePath = path.join(tempRoot, "wizard-status.sh");
|
|
await writeFile(
|
|
fixturePath,
|
|
`#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
export OPENCLAW_ONBOARD_SCENARIO_SOURCE_ONLY=1
|
|
export OPENCLAW_ONBOARD_E2E_TMPDIR=${JSON.stringify(tempRoot)}
|
|
OPENCLAW_ENTRY=node
|
|
openclaw_test_state_create() { :; }
|
|
source scripts/e2e/lib/onboard/scenario.sh
|
|
|
|
openclaw_e2e_run_script_with_pty() {
|
|
local _command="$1"
|
|
local log_path="$2"
|
|
printf 'fake wizard log\\n' >"$log_path"
|
|
exit 7
|
|
}
|
|
|
|
send_noop() { :; }
|
|
|
|
run_wizard_cmd failing-wizard fake-state "node fake-wizard" send_noop false
|
|
`,
|
|
);
|
|
|
|
try {
|
|
const result = spawnSync("bash", [fixturePath], {
|
|
cwd: process.cwd(),
|
|
encoding: "utf8",
|
|
});
|
|
const output = `${result.stdout}\n${result.stderr}`;
|
|
|
|
expect(result.status).toBe(7);
|
|
expect(output).toContain("Wizard exited with status 7");
|
|
expect(output).toContain("fake wizard log");
|
|
} finally {
|
|
await rm(tempRoot, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it("checks local onboarding logs for systemd noise", async () => {
|
|
const contents = await readFile("scripts/e2e/lib/onboard/scenario.sh", "utf8");
|
|
|
|
expect(contents).toContain(
|
|
'ONBOARD_TMP_DIR="$(mktemp -d "$ONBOARD_TMP_ROOT/openclaw-onboard.XXXXXX")"',
|
|
);
|
|
expect(contents).toContain('OPENCLAW_E2E_LOG_DIR="$ONBOARD_TMP_DIR/logs"');
|
|
expect(contents).toContain('GATEWAY_LOG_PATH="$ONBOARD_TMP_DIR/gateway-e2e.log"');
|
|
expect(contents).not.toContain("/tmp/gateway-e2e.log");
|
|
expect(contents).toContain('validate_local_basic_log "$OPENCLAW_E2E_LAST_LOG_PATH"');
|
|
expect(contents).not.toContain(
|
|
"validate_local_basic_log /tmp/openclaw-onboard-local-basic.log",
|
|
);
|
|
expect(contents).toContain(
|
|
'openclaw_e2e_assert_log_not_contains "$log_path" "systemctl --user unavailable"',
|
|
);
|
|
});
|
|
|
|
it("probes onboarding gateway readiness through TCP", async () => {
|
|
const tempRoot = await mkdtemp(path.join(tmpdir(), "openclaw-onboard-gateway-log-"));
|
|
const fixturePath = path.join(tempRoot, "gateway-log.sh");
|
|
await writeFile(
|
|
fixturePath,
|
|
`#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
export OPENCLAW_ONBOARD_SCENARIO_SOURCE_ONLY=1
|
|
export OPENCLAW_ONBOARD_E2E_TMPDIR=${JSON.stringify(tempRoot)}
|
|
OPENCLAW_ENTRY=node
|
|
source scripts/e2e/lib/onboard/scenario.sh
|
|
|
|
openclaw_e2e_probe_tcp() { return 0; }
|
|
sleep 30 &
|
|
GATEWAY_PID="$!"
|
|
printf 'listening on ws://127.0.0.1:18789\\n' >"$GATEWAY_LOG_PATH"
|
|
wait_for_gateway
|
|
case "$GATEWAY_LOG_PATH" in
|
|
"$ONBOARD_TMP_DIR"/*) ;;
|
|
*) echo "gateway log escaped scratch root: $GATEWAY_LOG_PATH" >&2; exit 1 ;;
|
|
esac
|
|
cleanup_onboard_artifacts
|
|
test ! -e "$ONBOARD_TMP_DIR"
|
|
`,
|
|
);
|
|
|
|
try {
|
|
const result = spawnSync("bash", [fixturePath], {
|
|
cwd: process.cwd(),
|
|
encoding: "utf8",
|
|
});
|
|
|
|
expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0);
|
|
} finally {
|
|
await rm(tempRoot, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it("rejects onboarding gateway readiness when the TCP probe fails", async () => {
|
|
const tempRoot = await mkdtemp(path.join(tmpdir(), "openclaw-onboard-gateway-tcp-"));
|
|
const fixturePath = path.join(tempRoot, "gateway-tcp.sh");
|
|
await writeFile(
|
|
fixturePath,
|
|
`#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
export OPENCLAW_ONBOARD_SCENARIO_SOURCE_ONLY=1
|
|
export OPENCLAW_ONBOARD_E2E_TMPDIR=${JSON.stringify(tempRoot)}
|
|
export OPENCLAW_ONBOARD_GATEWAY_WAIT_ATTEMPTS=2
|
|
export OPENCLAW_ONBOARD_GATEWAY_WAIT_INTERVAL_S=0.1
|
|
OPENCLAW_ENTRY=node
|
|
source scripts/e2e/lib/onboard/scenario.sh
|
|
|
|
openclaw_e2e_probe_tcp() { return 1; }
|
|
sleep 30 &
|
|
GATEWAY_PID="$!"
|
|
printf 'listening on ws://127.0.0.1:18789\\n' >"$GATEWAY_LOG_PATH"
|
|
if wait_for_gateway; then
|
|
echo "gateway readiness passed without TCP reachability" >&2
|
|
cleanup_onboard_artifacts
|
|
exit 1
|
|
fi
|
|
cleanup_onboard_artifacts
|
|
test ! -e "$ONBOARD_TMP_DIR"
|
|
`,
|
|
);
|
|
|
|
try {
|
|
const result = spawnSync("bash", [fixturePath], {
|
|
cwd: process.cwd(),
|
|
encoding: "utf8",
|
|
});
|
|
|
|
expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0);
|
|
expect(result.stdout).toContain("Gateway failed to start");
|
|
expect(result.stdout).toContain("TCP probe never succeeded");
|
|
} finally {
|
|
await rm(tempRoot, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it("rejects invalid onboarding gateway wait attempts before probing", async () => {
|
|
const tempRoot = await mkdtemp(path.join(tmpdir(), "openclaw-onboard-gateway-attempts-"));
|
|
const fixturePath = path.join(tempRoot, "gateway-attempts.sh");
|
|
await writeFile(
|
|
fixturePath,
|
|
`#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
export OPENCLAW_ONBOARD_SCENARIO_SOURCE_ONLY=1
|
|
export OPENCLAW_ONBOARD_E2E_TMPDIR=${JSON.stringify(tempRoot)}
|
|
export OPENCLAW_ONBOARD_GATEWAY_WAIT_ATTEMPTS=2x
|
|
OPENCLAW_ENTRY=node
|
|
source scripts/e2e/lib/onboard/scenario.sh
|
|
|
|
openclaw_e2e_probe_tcp() {
|
|
echo "probe should not run" >&2
|
|
return 1
|
|
}
|
|
set +e
|
|
wait_for_gateway
|
|
status="$?"
|
|
set -e
|
|
cleanup_onboard_artifacts
|
|
exit "$status"
|
|
`,
|
|
);
|
|
|
|
try {
|
|
const result = spawnSync("bash", [fixturePath], {
|
|
cwd: process.cwd(),
|
|
encoding: "utf8",
|
|
});
|
|
|
|
expect(result.status).toBe(2);
|
|
expect(result.stderr).toContain("invalid OPENCLAW_ONBOARD_GATEWAY_WAIT_ATTEMPTS: 2x");
|
|
expect(result.stderr).not.toContain("probe should not run");
|
|
} finally {
|
|
await rm(tempRoot, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it("removes fallback ClawHub skill install HOME on failure", async () => {
|
|
const tempRoot = await mkdtemp(path.join(tmpdir(), "openclaw-clawhub-home-test-"));
|
|
const fakeBin = path.join(tempRoot, "bin");
|
|
const scratchRoot = path.join(tempRoot, "scratch");
|
|
await mkdir(fakeBin, { recursive: true });
|
|
await mkdir(scratchRoot, { recursive: true });
|
|
await writeFile(
|
|
path.join(fakeBin, "pnpm"),
|
|
`#!/usr/bin/env bash
|
|
exit 42
|
|
`,
|
|
{ mode: 0o755 },
|
|
);
|
|
|
|
try {
|
|
const result = spawnSync("bash", ["scripts/e2e/lib/skills/clawhub-install-proof.sh"], {
|
|
cwd: process.cwd(),
|
|
encoding: "utf8",
|
|
env: {
|
|
...process.env,
|
|
OPENCLAW_CURRENT_PACKAGE_TGZ: "",
|
|
OPENCLAW_TEST_STATE_SCRIPT_B64: "",
|
|
PATH: `${fakeBin}:${process.env.PATH ?? ""}`,
|
|
TMPDIR: scratchRoot,
|
|
},
|
|
});
|
|
|
|
expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(42);
|
|
const scratchEntries = await readdir(scratchRoot);
|
|
expect(
|
|
scratchEntries.filter((entry) => entry.startsWith("openclaw-skill-install-home.")),
|
|
).toEqual([]);
|
|
} finally {
|
|
await rm(tempRoot, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it("rejects ClawHub skill info paths that only share a resolved prefix", async () => {
|
|
const tempRoot = await mkdtemp(path.join(tmpdir(), "openclaw-clawhub-info-path-"));
|
|
const workspaceDir = path.join(tempRoot, "workspace");
|
|
const slug = "demo";
|
|
const skillDir = path.join(workspaceDir, "skills", slug);
|
|
const escapedInfoPath = path.join(workspaceDir, "skills", `${slug}-escape`, "SKILL.md");
|
|
const configPath = path.join(tempRoot, "openclaw.json");
|
|
const originPath = path.join(skillDir, ".clawhub", "origin.json");
|
|
const lockPath = path.join(workspaceDir, ".clawhub", "lock.json");
|
|
const infoPath = path.join(tempRoot, "info.json");
|
|
|
|
try {
|
|
await mkdir(path.dirname(originPath), { recursive: true });
|
|
await mkdir(path.dirname(lockPath), { recursive: true });
|
|
await writeFile(path.join(skillDir, "SKILL.md"), `---\nname: Demo\n---\n`);
|
|
await writeFile(
|
|
configPath,
|
|
`${JSON.stringify({ skills: { install: { allowUploadedArchives: false } } })}\n`,
|
|
);
|
|
await writeFile(
|
|
originPath,
|
|
`${JSON.stringify({
|
|
installedVersion: "1.0.0",
|
|
registry: "https://clawhub.ai",
|
|
slug,
|
|
})}\n`,
|
|
);
|
|
await writeFile(
|
|
lockPath,
|
|
`${JSON.stringify({ skills: { [slug]: { version: "1.0.0" } } })}\n`,
|
|
);
|
|
await writeFile(
|
|
infoPath,
|
|
`${JSON.stringify({ filePath: escapedInfoPath, skillKey: "wrong-skill" })}\n`,
|
|
);
|
|
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
["--input-type=module", "-", configPath, skillDir, originPath, lockPath, infoPath, slug],
|
|
{
|
|
encoding: "utf8",
|
|
input: await extractClawhubSkillInstallVerifier(),
|
|
},
|
|
);
|
|
|
|
expect(result.status).not.toBe(0);
|
|
expect(result.stderr).toContain("skills info did not report installed skill demo");
|
|
} finally {
|
|
await rm(tempRoot, { force: true, recursive: true });
|
|
}
|
|
});
|
|
});
|