import { execFile } from "node:child_process"; import fs from "node:fs"; import net from "node:net"; import path from "node:path"; import { promisify } from "node:util"; import { afterEach, describe, expect, it } from "vitest"; import { publishableRecorderArtifacts, publishStartupFailure, } from "../../scripts/e2e/telegram-mantis-lane.ts"; import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js"; const execFileAsync = promisify(execFile); const tempDirs = useAutoCleanupTempDirTracker(afterEach); const laneScript = path.resolve("scripts/e2e/telegram-mantis-lane.ts"); function writeJson(file: string, value: unknown): void { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, `${JSON.stringify(value)}\n`); } async function setupHarness( options: { failEvents?: boolean; failRecorder?: boolean; userOnlyEvents?: boolean } = {}, ) { const root = tempDirs.make("telegram-mantis-lane-"); const outputRoot = path.join(root, "public"); const sessionRoot = path.join(root, "private"); const credentialFile = path.join(root, "credential.json"); const observerSocket = path.join(root, "observer.sock"); const recorderControlLog = path.join(root, "recorder-control.json"); const recorderLog = path.join(root, "recorder.log"); const recorderCommand = path.join(root, "recorder"); fs.mkdirSync(outputRoot); fs.mkdirSync(sessionRoot); writeJson(credentialFile, { groupId: "-100123456789", sutToken: "123456:secret-sut-token", testerUserId: "77", }); writeJson(path.join(root, "mock-response.json"), { chunkDelayMs: 0, text: "initial" }); fs.writeFileSync( recorderCommand, `#!/bin/sh\nprintf '%s\\n' "$*" >> ${JSON.stringify(recorderLog)}\ncp ${JSON.stringify(path.join(root, "mock-response.json"))} ${JSON.stringify(recorderControlLog)}\n${options.failRecorder ? "exit 1\n" : ""}`, { mode: 0o755 }, ); writeJson(path.join(sessionRoot, "candidate.active.json"), { attempt: 1, config: { mockResponse: "visible result" }, invocations: [{ args: {}, at: "2026-08-19T12:00:00.000Z", command: "start", cursor: 0 }], lane: "candidate", lastCursor: 0, observeSeconds: 0, observerJournal: path.join(root, "events.ndjson"), observerLog: path.join(root, "observer.log"), observerPidFile: path.join(root, "observer.pid.json"), observerSocket, privateDir: path.join(sessionRoot, "attempt"), recorderSession: path.join(sessionRoot, "attempt", "recorder.json"), repoRoot: "/prepared/candidate", sendCount: 0, startedAt: new Date().toISOString(), sut: { containerName: "openclaw-telegram-sut-test", gatewayLog: path.join(root, "gateway.log"), mockLog: path.join(root, "mock.log"), mockResponseControl: path.join(root, "mock-response.json"), requestLog: path.join(root, "requests.ndjson"), sutAttestation: { lane: "candidate", sha: "a".repeat(40) }, tempRoot: path.join(root, "sut"), }, }); const requests: Record[] = []; let cursor = 0; const server = net.createServer((socket) => { let input = ""; socket.setEncoding("utf8"); socket.on("data", (chunk) => { input += chunk.toString(); }); socket.on("end", () => { const request = JSON.parse(input) as Record; requests.push(request); if (request.command === "send") { cursor += 1; socket.end( `${JSON.stringify({ ok: true, cursor, sent: { actor: "user", kind: "message", messageId: "101", text: request.text } })}\n`, ); } else if (options.failEvents) { socket.end(`${JSON.stringify({ ok: false, error: "observer failed after send" })}\n`); } else { cursor += 2; socket.end( `${JSON.stringify({ ok: true, cursor, events: options.userOnlyEvents ? [{ actor: "user", kind: "message", messageId: "101", seq: cursor, text: "sent" }] : [ { actor: "bot", kind: "message", messageId: "102", seq: cursor - 1, text: "draft", }, { actor: "bot", kind: "edit", messageId: "102", seq: cursor, text: "final" }, ], })}\n`, ); } }); }); await new Promise((resolve, reject) => { server.once("error", reject); server.listen(observerSocket, resolve); }); return { close: () => new Promise((resolve, reject) => { server.close((error) => { if (error) { reject(error); } else { resolve(); } }); }), env: { ...process.env, OPENCLAW_MANTIS_CREDENTIAL_FILE: credentialFile, OPENCLAW_MANTIS_OUTPUT_ROOT: outputRoot, OPENCLAW_MANTIS_SESSION_ROOT: sessionRoot, OPENCLAW_TELEGRAM_DESKTOP_RECORDER_CMD: recorderCommand, }, outputRoot, recorderControlLog, recorderLog, requests, sessionRoot, }; } async function runLane(env: NodeJS.ProcessEnv, args: string[]) { return await execFileAsync(process.execPath, ["--import", "tsx", laneScript, ...args], { cwd: process.cwd(), env, }); } describe("Telegram Mantis free-form lane", () => { it("publishes only cropped visual evidence", () => { expect( publishableRecorderArtifacts({ desktopLog: "/private/desktop.log", ffmpegLog: "/private/ffmpeg.log", inspection1: "/private/inspection.png", previewGif: "/private/full.gif", previewGifCropped: "/private/cropped.gif", screenshot: "/private/cropped.png", trimmedVideo: "/private/full.mp4", trimmedVideoCropped: "/private/cropped.mp4", video: "/private/raw.mp4", }), ).toEqual({ inspection1: "/private/inspection.png", previewGifCropped: "/private/cropped.gif", screenshot: "/private/cropped.png", trimmedVideoCropped: "/private/cropped.mp4", }); }); it("promotes startup failures to the canonical trusted lane result", () => { const root = tempDirs.make("telegram-mantis-startup-failure-"); const outputRoot = path.join(root, "public"); const sessionRoot = path.join(root, "private"); fs.mkdirSync(outputRoot); fs.mkdirSync(sessionRoot); const startedAt = new Date().toISOString(); publishStartupFailure({ cleanupErrors: [], configRelative: "lane-config.json", error: new Error("desktop failed with 123456:secret-sut-token"), roots: { credentialFile: path.join(root, "credential.json"), outputRoot, sessionRoot, }, secret: "123456:secret-sut-token", startup: { attempt: 1, lane: "candidate", observerPidFile: path.join(sessionRoot, "observer.pid.json"), observerRequested: false, observerSocket: path.join(sessionRoot, "observer.sock"), privateDir: path.join(sessionRoot, "attempts", "candidate", "1"), recorderRequested: true, recorderSession: path.join(sessionRoot, "attempts", "candidate", "1", "recorder.json"), repoRoot: "/prepared/candidate", startedAt, }, sutAttestation: { lane: "candidate", sha: "a".repeat(40) }, }); const facts = JSON.parse(fs.readFileSync(path.join(sessionRoot, "candidate.json"), "utf8")); expect(facts).toMatchObject({ artifacts: {}, attempt: 1, cleanupErrors: [], error: "desktop failed with [redacted]", invocations: [ { args: { config: "lane-config.json", repoRoot: "/prepared/candidate" }, command: "start", cursor: 0, }, ], lane: "candidate", observation: { cursor: 0, events: [], observedSeconds: 0, truncated: false }, providerRequests: [], schemaVersion: 2, sendCount: 0, startedAt, status: "infra-error", sutAttestation: { lane: "candidate", sha: "a".repeat(40) }, }); expect( JSON.parse( fs.readFileSync( path.join(sessionRoot, "published", "candidate", "mantis-lane-facts.json"), "utf8", ), ), ).toEqual(facts); expect( JSON.parse( fs.readFileSync(path.join(outputRoot, "candidate", "mantis-lane-facts.json"), "utf8"), ), ).toEqual(facts); expect( JSON.parse( fs.readFileSync( path.join(outputRoot, "candidate", "telegram-user-crabbox-session-summary.json"), "utf8", ), ), ).toEqual({ artifacts: {}, status: "fail", sutAttestation: { lane: "candidate", sha: "a".repeat(40) }, }); }); it("lets the agent compose sends and continuous event observations", async () => { const harness = await setupHarness(); try { const result = await runLane(harness.env, [ "turn", "--lane", "candidate", "--text", "show progress", "--observe-seconds", "2", ]); expect(JSON.parse(result.stdout)).toMatchObject({ observed: { events: [ { actor: "bot", kind: "message", text: "draft" }, { actor: "bot", kind: "edit", text: "final" }, ], }, sent: { revealedMessageId: "101", sent: { actor: "user", messageId: "101" } }, }); expect(harness.requests).toEqual([ { command: "send", text: "show progress" }, { command: "events", seconds: 2, since: 1 }, ]); const state = JSON.parse( fs.readFileSync(path.join(harness.sessionRoot, "candidate.active.json"), "utf8"), ); expect(state).toMatchObject({ lastCursor: 3, observeSeconds: 2, sendCount: 1 }); expect(state.invocations.map((entry: { command: string }) => entry.command)).toEqual([ "start", "send", "reveal", "observe", ]); expect(fs.readFileSync(harness.recorderLog, "utf8")).toContain( "view --session attempt/recorder.json --message-id 101", ); expect(JSON.parse(fs.readFileSync(harness.recorderControlLog, "utf8"))).toMatchObject({ hold: true, }); expect( JSON.parse( fs.readFileSync( path.join(path.dirname(harness.outputRoot), "mock-response.json"), "utf8", ), ), ).toMatchObject({ hold: false }); expect(result.stdout).not.toContain("secret-sut-token"); } finally { await harness.close(); } }); it("keeps file inputs inside the public scenario directory", async () => { const harness = await setupHarness(); const outside = path.join(path.dirname(harness.outputRoot), "outside.txt"); fs.writeFileSync(outside, "not allowed"); try { await expect( runLane(harness.env, ["send", "--lane", "candidate", "--text-file", outside]), ).rejects.toThrow("--text-file must be inside the Mantis output directory"); expect(harness.requests).toEqual([]); } finally { await harness.close(); } }); it("retains the sent message when revealing it fails", async () => { const harness = await setupHarness({ failRecorder: true }); try { await expect( runLane(harness.env, ["send", "--lane", "candidate", "--text", "keep this send"]), ).rejects.toThrow(); const state = JSON.parse( fs.readFileSync(path.join(harness.sessionRoot, "candidate.active.json"), "utf8"), ); expect(state.sendCount).toBe(1); expect(state.invocations.at(-1)).toMatchObject({ command: "send" }); expect( JSON.parse( fs.readFileSync( path.join(path.dirname(harness.outputRoot), "mock-response.json"), "utf8", ), ), ).toMatchObject({ hold: false }); } finally { await harness.close(); } }); it("updates provider behavior through a private data file", async () => { const harness = await setupHarness(); const responseFile = path.join(harness.outputRoot, "response.txt"); fs.writeFileSync(responseFile, "stream this response"); try { const result = await runLane(harness.env, [ "mock", "--lane", "candidate", "--response-file", responseFile, "--chunk-delay-ms", "250", ]); expect(JSON.parse(result.stdout)).toMatchObject({ bytes: 20, chunkDelayMs: 250 }); expect( JSON.parse( fs.readFileSync( path.join(path.dirname(harness.outputRoot), "mock-response.json"), "utf8", ), ), ).toEqual({ chunkDelayMs: 250, text: "stream this response", }); expect(harness.requests).toEqual([]); } finally { await harness.close(); } }); it("passes arbitrary Responses API events to the mock provider", async () => { const harness = await setupHarness(); const eventsFile = path.join(harness.outputRoot, "response-events.json"); const events = [ { delta: "< / internal", type: "response.reasoning_text.delta" }, { delta: "VISIBLE", type: "response.output_text.delta" }, { response: { output: [], status: "completed" }, type: "response.completed" }, ]; fs.writeFileSync(eventsFile, JSON.stringify(events)); try { const result = await runLane(harness.env, [ "mock", "--lane", "candidate", "--response-events-file", eventsFile, ]); expect(JSON.parse(result.stdout)).toMatchObject({ events: 3 }); expect( JSON.parse( fs.readFileSync( path.join(path.dirname(harness.outputRoot), "mock-response.json"), "utf8", ), ), ).toEqual({ events }); } finally { await harness.close(); } }); it("serializes commands across both lanes on the shared user session", async () => { const harness = await setupHarness(); fs.writeFileSync(path.join(harness.sessionRoot, "harness.lock"), `${process.pid}\n`); try { await expect(runLane(harness.env, ["requests", "--lane", "candidate"])).rejects.toThrow( "shared Telegram harness already has a command in progress", ); expect(harness.requests).toEqual([]); } finally { await harness.close(); } }); it("rejects scenario flags that would otherwise be silently ignored", async () => { const harness = await setupHarness(); try { await expect( runLane(harness.env, [ "turn", "--lane", "candidate", "--text", "hello", "--observe-second", "2", ]), ).rejects.toThrow("turn does not accept --observe-second"); expect(harness.requests).toEqual([]); } finally { await harness.close(); } }); it("refuses to focus stale chat history outside the live proof timeline", async () => { const harness = await setupHarness(); try { await expect( runLane(harness.env, ["view", "--lane", "candidate", "--message-id", "999"]), ).rejects.toThrow("Message 999 was not emitted by the SUT bot in this proof session"); expect(harness.requests).toEqual([{ command: "events", seconds: 0, since: 0 }]); } finally { await harness.close(); } }); it("does not accept the user's outbound message as SUT evidence", async () => { const harness = await setupHarness({ userOnlyEvents: true }); try { await expect( runLane(harness.env, ["view", "--lane", "candidate", "--message-id", "101"]), ).rejects.toThrow("Message 101 was not emitted by the SUT bot in this proof session"); } finally { await harness.close(); } }); it("recovers a startup interrupted before any service launched", async () => { const harness = await setupHarness(); const active = path.join(harness.sessionRoot, "candidate.active.json"); const starting = path.join(harness.sessionRoot, "candidate.starting.json"); fs.rmSync(active); writeJson(starting, { attempt: 1, lane: "candidate", observerPidFile: path.join(harness.sessionRoot, "observer.pid.json"), observerRequested: false, observerSocket: path.join(harness.sessionRoot, "observer.sock"), privateDir: harness.sessionRoot, recorderRequested: false, recorderSession: path.join(harness.sessionRoot, "recorder.json"), repoRoot: "/prepared/candidate", startedAt: new Date().toISOString(), }); try { const result = await runLane(harness.env, ["abort", "--lane", "candidate"]); expect(JSON.parse(result.stdout)).toMatchObject({ status: "aborted-startup" }); expect(fs.existsSync(starting)).toBe(false); } finally { await harness.close(); } }); it("records a real turn send even when its observation fails", async () => { const harness = await setupHarness({ failEvents: true }); try { await expect( runLane(harness.env, [ "turn", "--lane", "candidate", "--text", "persist this send", "--observe-seconds", "1", ]), ).rejects.toThrow("observer failed after send"); const state = JSON.parse( fs.readFileSync(path.join(harness.sessionRoot, "candidate.active.json"), "utf8"), ); expect(state.sendCount).toBe(1); expect(state.invocations.at(-1)).toMatchObject({ command: "reveal" }); } finally { await harness.close(); } }); });