mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
fix: harden Mantis Slack desktop gateway proof
This commit is contained in:
@@ -208,7 +208,15 @@ describe("mantis Slack desktop smoke runtime", () => {
|
||||
await fs.mkdir(outputDir as string, { recursive: true });
|
||||
if (!String(outputDir).endsWith("slack-qa/")) {
|
||||
await fs.writeFile(path.join(outputDir as string, "slack-desktop-smoke.png"), "png");
|
||||
await fs.writeFile(path.join(outputDir as string, "remote-metadata.json"), "{}\n");
|
||||
await fs.writeFile(
|
||||
path.join(outputDir as string, "remote-metadata.json"),
|
||||
`${JSON.stringify({
|
||||
gatewayAlive: true,
|
||||
gatewayPid: "1234",
|
||||
openedUrl: "https://app.slack.com/client/TLEASED/CLEASED",
|
||||
qaExitCode: 0,
|
||||
})}\n`,
|
||||
);
|
||||
await fs.writeFile(path.join(outputDir as string, "slack-desktop-command.log"), "qa\n");
|
||||
}
|
||||
}
|
||||
@@ -249,10 +257,82 @@ describe("mantis Slack desktop smoke runtime", () => {
|
||||
const remoteScript = runCommand?.args.at(-1);
|
||||
expect(remoteScript).toContain("setup_gateway=1");
|
||||
expect(remoteScript).toContain("openclaw gateway run");
|
||||
expect(remoteScript).toContain('</dev/null >"$out/openclaw-gateway.log"');
|
||||
expect(remoteScript).toContain('kill -0 "$gateway_pid"');
|
||||
expect(remoteScript).toContain('disown "$gateway_pid"');
|
||||
expect(fetchMock.mock.calls.map(([url]) => describeFetchInput(url))).toEqual([
|
||||
"https://example.convex.site/qa-credentials/v1/acquire",
|
||||
"https://example.convex.site/qa-credentials/v1/release",
|
||||
]);
|
||||
const summary = JSON.parse(await fs.readFile(result.summaryPath, "utf8")) as {
|
||||
slackUrl: string;
|
||||
};
|
||||
expect(summary.slackUrl).toBe("https://app.slack.com/client/TLEASED/CLEASED");
|
||||
});
|
||||
|
||||
it("passes gateway setup when Crabbox returns non-zero after remote metadata proves success", async () => {
|
||||
const runner = vi.fn(async (command: string, args: readonly string[]) => {
|
||||
if (command === "/tmp/crabbox" && args[0] === "warmup") {
|
||||
return { stdout: "ready lease cbx_cafe123\n", stderr: "" };
|
||||
}
|
||||
if (command === "/tmp/crabbox" && args[0] === "inspect") {
|
||||
return {
|
||||
stdout: `${JSON.stringify({
|
||||
host: "203.0.113.10",
|
||||
id: "cbx_cafe123",
|
||||
provider: "hetzner",
|
||||
sshKey: "/tmp/key",
|
||||
sshPort: "2222",
|
||||
sshUser: "crabbox",
|
||||
state: "active",
|
||||
})}\n`,
|
||||
stderr: "",
|
||||
};
|
||||
}
|
||||
if (command === "/tmp/crabbox" && args[0] === "run") {
|
||||
throw new Error("remote command exited 1");
|
||||
}
|
||||
if (command === "rsync") {
|
||||
const outputDir = args.at(-1);
|
||||
await fs.mkdir(outputDir as string, { recursive: true });
|
||||
if (!String(outputDir).endsWith("slack-qa/")) {
|
||||
await fs.writeFile(path.join(outputDir as string, "slack-desktop-smoke.png"), "png");
|
||||
await fs.writeFile(
|
||||
path.join(outputDir as string, "remote-metadata.json"),
|
||||
`${JSON.stringify({
|
||||
gatewayAlive: true,
|
||||
gatewayPid: "4321",
|
||||
openedUrl: "https://app.slack.com/client/TOK/COK",
|
||||
qaExitCode: 0,
|
||||
})}\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return { stdout: "", stderr: "" };
|
||||
});
|
||||
|
||||
const result = await runMantisSlackDesktopSmoke({
|
||||
commandRunner: runner,
|
||||
crabboxBin: "/tmp/crabbox",
|
||||
env: {
|
||||
OPENAI_API_KEY: "openai-runtime-key",
|
||||
OPENCLAW_MANTIS_SLACK_APP_TOKEN: "xapp-direct",
|
||||
OPENCLAW_MANTIS_SLACK_BOT_TOKEN: "xoxb-direct",
|
||||
PATH: process.env.PATH,
|
||||
},
|
||||
gatewaySetup: true,
|
||||
now: () => new Date("2026-05-04T14:30:00.000Z"),
|
||||
outputDir: ".artifacts/qa-e2e/mantis/slack-desktop-gateway-metadata",
|
||||
repoRoot,
|
||||
});
|
||||
|
||||
expect(result.status).toBe("pass");
|
||||
const summary = JSON.parse(await fs.readFile(result.summaryPath, "utf8")) as {
|
||||
status: string;
|
||||
warning?: string;
|
||||
};
|
||||
expect(summary.status).toBe("pass");
|
||||
expect(summary.warning).toContain("remote command exited 1");
|
||||
});
|
||||
|
||||
it("copies the screenshot before reporting a failed remote Slack QA run", async () => {
|
||||
|
||||
@@ -100,6 +100,14 @@ type MantisSlackDesktopSmokeSummary = {
|
||||
slackUrl?: string;
|
||||
startedAt: string;
|
||||
status: "pass" | "fail";
|
||||
warning?: string;
|
||||
};
|
||||
|
||||
type SlackDesktopRemoteMetadata = {
|
||||
gatewayAlive?: boolean;
|
||||
gatewayPid?: string;
|
||||
openedUrl?: string;
|
||||
qaExitCode?: number;
|
||||
};
|
||||
|
||||
const DEFAULT_PROVIDER = "hetzner";
|
||||
@@ -183,6 +191,31 @@ async function pathExists(filePath: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function readRemoteMetadata(
|
||||
outputDir: string,
|
||||
): Promise<SlackDesktopRemoteMetadata | undefined> {
|
||||
const metadataPath = path.join(outputDir, "remote-metadata.json");
|
||||
if (!(await pathExists(metadataPath))) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(await fs.readFile(metadataPath, "utf8")) as unknown;
|
||||
if (!parsed || typeof parsed !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const candidate = parsed as Record<string, unknown>;
|
||||
return {
|
||||
gatewayAlive:
|
||||
typeof candidate.gatewayAlive === "boolean" ? candidate.gatewayAlive : undefined,
|
||||
gatewayPid: typeof candidate.gatewayPid === "string" ? candidate.gatewayPid : undefined,
|
||||
openedUrl: typeof candidate.openedUrl === "string" ? candidate.openedUrl : undefined,
|
||||
qaExitCode: typeof candidate.qaExitCode === "number" ? candidate.qaExitCode : undefined,
|
||||
};
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveCrabboxBin(params: {
|
||||
env: NodeJS.ProcessEnv;
|
||||
explicit?: string;
|
||||
@@ -444,7 +477,8 @@ if [ "$setup_gateway" = "1" ]; then
|
||||
--window-size=1440,1000 \
|
||||
--window-position=0,0 \
|
||||
--class=mantis-slack-desktop-smoke \
|
||||
"$slack_url" >"$out/chrome.log" 2>&1 &
|
||||
"$slack_url" </dev/null >"$out/chrome.log" 2>&1 &
|
||||
disown "$!" >/dev/null 2>&1 || true
|
||||
else
|
||||
"$browser_bin" \
|
||||
--user-data-dir="$profile" \
|
||||
@@ -496,9 +530,16 @@ qa_status=0
|
||||
MANTIS_SLACK_PATCH
|
||||
pnpm openclaw config patch --file "$out/slack.socket.patch.json5" --dry-run
|
||||
pnpm openclaw config patch --file "$out/slack.socket.patch.json5"
|
||||
nohup pnpm openclaw gateway run --dev --allow-unconfigured --port 38973 --cli-backend-logs >"$out/openclaw-gateway.log" 2>&1 &
|
||||
echo "$!" >"$out/openclaw-gateway.pid"
|
||||
nohup pnpm openclaw gateway run --dev --allow-unconfigured --port 38973 --cli-backend-logs </dev/null >"$out/openclaw-gateway.log" 2>&1 &
|
||||
gateway_pid="$!"
|
||||
echo "$gateway_pid" >"$out/openclaw-gateway.pid"
|
||||
sleep 12
|
||||
if ! kill -0 "$gateway_pid" >/dev/null 2>&1; then
|
||||
echo "OpenClaw gateway exited during startup." >&2
|
||||
wait "$gateway_pid" || true
|
||||
exit 1
|
||||
fi
|
||||
disown "$gateway_pid" >/dev/null 2>&1 || true
|
||||
else
|
||||
qa_args=(openclaw qa slack --repo-root . --output-dir "$out/slack-qa" --provider-mode "$provider_mode" --model "$primary_model" --alt-model "$alternate_model" --credential-source "$credential_source" --credential-role "$credential_role")
|
||||
if [ "$fast_mode" = "1" ]; then
|
||||
@@ -522,6 +563,8 @@ cat >"$out/remote-metadata.json" <<MANTIS_REMOTE_METADATA
|
||||
"display": "$DISPLAY",
|
||||
"openedUrl": "$slack_url",
|
||||
"gatewaySetup": $setup_gateway,
|
||||
"gatewayAlive": $(if [ "$setup_gateway" = "1" ] && [ -f "$out/openclaw-gateway.pid" ] && kill -0 "$(cat "$out/openclaw-gateway.pid")" >/dev/null 2>&1; then echo true; else echo false; fi),
|
||||
"gatewayPid": "$(if [ -f "$out/openclaw-gateway.pid" ]; then cat "$out/openclaw-gateway.pid"; fi)",
|
||||
"gatewayPort": 38973,
|
||||
"qaExitCode": $qa_status,
|
||||
"credentialSource": "$credential_source",
|
||||
@@ -776,6 +819,7 @@ export async function runMantisSlackDesktopSmoke(
|
||||
let screenshotPath: string | undefined;
|
||||
let slackQaDir: string | undefined;
|
||||
let videoPath: string | undefined;
|
||||
let remoteMetadata: SlackDesktopRemoteMetadata | undefined;
|
||||
|
||||
try {
|
||||
const preparedCredentialEnv = await prepareGatewayCredentialEnv({
|
||||
@@ -855,13 +899,22 @@ export async function runMantisSlackDesktopSmoke(
|
||||
if (!(await pathExists(videoPath))) {
|
||||
videoPath = undefined;
|
||||
}
|
||||
remoteMetadata = await readRemoteMetadata(outputDir);
|
||||
slackQaDir = path.join(outputDir, "slack-qa");
|
||||
if (!(await pathExists(screenshotPath))) {
|
||||
throw new Error("Slack desktop screenshot was not copied back from Crabbox.");
|
||||
}
|
||||
if (remoteRunError) {
|
||||
const gatewaySetupCompleted =
|
||||
gatewaySetup && remoteMetadata?.qaExitCode === 0 && remoteMetadata.gatewayAlive === true;
|
||||
if (remoteRunError && !gatewaySetupCompleted) {
|
||||
throw remoteRunError;
|
||||
}
|
||||
if (gatewaySetup && !gatewaySetupCompleted) {
|
||||
throw new Error("Slack desktop gateway setup did not report a live OpenClaw gateway.");
|
||||
}
|
||||
const ignoredRemoteRunError = remoteRunError
|
||||
? `Crabbox returned a non-zero command status after the gateway setup completed: ${formatErrorMessage(remoteRunError)}`
|
||||
: undefined;
|
||||
summary = {
|
||||
artifacts: {
|
||||
reportPath,
|
||||
@@ -882,9 +935,10 @@ export async function runMantisSlackDesktopSmoke(
|
||||
finishedAt: new Date().toISOString(),
|
||||
outputDir,
|
||||
remoteOutputDir,
|
||||
slackUrl,
|
||||
slackUrl: trimToValue(remoteMetadata?.openedUrl) ?? slackUrl,
|
||||
startedAt: startedAt.toISOString(),
|
||||
status: "pass",
|
||||
warning: ignoredRemoteRunError,
|
||||
};
|
||||
return {
|
||||
outputDir,
|
||||
|
||||
Reference in New Issue
Block a user