mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 11:55:47 -06:00
234df15a6d
* build(deps): refresh dependencies after cooldown Apply dependency, toolchain, action, image, and exact tool updates released by the inclusive 2026-08-16 seven-day cutoff. Adapt owner boundaries for the resulting CUA, logging, Teams, Markdown, native, and test-harness contract changes while retaining versions blocked by upstream compatibility constraints. * fix(ui): align markdown renderer env typing * fix(deps): align postcss and mistral peer contracts * fix(deps): repair refreshed dependency contracts * fix(deps): retain tslog startup budget * fix(ci): verify Android tools with SHA-256 * fix(ci): fence Android SDK cache version
283 lines
12 KiB
TypeScript
283 lines
12 KiB
TypeScript
import { spawn, spawnSync } from "node:child_process";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import {
|
|
runSutContainerAction,
|
|
waitForLog,
|
|
writeSutConfig,
|
|
} from "../../scripts/e2e/telegram-mantis-sut.ts";
|
|
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
|
|
|
|
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
|
const linuxIt = process.platform === "linux" ? it : it.skip;
|
|
|
|
function sutSupervisorCommand(): string {
|
|
const source = fs.readFileSync("scripts/mantis/mantis-sut-container.sh", "utf8");
|
|
const match = source.match(/readonly sut_command='([\s\S]*?)'\n\nrequire_active_sut\(\)/u);
|
|
if (!match?.[1]) {
|
|
throw new Error("Could not extract the SUT gateway supervisor.");
|
|
}
|
|
return match[1];
|
|
}
|
|
|
|
describe("Telegram Mantis SUT", () => {
|
|
it("keeps stderr when a container action is terminated", () => {
|
|
expect(() =>
|
|
runSutContainerAction("stop", "openclaw-telegram-sut-test", "/tmp/runtime", () => ({
|
|
signal: "SIGTERM",
|
|
status: null,
|
|
stderr: "permission denied while opening the Docker socket",
|
|
})),
|
|
).toThrow("permission denied while opening the Docker socket");
|
|
});
|
|
|
|
it("reports a silent container daemon exit instead of a mock-openai timeout", async () => {
|
|
const root = tempDirs.make("telegram-mantis-start-failure-");
|
|
const mockLog = path.join(root, "mock-openai.log");
|
|
const daemonLog = path.join(root, "sut-container.log");
|
|
fs.writeFileSync(mockLog, "");
|
|
fs.writeFileSync(daemonLog, "");
|
|
const child = spawn(process.execPath, ["-e", "process.exit(23)"], { stdio: "ignore" });
|
|
|
|
await expect(
|
|
waitForLog(mockLog, /mock-openai listening/u, "mock-openai", 30_000, {
|
|
daemon: { child },
|
|
logPath: daemonLog,
|
|
}),
|
|
).rejects.toThrow(
|
|
"Container-isolated SUT exited with exit code 23 before mock-openai became ready.\nsut-container.log: <empty>",
|
|
);
|
|
}, 40_000);
|
|
|
|
linuxIt("releases the runtime claim before deadline-exposed removal", () => {
|
|
const root = tempDirs.make("telegram-mantis-cleanup-");
|
|
const binDir = path.join(root, "bin");
|
|
const runtimeParent = path.join(root, "runtime");
|
|
const runtimeRootFile = path.join(root, "runtime-root");
|
|
const released = path.join(root, "released");
|
|
const containerName = "openclaw-telegram-sut-dead";
|
|
const runtimeSource = "/tmp/openclaw-tg-crabbox-sut-Dead";
|
|
fs.mkdirSync(binDir);
|
|
fs.mkdirSync(path.join(runtimeParent, "claims"), { recursive: true });
|
|
fs.writeFileSync(runtimeRootFile, `${runtimeParent}\n`);
|
|
const ownerPid = Number(
|
|
spawnSync(
|
|
"/bin/sh",
|
|
[
|
|
"-c",
|
|
`/usr/bin/setsid /bin/bash -c ${JSON.stringify(`trap 'touch ${JSON.stringify(released)}; exit 0' TERM; touch ${JSON.stringify(path.join(root, "ready"))}; while :; do sleep 10; done`)} >/dev/null 2>&1 & pid=$!; while [ ! -e ${JSON.stringify(path.join(root, "ready"))} ]; do :; done; echo $pid`,
|
|
],
|
|
{ encoding: "utf8" },
|
|
).stdout.trim(),
|
|
);
|
|
const stat = fs.readFileSync(`/proc/${ownerPid}/stat`, "utf8");
|
|
const startTime = stat.slice(stat.lastIndexOf(") ") + 2).split(" ")[19];
|
|
const claimPath = path.join(runtimeParent, "claims", `${containerName}.claim`);
|
|
fs.writeFileSync(claimPath, `${runtimeSource}\t${ownerPid}\t${ownerPid}\t${startTime}\n`, {
|
|
mode: 0o400,
|
|
});
|
|
const docker = path.join(binDir, "docker");
|
|
fs.writeFileSync(
|
|
docker,
|
|
`#!/bin/sh\ncase "$1 $2" in\n "container ls") echo ${containerName} ;;\n "rm --force") sleep 5; exit 1 ;;\n "network ls") exit 0 ;;\n *) exit 1 ;;\nesac\n`,
|
|
{ mode: 0o755 },
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(binDir, "install"),
|
|
'#!/bin/bash\ndestination="${!#}"\n: >"$destination"\nchmod 0400 "$destination"\n',
|
|
{ mode: 0o755 },
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(binDir, "stat"),
|
|
`#!/bin/sh\nif [ "$1" = -c ] && [ "$2" = %u ] && [ "$3" = ${JSON.stringify(claimPath)} ]; then echo 0; else exec /usr/bin/stat "$@"; fi\n`,
|
|
{ mode: 0o755 },
|
|
);
|
|
const sutScript = path.join(root, "mantis-sut-container.sh");
|
|
const source = fs
|
|
.readFileSync("scripts/mantis/mantis-sut-container.sh", "utf8")
|
|
.replace(
|
|
'readonly runtime_root_file="/etc/openclaw-mantis-sut-runtime-root"',
|
|
`readonly runtime_root_file=${JSON.stringify(runtimeRootFile)}`,
|
|
)
|
|
.replace(
|
|
'readonly docker_bin="/usr/bin/docker"',
|
|
`readonly docker_bin=${JSON.stringify(docker)}`,
|
|
)
|
|
.replace("--kill-after=5s 30s", "--kill-after=1s 1s");
|
|
// A substitution that stops matching would silently point the test at the real Docker
|
|
// binary and the real 30s deadline, so it would still pass while proving nothing.
|
|
expect(source).toContain(runtimeRootFile);
|
|
expect(source).toContain(docker);
|
|
expect(source).toContain("--kill-after=1s 1s");
|
|
fs.writeFileSync(sutScript, source, { mode: 0o755 });
|
|
|
|
try {
|
|
const result = spawnSync(sutScript, ["stop", containerName, runtimeSource], {
|
|
env: { ...process.env, PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ""}` },
|
|
});
|
|
expect(result.status).toBe(124);
|
|
expect(fs.existsSync(released)).toBe(true);
|
|
} finally {
|
|
try {
|
|
process.kill(-ownerPid, "SIGKILL");
|
|
} catch {}
|
|
}
|
|
});
|
|
|
|
linuxIt("waits for the claimed runtime owner before returning from stop", () => {
|
|
const root = tempDirs.make("telegram-mantis-stop-sync-");
|
|
const binDir = path.join(root, "bin");
|
|
const runtimeParent = path.join(root, "runtime");
|
|
const runtimeRootFile = path.join(root, "runtime-root");
|
|
const released = path.join(root, "released");
|
|
const containerName = "openclaw-telegram-sut-feed";
|
|
const runtimeSource = "/tmp/openclaw-tg-crabbox-sut-Sync";
|
|
fs.mkdirSync(binDir);
|
|
fs.mkdirSync(path.join(runtimeParent, "claims"), { recursive: true });
|
|
fs.writeFileSync(runtimeRootFile, `${runtimeParent}\n`);
|
|
const ownerPid = Number(
|
|
spawnSync(
|
|
"/bin/sh",
|
|
[
|
|
"-c",
|
|
`/usr/bin/setsid /bin/bash -c ${JSON.stringify(`trap 'sleep 1; touch ${JSON.stringify(released)}; exit 0' TERM; touch ${JSON.stringify(path.join(root, "ready"))}; while :; do sleep 10; done`)} >/dev/null 2>&1 & pid=$!; while [ ! -e ${JSON.stringify(path.join(root, "ready"))} ]; do :; done; echo $pid`,
|
|
],
|
|
{ encoding: "utf8" },
|
|
).stdout.trim(),
|
|
);
|
|
const stat = fs.readFileSync(`/proc/${ownerPid}/stat`, "utf8");
|
|
const startTime = stat.slice(stat.lastIndexOf(") ") + 2).split(" ")[19];
|
|
const claimPath = path.join(runtimeParent, "claims", `${containerName}.claim`);
|
|
fs.writeFileSync(claimPath, `${runtimeSource}\t${ownerPid}\t${ownerPid}\t${startTime}\n`, {
|
|
mode: 0o400,
|
|
});
|
|
const docker = path.join(binDir, "docker");
|
|
fs.writeFileSync(
|
|
docker,
|
|
'#!/bin/sh\ncase "$1 $2" in\n "container ls"|"network ls") exit 0 ;;\n *) exit 1 ;;\nesac\n',
|
|
{ mode: 0o755 },
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(binDir, "install"),
|
|
'#!/bin/bash\ndestination="${!#}"\n: >"$destination"\nchmod 0400 "$destination"\n',
|
|
{ mode: 0o755 },
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(binDir, "stat"),
|
|
`#!/bin/sh\nif [ "$1" = -c ] && [ "$2" = %u ] && [ "$3" = ${JSON.stringify(claimPath)} ]; then echo 0; else exec /usr/bin/stat "$@"; fi\n`,
|
|
{ mode: 0o755 },
|
|
);
|
|
const sutScript = path.join(root, "mantis-sut-container.sh");
|
|
const source = fs
|
|
.readFileSync("scripts/mantis/mantis-sut-container.sh", "utf8")
|
|
.replace(
|
|
'readonly runtime_root_file="/etc/openclaw-mantis-sut-runtime-root"',
|
|
`readonly runtime_root_file=${JSON.stringify(runtimeRootFile)}`,
|
|
)
|
|
.replace(
|
|
'readonly docker_bin="/usr/bin/docker"',
|
|
`readonly docker_bin=${JSON.stringify(docker)}`,
|
|
);
|
|
expect(source).toContain(runtimeRootFile);
|
|
expect(source).toContain(docker);
|
|
fs.writeFileSync(sutScript, source, { mode: 0o755 });
|
|
|
|
try {
|
|
const result = spawnSync(sutScript, ["stop", containerName, runtimeSource], {
|
|
env: { ...process.env, PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ""}` },
|
|
});
|
|
expect(result.status, result.stderr.toString()).toBe(0);
|
|
expect(fs.existsSync(released)).toBe(true);
|
|
} finally {
|
|
try {
|
|
process.kill(-ownerPid, "SIGKILL");
|
|
} catch {}
|
|
}
|
|
});
|
|
|
|
it("relaunches the gateway only when a restart request exists", () => {
|
|
const root = tempDirs.make("telegram-mantis-supervisor-");
|
|
const binDir = path.join(root, "bin");
|
|
const countFile = path.join(root, "gateway-count");
|
|
const configPath = path.join(root, "openclaw.json");
|
|
const gatewayLog = path.join(root, "gateway.log");
|
|
fs.mkdirSync(binDir);
|
|
fs.writeFileSync(configPath, "{}\n");
|
|
fs.writeFileSync(
|
|
path.join(binDir, "node"),
|
|
`#!/bin/sh
|
|
count=0
|
|
if [ -f ${JSON.stringify(countFile)} ]; then count=$(cat ${JSON.stringify(countFile)}); fi
|
|
count=$((count + 1))
|
|
printf '%s\\n' "$count" > ${JSON.stringify(countFile)}
|
|
printf '[gateway] ready\\n'
|
|
if [ "\${RESTART_ON_FIRST:-0}" = 1 ] && [ "$count" -eq 1 ]; then
|
|
: > ${JSON.stringify(path.join(root, "gateway-restart.request"))}
|
|
exit 23
|
|
fi
|
|
exit "\${GATEWAY_EXIT_CODE:-0}"
|
|
`,
|
|
{ mode: 0o755 },
|
|
);
|
|
const run = (extraEnv: NodeJS.ProcessEnv) =>
|
|
spawnSync("/bin/sh", ["-c", sutSupervisorCommand()], {
|
|
cwd: root,
|
|
encoding: "utf8",
|
|
env: {
|
|
...process.env,
|
|
...extraEnv,
|
|
GATEWAY_LOG: gatewayLog,
|
|
OPENCLAW_CONFIG_PATH: configPath,
|
|
OPENCLAW_GATEWAY_PORT: "19879",
|
|
PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ""}`,
|
|
},
|
|
});
|
|
|
|
const restarted = run({ RESTART_ON_FIRST: "1" });
|
|
expect(restarted.status, restarted.stderr).toBe(0);
|
|
expect(fs.readFileSync(countFile, "utf8").trim()).toBe("2");
|
|
expect(fs.readFileSync(gatewayLog, "utf8")).toContain("[mantis] restarting gateway");
|
|
expect(fs.existsSync(path.join(root, "gateway.pid"))).toBe(false);
|
|
|
|
fs.rmSync(countFile);
|
|
fs.rmSync(gatewayLog);
|
|
const exited = run({ GATEWAY_EXIT_CODE: "23" });
|
|
expect(exited.status, exited.stderr).toBe(23);
|
|
expect(fs.readFileSync(countFile, "utf8").trim()).toBe("1");
|
|
expect(fs.readFileSync(gatewayLog, "utf8")).not.toContain("restarting gateway");
|
|
expect(fs.existsSync(path.join(root, "gateway.pid"))).toBe(false);
|
|
});
|
|
|
|
it("lets the proof agent patch the complete ephemeral gateway config", () => {
|
|
const outputDir = tempDirs.make("telegram-mantis-config-");
|
|
const { configPath } = writeSutConfig({
|
|
configPatch: {
|
|
channels: {
|
|
telegram: {
|
|
apiRoot: "https://example.invalid",
|
|
botToken: "not-the-sut-token",
|
|
streaming: { mode: "partial" },
|
|
},
|
|
},
|
|
session: { sendPolicy: { default: "deny" } },
|
|
},
|
|
gatewayPort: 19_879,
|
|
groupId: "-100123456789",
|
|
mockHost: "mock-openai",
|
|
mockPort: 19_882,
|
|
outputDir,
|
|
testerId: "12345",
|
|
});
|
|
|
|
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
expect(config.channels.telegram.apiRoot).toBe("https://example.invalid");
|
|
expect(config.channels.telegram.botToken).toBe("not-the-sut-token");
|
|
expect(config.channels.telegram.streaming).toEqual({ mode: "partial" });
|
|
expect(config.channels.telegram).not.toHaveProperty("replyToMode");
|
|
expect(config.commands.ownerAllowFrom).toEqual(["telegram:12345"]);
|
|
expect(config.models.providers.openai.baseUrl).toBe("http://mock-openai:19882/v1");
|
|
expect(config.session.sendPolicy).toEqual({ default: "deny" });
|
|
});
|
|
});
|