perf(e2e): reuse one authorized Telegram Desktop session per Mantis run (#127835)

* refactor(mantis): reuse authorized desktop captures

* fix(mantis): budget desktop authorization failures

* chore(mantis): bound desktop proof retries

* fix(e2e): drop unused recorder failure fact type export

* fix(ci): route Mantis desktop teardown through the recorder wrapper

Cleanup invoked the internal recorder executable as mantis-sut, which is
deliberately kept out of the docker group and cannot read the
recorder-owned session file; teardown therefore failed and blocked
safe_to_release. The cleanup step already runs as the recorder user, so
call the public wrapper whose exec shim cds into the session root.

* fix(e2e): make recorder failure fact lane-readable; document v2 lifecycle

The Mantis workflow runs the recorder as the desktop user while the lane
reads the authorization-failure fact as mantis-sut; 0600 made that read
fail EACCES and silently disabled the two-attempt retry budget. Write the
fact 0644 — the 0770 attempt directory bounds visibility.

Update the mantis doc's recorder section for the v2 session lifecycle:
required --session handle with healthy-session reuse, capture-only stop,
and teardown owning authorization termination and lease release.
This commit is contained in:
Ayaan Zaidi
2026-08-22 15:58:12 +05:30
committed by GitHub
parent 2d0fff4ac5
commit da0cb592dd
9 changed files with 797 additions and 253 deletions
@@ -37,6 +37,7 @@ type WorkflowJob = {
if?: string;
needs?: string | string[];
steps?: WorkflowStep[];
"timeout-minutes"?: number;
};
type Workflow = {
@@ -265,6 +266,7 @@ describe("Mantis Telegram Desktop proof workflow", () => {
expect(privateCleanupIndex).toBeGreaterThan(cleanupIndex);
expect(inspectIndex).toBeGreaterThan(cleanupIndex);
const abandoned = workflowStep("Clean up abandoned Mantis sessions");
expect(workflow.jobs?.run_telegram_desktop_proof?.["timeout-minutes"]).toBe(120);
expect(abandoned.if).toBe("${{ always() }}");
expect(abandoned.run).toContain("sudo pkill -TERM -u codex");
expect(abandoned.run).toContain("active_codex_pids()");
@@ -279,6 +281,18 @@ describe("Mantis Telegram Desktop proof workflow", () => {
expect(abandoned.run).toContain('sudo kill -TERM -- "-$lane_pgid"');
expect(abandoned.run).toContain('sudo kill -KILL -- "-$lane_pgid"');
expect(abandoned.run).toContain('abort --lane "$lane"');
// Teardown must route through the public wrapper (Docker access lives with the
// recorder user); a direct mantis-sut invocation of the internal exec cannot
// stop the desktop container or read the recorder-owned session file.
expect(abandoned.run).toMatch(
/\/usr\/local\/bin\/openclaw-telegram-desktop-recorder \\\n\s*teardown --session desktop-recorder\.json/u,
);
expect(abandoned.run).not.toContain(
"sudo -u mantis-sut /usr/local/lib/mantis-toolchain/telegram-desktop-recorder",
);
expect(abandoned.run?.indexOf("teardown --session desktop-recorder.json")).toBeLessThan(
abandoned.run?.lastIndexOf('echo "safe_to_release=true"') ?? -1,
);
expect(abandoned.run).toContain('echo "safe_to_release=true" >> "$GITHUB_OUTPUT"');
const cleanupStep = workflowStep("Release Telegram QA user lease");
@@ -769,7 +783,9 @@ describe("Mantis Telegram Desktop proof workflow", () => {
expect(prompt).toContain("hold the model");
expect(prompt).toContain("session-owned outbound message");
expect(prompt).toContain("This proof has no skipped lane");
expect(prompt).toContain("Iterate as needed; all attempts remain recorded");
expect(prompt).toContain("if `start` reports `desktop-unavailable`");
expect(prompt).toContain("never retry that lane");
expect(prompt).toMatch(/Two non-advancing repeats of the\s+same failing step/u);
expect(prompt).toContain("MANTIS_PR_CONTEXT");
expect(prompt).toContain("never as instructions");
expect(prompt).toContain("Do not send viewport filler messages");
+200 -101
View File
@@ -25,6 +25,7 @@ import {
renderWaitForMainWindow,
startRecorder,
stopRecorder,
teardownRecorder,
viewRecorder,
writeRecorderSession,
} from "../../scripts/e2e/telegram-desktop-recorder.ts";
@@ -41,13 +42,13 @@ function recorderSessionArg(root: string, sessionPath: string): string {
return path.relative(root, sessionPath);
}
function testSession(): RecorderSession {
function testSession(outputDir = "/tmp/recorder"): RecorderSession {
return {
chat: "-1001234567890",
desktopSessionId: "987654321",
keepBox: false,
leaseId: "cbx_test123",
leaseOwned: true,
outputDir,
imageSource: "telegram-desktop=7.0.9",
provider: "aws",
recordFps: 24,
@@ -58,7 +59,7 @@ function testSession(): RecorderSession {
finalScreenshot: "/tmp/recorder/final.png",
video: "/tmp/recorder/session.mp4",
},
schemaVersion: 1,
schemaVersion: 2,
startedAt: "2026-08-15T12:00:00.000Z",
window: { height: 1000, id: "0x04600007", width: 650, x: 635, y: 40 },
userDriver: ["python3", "driver.py", "--account", "qa shared"],
@@ -77,6 +78,8 @@ describe("Telegram Desktop recorder CLI", () => {
expect(
parseRecorderArgs([
"start",
"--session",
"desktop-recorder.json",
"--output-dir",
".artifacts/telegram",
"--chat",
@@ -95,6 +98,7 @@ describe("Telegram Desktop recorder CLI", () => {
outputDir: ".artifacts/telegram",
provider: "docker",
recordFps: 24,
sessionPath: "desktop-recorder.json",
ttl: "2h",
userDriver: ["uv", "run", "driver.py", "--json"],
});
@@ -108,18 +112,10 @@ describe("Telegram Desktop recorder CLI", () => {
parseRecorderArgs(["screenshot", "--session", "recorder.json", "--output", "shot.png"]),
).toEqual({ command: "screenshot", output: "shot.png", sessionPath: "recorder.json" });
expect(
parseRecorderArgs([
"stop",
"--session",
"recorder.json",
"--crop",
"telegram-window",
"--keep-box",
]),
parseRecorderArgs(["stop", "--session", "recorder.json", "--crop", "telegram-window"]),
).toEqual({
command: "stop",
crop: "telegram-window",
keepBox: true,
sessionPath: "recorder.json",
});
expect(parseRecorderArgs(["status", "--session", "recorder.json"])).toEqual({
@@ -130,6 +126,10 @@ describe("Telegram Desktop recorder CLI", () => {
command: "recover",
sessionPath: "recorder.json",
});
expect(parseRecorderArgs(["teardown", "--session", "recorder.json"])).toEqual({
command: "teardown",
sessionPath: "recorder.json",
});
expect(parseRecorderArgs(["artifacts", "--session", "recorder.json"])).toEqual({
command: "artifacts",
sessionPath: "recorder.json",
@@ -156,7 +156,7 @@ describe("Telegram Desktop recorder CLI", () => {
const root = makeTempDir();
const sessionPath = path.join(root, "recorder.json");
const actionsPath = path.join(root, "click.json");
writeRecorderSession(sessionPath, testSession());
writeRecorderSession(sessionPath, testSession(root));
fs.writeFileSync(
actionsPath,
JSON.stringify([
@@ -253,6 +253,20 @@ describe("Telegram Desktop recorder CLI", () => {
]),
).toThrow("--user-driver is required");
});
it("requires a run-scoped session handle", () => {
expect(() =>
parseRecorderArgs([
"start",
"--output-dir",
".artifacts/telegram",
"--chat",
"-1001234",
"--user-driver",
"driver",
]),
).toThrow("--session is required");
});
});
describe("Telegram Desktop recorder remote contract", () => {
@@ -333,6 +347,7 @@ describe("Telegram Desktop recorder remote contract", () => {
outputDir: "out",
provider: testCase.provider,
recordFps: 24,
sessionPath: "desktop-recorder.json",
ttl: "2h",
userDriver: ["python3", "driver.py"],
},
@@ -382,6 +397,7 @@ describe("Telegram Desktop recorder remote contract", () => {
outputDir: "out",
provider: "docker",
recordFps: 24,
sessionPath: "desktop-recorder.json",
ttl: "2h",
userDriver: ["python3", "driver.py"],
},
@@ -428,6 +444,7 @@ describe("Telegram Desktop recorder remote contract", () => {
outputDir: "out",
provider: "docker",
recordFps: 24,
sessionPath: "desktop-recorder.json",
ttl: "2h",
userDriver: ["python3", "driver.py"],
},
@@ -479,50 +496,44 @@ describe("Telegram Desktop recorder remote contract", () => {
outputDir: "out",
provider: "docker",
recordFps: 24,
sessionPath: "desktop-recorder.json",
ttl: "2h",
userDriver: ["python3", "driver.py"],
},
operations,
),
).rejects.toThrow(
"Telegram server accepted 2 login tokens, but Telegram Desktop stayed on the QR screen: permission denied reading the remote Docker socket",
"token-accepted-no-transition: Telegram server accepted 2 login tokens, but Telegram Desktop stayed on the QR screen: permission denied reading the remote Docker socket",
);
expect(
runCommand.mock.calls.filter(([call]) => call.args.includes("terminate-session")),
).toHaveLength(2);
expect(
JSON.parse(
fs.readFileSync(
path.join(root, "out", "telegram-desktop-authorization-failure.json"),
"utf8",
),
),
).toMatchObject({ failures: [{ classification: "token-accepted-no-transition" }] });
});
it("reprovisions one fresh local desktop after an accepted-token wedge", async () => {
it("reuses one authorized desktop across sequential captures", async () => {
const root = makeTempDir();
let container = 0;
let qrAttempt = 0;
const runCommand = vi.fn<RunCommand>(async (call) => {
if (call.command === "docker") {
return { stderr: "", stdout: "[]" };
}
if (call.args[0] === "warmup") {
container += 1;
return {
stderr: "",
stdout: `leased ${container === 1 ? "cbx_0a1b2c" : "cbx_0a1b2d"} slug=quiet-crab`,
};
return { stderr: "", stdout: "leased cbx_0a1b2c slug=quiet-crab" };
}
if (call.args.includes("confirm-qr")) {
return {
stderr: "",
stdout: JSON.stringify({
ok: true,
session: { id: `${container}${qrAttempt}`, isPasswordPending: false },
}),
stdout: JSON.stringify({ ok: true, session: { id: "91234", isPasswordPending: false } }),
};
}
if (
call.args.includes("terminate-session") ||
call.args.includes("terminate-desktop-sessions")
) {
return { stderr: "", stdout: JSON.stringify({ ok: true }) };
}
return { stderr: "", stdout: "" };
return { stderr: "", stdout: JSON.stringify({ ok: true }) };
});
const inspectCrabbox = vi.fn(async () => ({
sshHost: "host",
@@ -538,11 +549,7 @@ describe("Telegram Desktop recorder remote contract", () => {
scpFromRemote: vi.fn(async () => undefined),
sshRun: vi.fn(async ({ command }: { command: string }) => {
if (command.includes("telegram-login-qr.png")) {
qrAttempt += 1;
return { stderr: "", stdout: `tg://login?token=attempt-${qrAttempt}` };
}
if (command.includes("Telegram Desktop did not reach the main window") && container === 1) {
throw new Error("first desktop stayed on QR");
return { stderr: "", stdout: "tg://login?token=first-capture" };
}
if (command.includes("getwindowgeometry")) {
return { stderr: "", stdout: "0x04600007 635 40 650 1000" };
@@ -551,30 +558,35 @@ describe("Telegram Desktop recorder remote contract", () => {
}),
} satisfies RecorderOperations;
const result = await startRecorder(
root,
{
command: "start",
chat: "-1001234567890",
crabboxClass: "standard",
idleTimeout: "1h",
json: false,
outputDir: "out",
provider: "docker",
recordFps: 24,
ttl: "2h",
userDriver: ["python3", "driver.py"],
},
operations,
);
const options = {
command: "start" as const,
chat: "-1001234567890",
crabboxClass: "standard",
idleTimeout: "1h",
json: false,
outputDir: "attempt-1",
provider: "docker" as const,
recordFps: 24,
sessionPath: "desktop-recorder.json",
ttl: "2h",
userDriver: ["python3", "driver.py"],
};
const first = await startRecorder(root, options, operations);
await stopRecorder(root, { command: "stop", sessionPath: "desktop-recorder.json" }, operations);
const second = await startRecorder(root, { ...options, outputDir: "attempt-2" }, operations);
expect(inspectCrabbox).toHaveBeenCalledTimes(2);
expect(runCommand.mock.calls.filter(([call]) => call.args[0] === "warmup")).toHaveLength(2);
expect(runCommand.mock.calls).toContainEqual([
expect.objectContaining({ args: ["stop", "--provider", "docker", "cbx_0a1b2c"] }),
]);
expect(result.session).toMatchObject({ leaseId: "cbx_0a1b2d", leaseOwned: true });
expect(readRecorderSession(result.sessionPath)).toMatchObject({ leaseId: "cbx_0a1b2d" });
expect(runCommand.mock.calls.filter(([call]) => call.args[0] === "warmup")).toHaveLength(1);
expect(runCommand.mock.calls.filter(([call]) => call.args.includes("confirm-qr"))).toHaveLength(
1,
);
expect(
operations.sshRun.mock.calls.filter(([call]) => call.command.includes("x11grab")),
).toHaveLength(2);
expect(first.sessionPath).toBe(second.sessionPath);
expect(readRecorderSession(second.sessionPath)).toMatchObject({
leaseId: "cbx_0a1b2c",
outputDir: path.join(root, "attempt-2"),
});
});
it("hides the prepared chat before recording starts", async () => {
@@ -617,6 +629,7 @@ describe("Telegram Desktop recorder remote contract", () => {
outputDir: "out",
provider: "docker",
recordFps: 24,
sessionPath: "desktop-recorder.json",
ttl: "2h",
userDriver: ["python3", "driver.py"],
},
@@ -674,6 +687,7 @@ describe("Telegram Desktop recorder remote contract", () => {
outputDir: "out",
provider: "docker" as const,
recordFps: 24,
sessionPath: "desktop-recorder.json",
ttl: "2h",
userDriver: ["python3", "driver.py"],
};
@@ -683,10 +697,24 @@ describe("Telegram Desktop recorder remote contract", () => {
vi.useFakeTimers();
try {
const exhausted = expect(startRecorder(root, options, operations)).rejects.toThrow(
"telegram-login-screen.png",
"qr-unreadable: Telegram Desktop did not leave the login screen after 6 attempts",
);
await vi.runAllTimersAsync();
await exhausted;
expect(
JSON.parse(
fs.readFileSync(
path.join(root, "out", "telegram-desktop-authorization-failure.json"),
"utf8",
),
),
).toMatchObject({ failures: [{ classification: "qr-unreadable" }] });
// The lane reads this fact as a different OS user than the recorder; 0600
// would break the retry budget with EACCES.
expect(
fs.statSync(path.join(root, "out", "telegram-desktop-authorization-failure.json")).mode &
0o777,
).toBe(0o644);
expect(scpFromRemote).toHaveBeenCalledWith(
expect.objectContaining({ remote: expect.stringContaining("telegram-login-qr.png") }),
);
@@ -702,6 +730,77 @@ describe("Telegram Desktop recorder remote contract", () => {
}
});
it("classifies one accepted token without a main-window transition", async () => {
const root = makeTempDir();
let qrAttempt = 0;
const operations = {
createCroppedMotionPreview: vi.fn(async () => ({ crop: "", fps: 24, outputWidth: 430 })),
createMotionPreview: vi.fn(async () => ({})),
inspectCrabbox: vi.fn(async () => ({
sshHost: "host",
sshKey: "/tmp/key",
sshPort: "22",
sshUser: "user",
})),
runCommand: vi.fn<RunCommand>(async (call) => ({
stderr: "",
stdout: call.args.includes("confirm-qr")
? JSON.stringify({ ok: true, session: { id: 91234, isPasswordPending: false } })
: JSON.stringify({ ok: true }),
})),
scpFromRemote: vi.fn(async () => undefined),
sshRun: vi.fn(async ({ command }: { command: string }) => {
if (command.includes("telegram-login-qr.png")) {
qrAttempt += 1;
if (qrAttempt === 1) {
return { stderr: "", stdout: "tg://login?token=accepted-once" };
}
throw new Error("zbarimg: no barcode detected");
}
if (command.includes("Telegram Desktop did not reach the main window")) {
throw new Error("Telegram Desktop did not reach the main window");
}
return { stderr: "", stdout: "" };
}),
} satisfies RecorderOperations;
vi.useFakeTimers();
try {
const failed = expect(
startRecorder(
root,
{
command: "start",
chat: "-1001234567890",
crabboxClass: "standard",
idleTimeout: "1h",
json: false,
leaseId: "cbx_borrowed",
outputDir: "out",
provider: "docker",
recordFps: 24,
sessionPath: "desktop-recorder.json",
ttl: "2h",
userDriver: ["python3", "driver.py"],
},
operations,
),
).rejects.toThrow("main-window-timeout: Telegram Desktop did not reach the main window");
await vi.runAllTimersAsync();
await failed;
expect(
JSON.parse(
fs.readFileSync(
path.join(root, "out", "telegram-desktop-authorization-failure.json"),
"utf8",
),
),
).toMatchObject({ failures: [{ classification: "main-window-timeout" }] });
} finally {
vi.useRealTimers();
}
});
it("reports the blocked user when the output dir is not writable", async () => {
const root = makeTempDir();
// The agent and the recorder run as different users, so this fails in the lane and not
@@ -738,6 +837,7 @@ describe("Telegram Desktop recorder remote contract", () => {
outputDir: "out",
provider: "docker",
recordFps: 24,
sessionPath: "desktop-recorder.json",
ttl: "2h",
userDriver: ["python3", "driver.py"],
},
@@ -880,7 +980,7 @@ describe("Telegram Desktop recorder window geometry", () => {
const root = makeTempDir();
const sessionPath = path.join(root, "recorder.json");
writeRecorderSession(sessionPath, {
...testSession(),
...testSession(root),
window: { height: 995, id: "0x04600007", width: 648, x: 636, y: 45 },
});
const cropped = vi.fn(async () => ({ crop: "", fps: 24, outputWidth: 648 }));
@@ -907,7 +1007,6 @@ describe("Telegram Desktop recorder window geometry", () => {
{
command: "stop",
crop: "telegram-window",
keepBox: false,
sessionPath: recorderSessionArg(root, sessionPath),
},
operations,
@@ -927,9 +1026,10 @@ describe("Telegram Desktop recorder window geometry", () => {
});
describe("Telegram Desktop recorder session lifecycle", () => {
it("round-trips recorder.json schema version 1", () => {
const sessionPath = path.join(makeTempDir(), "recorder.json");
const session = testSession();
it("round-trips recorder.json schema version 2", () => {
const root = makeTempDir();
const sessionPath = path.join(root, "recorder.json");
const session = testSession(root);
writeRecorderSession(sessionPath, session);
@@ -943,7 +1043,7 @@ describe("Telegram Desktop recorder session lifecycle", () => {
const screenshot = path.join(root, "screenshot.png");
fs.writeFileSync(screenshot, "proof", { mode: 0o600 });
writeRecorderSession(sessionPath, {
...testSession(),
...testSession(root),
artifacts: { screenshot },
});
@@ -961,7 +1061,7 @@ describe("Telegram Desktop recorder session lifecycle", () => {
it("keeps every recorder path inside its fixed working directory", async () => {
const root = makeTempDir();
const sessionPath = path.join(root, "recorder.json");
writeRecorderSession(sessionPath, testSession());
writeRecorderSession(sessionPath, testSession(root));
expect(() =>
recorderArtifacts(root, { command: "artifacts", sessionPath: "../recorder.json" }),
).toThrow("--session must stay inside the recorder root");
@@ -991,11 +1091,13 @@ describe("Telegram Desktop recorder session lifecycle", () => {
).rejects.toThrow("--output must be relative");
});
it("writes the default screenshot beside a relative session path", async () => {
it("writes the default screenshot in the current capture directory", async () => {
const root = makeTempDir();
const sessionPath = path.join(root, "attempt", "recorder.json");
fs.mkdirSync(path.dirname(sessionPath));
writeRecorderSession(sessionPath, testSession());
const captureDir = path.join(root, "capture");
fs.mkdirSync(captureDir);
writeRecorderSession(sessionPath, testSession(captureDir));
const scpFromRemote = vi.fn(async () => undefined);
const output = await screenshotRecorder(
@@ -1016,7 +1118,7 @@ describe("Telegram Desktop recorder session lifecycle", () => {
},
);
expect(path.dirname(output)).toBe(path.dirname(sessionPath));
expect(path.dirname(output)).toBe(captureDir);
expect(scpFromRemote).toHaveBeenCalledWith(expect.objectContaining({ local: output }));
});
@@ -1057,7 +1159,7 @@ describe("Telegram Desktop recorder session lifecycle", () => {
expect(fs.existsSync(`${sessionPath}.starting`)).toBe(false);
});
it("never stops a borrowed --lease-id box, on failure or on stop", async () => {
it("never stops a borrowed --lease-id box, on failure or teardown", async () => {
const root = makeTempDir();
const calls: Array<{ args: string[]; command: string }> = [];
const mockedRun: RunCommand = async (params) => {
@@ -1088,6 +1190,7 @@ describe("Telegram Desktop recorder session lifecycle", () => {
outputDir: "out",
provider: "aws",
recordFps: 24,
sessionPath: "desktop-recorder.json",
ttl: "2h",
userDriver: ["python3", "driver.py"],
},
@@ -1099,7 +1202,7 @@ describe("Telegram Desktop recorder session lifecycle", () => {
const sessionPath = path.join(root, "recorder.json");
writeRecorderSession(sessionPath, {
...testSession(),
...testSession(root),
leaseId: "cbx_borrowed",
leaseOwned: false,
});
@@ -1112,19 +1215,19 @@ describe("Telegram Desktop recorder session lifecycle", () => {
sshUser: "user",
})),
} satisfies RecorderOperations;
await stopRecorder(
await teardownRecorder(
root,
{ command: "stop", keepBox: false, sessionPath: recorderSessionArg(root, sessionPath) },
operations,
{ command: "teardown", sessionPath: recorderSessionArg(root, sessionPath) },
{ runCommand: operations.runCommand },
);
expect(calls.some((call) => call.args.includes("terminate-session"))).toBe(true);
expect(calls.some((call) => call.args[0] === "stop")).toBe(false);
});
it("keeps the Desktop authorization and the box alive with --keep-box", async () => {
it("keeps the Desktop authorization and box alive when a capture stops", async () => {
const root = makeTempDir();
const sessionPath = path.join(root, "recorder.json");
writeRecorderSession(sessionPath, testSession());
writeRecorderSession(sessionPath, testSession(root));
const calls: Array<{ args: string[]; command: string }> = [];
const mockedRun: RunCommand = async (params) => {
calls.push({ args: params.args, command: params.command });
@@ -1144,25 +1247,20 @@ describe("Telegram Desktop recorder session lifecycle", () => {
sshRun: vi.fn(async () => ({ stderr: "", stdout: "" })),
} satisfies RecorderOperations;
const stopped = await stopRecorder(
await stopRecorder(
root,
{
command: "stop",
keepBox: true,
sessionPath: recorderSessionArg(root, sessionPath),
},
{ command: "stop", sessionPath: recorderSessionArg(root, sessionPath) },
operations,
);
expect(stopped.keepBox).toBe(true);
expect(calls.some((call) => call.args.includes("terminate-session"))).toBe(false);
expect(calls.some((call) => call.args[0] === "stop")).toBe(false);
});
it("uses the recorded provider for view, inspect, and owned-lease stop", async () => {
it("uses the recorded provider for view, capture stop, and teardown", async () => {
const root = makeTempDir();
const sessionPath = path.join(root, "recorder.json");
writeRecorderSession(sessionPath, {
...testSession(),
...testSession(root),
imageSource: "openclaw-telegram-desktop:7.0.9",
provider: "docker",
});
@@ -1198,9 +1296,14 @@ describe("Telegram Desktop recorder session lifecycle", () => {
);
await stopRecorder(
root,
{ command: "stop", keepBox: false, sessionPath: recorderSessionArg(root, sessionPath) },
{ command: "stop", sessionPath: recorderSessionArg(root, sessionPath) },
operations,
);
await teardownRecorder(
root,
{ command: "teardown", sessionPath: recorderSessionArg(root, sessionPath) },
{ runCommand: operations.runCommand },
);
expect(inspectCrabbox).toHaveBeenCalledTimes(2);
expect(sshCommands[0]).toContain('xdotool windowmap "$win"');
@@ -1222,7 +1325,7 @@ describe("Telegram Desktop recorder session lifecycle", () => {
it("finishes cleanly without previews when the lease is already gone", async () => {
const root = makeTempDir();
const sessionPath = path.join(root, "recorder.json");
writeRecorderSession(sessionPath, testSession());
writeRecorderSession(sessionPath, testSession(root));
const preview = vi.fn(async () => ({}));
const cropped = vi.fn(async () => ({ crop: "", fps: 24, outputWidth: 650 }));
const operations = {
@@ -1244,7 +1347,6 @@ describe("Telegram Desktop recorder session lifecycle", () => {
{
command: "stop",
crop: "telegram-window",
keepBox: false,
sessionPath: recorderSessionArg(root, sessionPath),
},
operations,
@@ -1254,13 +1356,12 @@ describe("Telegram Desktop recorder session lifecycle", () => {
expect(cropped).not.toHaveBeenCalled();
});
it("keeps artifacts recorded by an earlier keep-box stop", async () => {
it("keeps artifacts recorded by an earlier capture stop", async () => {
const root = makeTempDir();
const sessionPath = path.join(root, "recorder.json");
writeRecorderSession(sessionPath, {
...testSession(),
...testSession(root),
artifacts: { previewGif: "/kept/motion.gif", video: "/kept/session.mp4" },
keepBox: true,
});
const operations = {
createCroppedMotionPreview: vi.fn(async () => ({ crop: "", fps: 24, outputWidth: 650 })),
@@ -1280,7 +1381,6 @@ describe("Telegram Desktop recorder session lifecycle", () => {
root,
{
command: "stop",
keepBox: false,
sessionPath: recorderSessionArg(root, sessionPath),
},
operations,
@@ -1291,10 +1391,10 @@ describe("Telegram Desktop recorder session lifecycle", () => {
});
});
it("still stops Crabbox and reports failure when local session termination fails", async () => {
it("still stops Crabbox and reports teardown failure when session termination fails", async () => {
const root = makeTempDir();
const sessionPath = path.join(root, "recorder.json");
writeRecorderSession(sessionPath, testSession());
writeRecorderSession(sessionPath, testSession(root));
const calls: Array<{ args: string[]; command: string }> = [];
const mockedRun: RunCommand = async (params) => {
calls.push({ args: params.args, command: params.command });
@@ -1318,10 +1418,10 @@ describe("Telegram Desktop recorder session lifecycle", () => {
} satisfies RecorderOperations;
await expect(
stopRecorder(
teardownRecorder(
root,
{ command: "stop", keepBox: false, sessionPath: recorderSessionArg(root, sessionPath) },
operations,
{ command: "teardown", sessionPath: recorderSessionArg(root, sessionPath) },
{ runCommand: operations.runCommand },
),
).rejects.toThrow("terminate Telegram Desktop session: terminate failed");
@@ -1342,7 +1442,6 @@ describe("Telegram Desktop recorder session lifecycle", () => {
command: "crabbox",
});
const stopped = readRecorderSession(sessionPath);
expect(stopped.stoppedAt).toBeDefined();
expect(stopped.cleanupErrors).toContain("terminate Telegram Desktop session: terminate failed");
});
});
+120 -5
View File
@@ -8,6 +8,7 @@ import { afterEach, describe, expect, it } from "vitest";
import {
publishableRecorderArtifacts,
publishStartupFailure,
startDesktopRecorder,
} from "../../scripts/e2e/telegram-mantis-lane.ts";
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
@@ -77,7 +78,7 @@ async function setupHarness(
observerPidFile: path.join(root, "observer.pid.json"),
observerSocket,
privateDir: path.join(sessionRoot, "attempt"),
recorderSession: path.join(sessionRoot, "attempt", "recorder.json"),
recorderSession: path.join(sessionRoot, "desktop-recorder.json"),
repoRoot: "/prepared/candidate",
sendCount: 0,
startedAt: new Date().toISOString(),
@@ -177,6 +178,120 @@ async function runLane(env: NodeJS.ProcessEnv, args: string[]) {
}
describe("Telegram Mantis free-form lane", () => {
it("passes one run-scoped recorder session across sequential lane starts", async () => {
const root = tempDirs.make("telegram-mantis-recorder-reuse-");
const sessionRoot = path.join(root, "private");
const recorderCommand = path.join(root, "recorder");
const recorderLog = path.join(root, "recorder.log");
const provisionLog = path.join(root, "provision.log");
fs.mkdirSync(sessionRoot);
fs.writeFileSync(
recorderCommand,
`#!/bin/sh
session=
while [ "$#" -gt 0 ]; do
if [ "$1" = --session ]; then session=$2; break; fi
shift
done
printf '%s\\n' "$session" >> ${JSON.stringify(recorderLog)}
if [ ! -f ${JSON.stringify(sessionRoot)}/"$session" ]; then
printf 'provision\\n' >> ${JSON.stringify(provisionLog)}
: > ${JSON.stringify(sessionRoot)}/"$session"
fi
`,
{ mode: 0o755 },
);
const priorSessionRoot = process.env.OPENCLAW_MANTIS_SESSION_ROOT;
process.env.OPENCLAW_MANTIS_SESSION_ROOT = sessionRoot;
try {
for (const [lane, attempt] of [
["baseline", "1"],
["candidate", "1"],
] as const) {
await startDesktopRecorder({
chat: "-100123456789",
outputDir: path.join(sessionRoot, "attempts", lane, attempt),
recorderCommand,
sessionPath: path.join(sessionRoot, "desktop-recorder.json"),
sessionRoot,
userDriver: "/usr/local/bin/telegram-user-driver",
});
}
} finally {
if (priorSessionRoot === undefined) {
delete process.env.OPENCLAW_MANTIS_SESSION_ROOT;
} else {
process.env.OPENCLAW_MANTIS_SESSION_ROOT = priorSessionRoot;
}
}
expect(fs.readFileSync(recorderLog, "utf8").trim().split("\n")).toEqual([
"desktop-recorder.json",
"desktop-recorder.json",
]);
expect(fs.readFileSync(provisionLog, "utf8").trim().split("\n")).toEqual(["provision"]);
});
it("stops invoking the recorder after two authorization failures", async () => {
const root = tempDirs.make("telegram-mantis-recorder-budget-");
const sessionRoot = path.join(root, "private");
const recorderCommand = path.join(root, "recorder");
const recorderLog = path.join(root, "recorder.log");
fs.mkdirSync(sessionRoot);
fs.writeFileSync(
recorderCommand,
`#!/bin/sh
printf '%s\\n' "$*" >> ${JSON.stringify(recorderLog)}
output=
while [ "$#" -gt 0 ]; do
if [ "$1" = --output-dir ]; then output=$2; break; fi
shift
done
count=$(wc -l < ${JSON.stringify(recorderLog)})
classification=qr-unreadable
accepted=0
if [ "$count" -eq 2 ]; then classification=token-accepted-no-transition; accepted=2; fi
mkdir -p ${JSON.stringify(sessionRoot)}/"$output"
printf '{"failures":[{"acceptedTokenCount":%s,"classification":"%s","failedAt":"2026-08-22T00:00:00.000Z","loginScreenshotPath":"%s/login.png","qrAttemptCount":6}],"schemaVersion":1}\n' "$accepted" "$classification" ${JSON.stringify(sessionRoot)}/"$output" > ${JSON.stringify(sessionRoot)}/"$output"/telegram-desktop-authorization-failure.json
exit 1
`,
{ mode: 0o755 },
);
const priorSessionRoot = process.env.OPENCLAW_MANTIS_SESSION_ROOT;
process.env.OPENCLAW_MANTIS_SESSION_ROOT = sessionRoot;
const start = (attempt: number) =>
startDesktopRecorder({
chat: "-100123456789",
outputDir: path.join(sessionRoot, "attempts", "candidate", String(attempt)),
recorderCommand,
sessionPath: path.join(sessionRoot, "desktop-recorder.json"),
sessionRoot,
userDriver: "/usr/local/bin/telegram-user-driver",
});
try {
await expect(start(1)).rejects.toThrow();
await expect(start(2)).rejects.toThrow(
"desktop-unavailable: stop retrying; this run's desktop is unavailable",
);
await expect(start(3)).rejects.toThrow(
/attemptCount=2, classification=token-accepted-no-transition.*loginScreenshotPath=.*login\.png/u,
);
} finally {
if (priorSessionRoot === undefined) {
delete process.env.OPENCLAW_MANTIS_SESSION_ROOT;
} else {
process.env.OPENCLAW_MANTIS_SESSION_ROOT = priorSessionRoot;
}
}
expect(fs.readFileSync(recorderLog, "utf8").trim().split("\n")).toHaveLength(2);
expect(
JSON.parse(fs.readFileSync(path.join(sessionRoot, "desktop-recorder-failures.json"), "utf8")),
).toMatchObject({
attemptCount: 2,
classification: "token-accepted-no-transition",
unavailable: true,
});
});
it("publishes only cropped visual evidence", () => {
expect(
publishableRecorderArtifacts({
@@ -223,7 +338,7 @@ describe("Telegram Mantis free-form lane", () => {
observerSocket: path.join(sessionRoot, "observer.sock"),
privateDir: path.join(sessionRoot, "attempts", "candidate", "1"),
recorderRequested: true,
recorderSession: path.join(sessionRoot, "attempts", "candidate", "1", "recorder.json"),
recorderSession: path.join(sessionRoot, "desktop-recorder.json"),
repoRoot: "/prepared/candidate",
startedAt,
},
@@ -321,7 +436,7 @@ describe("Telegram Mantis free-form lane", () => {
"observe",
]);
expect(fs.readFileSync(harness.recorderLog, "utf8")).toContain(
"view --session attempt/recorder.json --message-id 101",
"view --session desktop-recorder.json --message-id 101",
);
expect(JSON.parse(fs.readFileSync(harness.recorderControlLog, "utf8"))).toMatchObject({
hold: true,
@@ -404,7 +519,7 @@ describe("Telegram Mantis free-form lane", () => {
results: [{ command: "click", stdout: "clicked\n" }],
});
expect(fs.readFileSync(harness.recorderLog, "utf8")).toContain(
"actions --session attempt/recorder.json --actions-file attempt/desktop-actions-2.json --timeout-seconds 90",
"actions --session desktop-recorder.json --actions-file attempt/desktop-actions-2.json --timeout-seconds 90",
);
const state = JSON.parse(
fs.readFileSync(path.join(harness.sessionRoot, "candidate.active.json"), "utf8"),
@@ -869,7 +984,7 @@ describe("Telegram Mantis free-form lane", () => {
observerSocket: path.join(harness.sessionRoot, "observer.sock"),
privateDir: harness.sessionRoot,
recorderRequested: false,
recorderSession: path.join(harness.sessionRoot, "recorder.json"),
recorderSession: path.join(harness.sessionRoot, "desktop-recorder.json"),
repoRoot: "/prepared/candidate",
startedAt: new Date().toISOString(),
});