Files
oh-my-pi-telegram/tests/process-shutdown.test.ts
Evandro Fonseca Junior d1d04001d0
Release / release (push) Failing after 5s
Validate / validate (push) Failing after 5m21s
chore: fork @llblab/[email protected] as @evandrodevbr/oh-my-pi-telegram
Fork of upstream llblab/[email protected].0 (SHA afe09c5) repackaged as a
drop-in extension for can1357/oh-my-pi (omp >= 16.5.2).

Changes vs upstream:
- Rename package: @llblab/pi-telegram -> @evandrodevbr/oh-my-pi-telegram
- Bump version: 0.22.0 -> 0.22.0-evandro.1
- Update repository/homepage/bugs URLs to evandrodevbr
- Add omp field in package.json (displayName + compatibleWith)
- README: prepend 'Oh My Pi (omp) Compatibility' section with install
  commands, verified-components table, upstream lineage, and rationale
- CHANGELOG: add '0.22.0-evandro.1' entry under Unreleased
- docs/: rename @llblab/pi-telegram imports to @evandrodevbr/oh-my-pi-telegram
- tests/public-api.test.ts: rename package import paths

Gates re-run on the fork:
- npm run typecheck  -> tsc --noEmit (success)
- npm test           -> 1229 pass / 0 fail / 1 platform-only skip
- npm run audit      -> found 0 vulnerabilities
- npm run pack:check -> evandrodevbr-oh-my-pi-telegram-0.22.0-evandro.1.tgz

Verified:
- omp install --link ./pi-telegram -> Linked OK
- omp plugin list -> @evandrodevbr/[email protected].0-evandro.1

Upstream lineage: badlogic/pi-telegram -> llblab/pi-telegram (0.22.0) -> evandrodevbr/oh-my-pi-telegram (this fork)
2026-07-15 14:18:16 -03:00

737 lines
25 KiB
TypeScript

/**
* Process-level shutdown regressions for the Telegram extension
* Zones: process lifecycle, telegram polling, shutdown cleanup
* Verifies session shutdown releases or unrefs runtime handles so headless processes can exit
*/
import assert from "node:assert/strict";
import { spawn, spawnSync } from "node:child_process";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";
const REPO_ROOT = fileURLToPath(new URL("..", import.meta.url));
const PI_CLI_AVAILABLE = (() => {
const result = spawnSync("pi", ["--version"], { stdio: "ignore" });
return !result.error && result.status === 0;
})();
function runNodeScript(
script: string,
options: { env?: NodeJS.ProcessEnv; timeoutMs?: number } = {},
): Promise<{
code: number | null;
signal: NodeJS.Signals | null;
stdout: string;
stderr: string;
}> {
const child = spawn(
process.execPath,
["--experimental-strip-types", "--input-type=module", "-e", script],
{
cwd: REPO_ROOT,
env: { ...process.env, ...options.env },
stdio: ["ignore", "pipe", "pipe"],
},
);
let stdout = "";
let stderr = "";
child.stdout.setEncoding("utf8");
child.stderr.setEncoding("utf8");
child.stdout.on("data", (chunk) => {
stdout += chunk;
});
child.stderr.on("data", (chunk) => {
stderr += chunk;
});
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
child.kill("SIGKILL");
reject(
new Error(
`Child process did not exit. stdout=${stdout} stderr=${stderr}`,
),
);
}, options.timeoutMs ?? 5000);
timeout.unref?.();
child.on("error", (error) => {
clearTimeout(timeout);
reject(error);
});
child.on("exit", (code, signal) => {
clearTimeout(timeout);
resolve({ code, signal, stdout, stderr });
});
});
}
async function waitForFileText(
path: string,
predicate: (text: string) => boolean,
timeoutMs = 3000,
): Promise<string> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const text = await readFile(path, "utf8").catch(() => "");
if (predicate(text)) return text;
await new Promise((resolve) => setTimeout(resolve, 20));
}
const text = await readFile(path, "utf8").catch(() => "");
assert.fail(`Timed out waiting for ${path}. Current text: ${text}`);
}
function runPiPrint(
args: string[],
options: { env?: NodeJS.ProcessEnv; timeoutMs?: number } = {},
): Promise<{
code: number | null;
signal: NodeJS.Signals | null;
stdout: string;
stderr: string;
}> {
const child = spawn("pi", args, {
cwd: REPO_ROOT,
env: { ...process.env, ...options.env },
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
child.stdout.setEncoding("utf8");
child.stderr.setEncoding("utf8");
child.stdout.on("data", (chunk) => {
stdout += chunk;
});
child.stderr.on("data", (chunk) => {
stderr += chunk;
});
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
child.kill("SIGKILL");
reject(
new Error(`pi -p did not exit. stdout=${stdout} stderr=${stderr}`),
);
}, options.timeoutMs ?? 10_000);
timeout.unref?.();
child.on("error", (error) => {
clearTimeout(timeout);
reject(error);
});
child.on("exit", (code, signal) => {
clearTimeout(timeout);
resolve({ code, signal, stdout, stderr });
});
});
}
async function createPiPrintFixtureExtension(tempDir: string): Promise<string> {
const fixturePath = join(tempDir, "fixture-provider.ts");
await writeFile(
fixturePath,
`import { appendFileSync, writeFileSync } from "node:fs";\n` +
`import { join } from "node:path";\n` +
`import { createAssistantMessageEventStream } from "@earendil-works/pi-ai";\n\n` +
`export default function (pi) {\n` +
` const agentDir = process.env.PI_CODING_AGENT_DIR;\n` +
` if (process.env.PI_TELEGRAM_TEST_LOCK_MODE === "owner" && agentDir) {\n` +
` writeFileSync(join(agentDir, "locks.json"), JSON.stringify({ "@llblab/pi-telegram": { pid: process.pid } }) + "\\n");\n` +
` }\n` +
` pi.on("session_start", (_event, ctx) => {\n` +
` const forcedMode = process.env.PI_TELEGRAM_TEST_CTX_MODE;\n` +
` if (forcedMode && ctx.mode === undefined) ctx.mode = forcedMode;\n` +
` const methodMarker = process.env.PI_TELEGRAM_TEST_METHOD_MARKER;\n` +
` if (methodMarker) appendFileSync(methodMarker, "session-mode:" + String(ctx.mode) + "\\n");\n` +
` });\n` +
` globalThis.fetch = async (input, init = {}) => {\n` +
` const method = String(input).split("/").at(-1);\n` +
` const methodMarker = process.env.PI_TELEGRAM_TEST_METHOD_MARKER;\n` +
` if (methodMarker) appendFileSync(methodMarker, method + "\\n");\n` +
` if (method === "deleteWebhook") return { json: async () => ({ ok: true, result: true }) };\n` +
` if (method === "sendChatAction") return { json: async () => ({ ok: true, result: true }) };\n` +
` if (method === "sendMessage") {\n` +
` const marker = process.env.PI_TELEGRAM_TEST_SEND_MARKER;\n` +
` if (marker) writeFileSync(marker, String(init.body ?? "") + "\\n");\n` +
` return { json: async () => ({ ok: true, result: { message_id: 100 } }) };\n` +
` }\n` +
` if (method === "getUpdates") {\n` +
` const signal = init.signal;\n` +
` return new Promise((_, reject) => {\n` +
` const abort = () => reject(new DOMException("stop", "AbortError"));\n` +
` if (signal?.aborted) { abort(); return; }\n` +
` signal?.addEventListener("abort", abort, { once: true });\n` +
` });\n` +
` }\n` +
` throw new Error("Unexpected Telegram API method: " + method);\n` +
` };\n` +
` pi.registerProvider("fixture", {\n` +
` name: "Fixture Provider", baseUrl: "http://fixture.local", apiKey: "fixture", api: "fixture-stream",\n` +
` models: [{ id: "echo", name: "Echo", reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 8000, maxTokens: 1000 }],\n` +
` streamSimple(model) {\n` +
` const stream = createAssistantMessageEventStream();\n` +
` queueMicrotask(() => {\n` +
` const output = { role: "assistant", content: [], api: model.api, provider: model.provider, model: model.id, usage: { input: 1, output: 2, cacheRead: 0, cacheWrite: 0, totalTokens: 3, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } }, stopReason: "stop", timestamp: Date.now() };\n` +
` const text = "fixture ok";\n` +
` output.content.push({ type: "text", text: "" });\n` +
` stream.push({ type: "start", partial: output });\n` +
` stream.push({ type: "text_start", contentIndex: 0, partial: output });\n` +
` output.content[0].text = text;\n` +
` stream.push({ type: "text_delta", contentIndex: 0, delta: text, partial: output });\n` +
` stream.push({ type: "text_end", contentIndex: 0, content: text, partial: output });\n` +
` stream.push({ type: "done", reason: "stop", message: output });\n` +
` stream.end();\n` +
` });\n` +
` return stream;\n` +
` },\n` +
` });\n` +
`}\n`,
"utf8",
);
return fixturePath;
}
async function createPiPrintAgentDir(
config: Record<string, unknown>,
locks: Record<string, unknown> = {},
): Promise<string> {
const agentDir = await mkdtemp(join(tmpdir(), "pi-telegram-pi-print-"));
await writeFile(
join(agentDir, "telegram.json"),
JSON.stringify(config, null, "\t") + "\n",
"utf8",
);
await writeFile(
join(agentDir, "locks.json"),
JSON.stringify(locks, null, "\t") + "\n",
"utf8",
);
return agentDir;
}
async function runPiPrintWithTelegram(
agentDir: string,
fixtureExtension: string,
env: NodeJS.ProcessEnv = {},
) {
return runPiPrint(
[
"-p",
"--offline",
"--no-extensions",
"--no-skills",
"--no-prompt-templates",
"--no-themes",
"--no-context-files",
"--no-session",
"--extension",
fixtureExtension,
"--extension",
join(REPO_ROOT, "index.ts"),
"--provider",
"fixture",
"--model",
"echo",
"Say OK.",
],
{ env: { PI_CODING_AGENT_DIR: agentDir, ...env } },
);
}
test("Child process sharing the agent dir does not poll while parent owns Telegram lock", async () => {
const tempDir = await mkdtemp(join(tmpdir(), "pi-telegram-process-lock-"));
const markerPath = join(tempDir, "telegram-methods.log");
const stopPath = join(tempDir, "stop");
const agentDir = await createPiPrintAgentDir({
botToken: "123:abc",
allowedUserId: 77,
lastUpdateId: 0,
});
const extensionUrl = new URL("../index.ts", import.meta.url).href;
const parentScript = `
import { appendFileSync, existsSync, writeFileSync } from "node:fs";
import { join } from "node:path";
const agentDir = process.env.PI_CODING_AGENT_DIR;
const markerPath = process.env.PI_TELEGRAM_TEST_METHOD_MARKER;
const stopPath = process.env.PI_TELEGRAM_TEST_STOP_PATH;
const cwd = "/repo/parent-owner";
writeFileSync(
join(agentDir, "locks.json"),
JSON.stringify({ "@llblab/pi-telegram": { pid: process.pid, cwd } }) + "\\n",
);
globalThis.fetch = async (input, init = {}) => {
const method = String(input).split("/").at(-1);
appendFileSync(markerPath, "parent:" + method + "\\n");
if (method === "deleteWebhook") {
return { json: async () => ({ ok: true, result: true }) };
}
if (method === "getUpdates") {
const signal = init.signal;
return new Promise((_, reject) => {
const abort = () => reject(new DOMException("stop", "AbortError"));
if (signal?.aborted) { abort(); return; }
signal?.addEventListener("abort", abort, { once: true });
});
}
throw new Error("Unexpected Telegram API method: " + method);
};
const handlers = new Map();
const pi = {
on: (event, handler) => handlers.set(event, handler),
registerCommand: () => {},
registerTool: () => {},
sendUserMessage: () => {},
getCommands: () => [],
getThinkingLevel: () => "medium",
};
const extension = (await import(${JSON.stringify(extensionUrl)})).default;
extension(pi);
const ctx = {
cwd,
hasUI: false,
ui: {
theme: { fg: (_token, text) => text },
setStatus: () => {},
notify: () => {},
},
isIdle: () => true,
hasPendingMessages: () => false,
abort: () => {},
};
await handlers.get("session_start")?.({}, ctx);
const timer = setInterval(async () => {
if (!existsSync(stopPath)) return;
clearInterval(timer);
await handlers.get("session_shutdown")?.({}, ctx);
process.exit(0);
}, 20);
setTimeout(() => process.exit(2), 5000).unref?.();
`;
const parent = spawn(
process.execPath,
["--experimental-strip-types", "--input-type=module", "-e", parentScript],
{
cwd: REPO_ROOT,
env: {
...process.env,
PI_CODING_AGENT_DIR: agentDir,
PI_TELEGRAM_TEST_METHOD_MARKER: markerPath,
PI_TELEGRAM_TEST_STOP_PATH: stopPath,
},
stdio: ["ignore", "ignore", "pipe"],
},
);
let parentStderr = "";
parent.stderr.setEncoding("utf8");
parent.stderr.on("data", (chunk) => {
parentStderr += chunk;
});
const parentExit = new Promise<{ code: number | null }>((resolve) => {
parent.on("exit", (code) => resolve({ code }));
});
try {
await waitForFileText(markerPath, (text) =>
text.includes("parent:getUpdates"),
);
const childScript = `
import { appendFileSync } from "node:fs";
const markerPath = process.env.PI_TELEGRAM_TEST_METHOD_MARKER;
globalThis.fetch = async (input) => {
const method = String(input).split("/").at(-1);
appendFileSync(markerPath, "child:" + method + "\\n");
return { json: async () => ({ ok: true, result: true }) };
};
const handlers = new Map();
const pi = {
on: (event, handler) => handlers.set(event, handler),
registerCommand: () => {},
registerTool: () => {},
sendUserMessage: () => {},
getCommands: () => [],
getThinkingLevel: () => "medium",
};
const extension = (await import(${JSON.stringify(extensionUrl)})).default;
extension(pi);
const ctx = {
cwd: "/repo/parent-owner",
hasUI: false,
ui: {
theme: { fg: (_token, text) => text },
setStatus: () => {},
notify: () => {},
},
isIdle: () => true,
hasPendingMessages: () => false,
abort: () => {},
};
await handlers.get("session_start")?.({}, ctx);
await new Promise((resolve) => setImmediate(resolve));
`;
const child = await runNodeScript(childScript, {
env: {
PI_CODING_AGENT_DIR: agentDir,
PI_TELEGRAM_TEST_METHOD_MARKER: markerPath,
},
});
assert.equal(child.code, 0, child.stderr);
const methods = await readFile(markerPath, "utf8");
assert.match(methods, /parent:getUpdates/);
assert.doesNotMatch(methods, /child:getUpdates/);
} finally {
let parentError: unknown;
try {
await writeFile(stopPath, "stop", "utf8");
const timeout = new Promise<never>((_resolve, reject) => {
const timer = setTimeout(() => {
parent.kill("SIGKILL");
reject(new Error(`Parent did not exit. stderr=${parentStderr}`));
}, 2000);
timer.unref?.();
});
const { code } = await Promise.race([parentExit, timeout]);
if (code !== 0)
parentError = new Error(
`Parent exited with ${code}. stderr=${parentStderr}`,
);
} catch (error) {
parentError = error;
}
await rm(agentDir, { recursive: true, force: true });
await rm(tempDir, { recursive: true, force: true });
if (parentError) throw parentError;
}
});
test("Direct Telegram tools refuse delivery from a non-owner process", async () => {
const tempDir = await mkdtemp(join(tmpdir(), "pi-telegram-direct-owner-"));
const markerPath = join(tempDir, "telegram-methods.log");
const attachmentPath = join(tempDir, "demo.txt");
await writeFile(attachmentPath, "demo", "utf8");
const agentDir = await createPiPrintAgentDir(
{
botToken: "123:abc",
allowedUserId: 77,
lastUpdateId: 0,
},
{
"@llblab/pi-telegram": {
pid: process.pid,
cwd: "/repo/live-owner",
},
},
);
const extensionUrl = new URL("../index.ts", import.meta.url).href;
try {
const script = `
import { appendFileSync } from "node:fs";
const markerPath = process.env.PI_TELEGRAM_TEST_METHOD_MARKER;
const attachmentPath = process.env.PI_TELEGRAM_TEST_ATTACHMENT_PATH;
globalThis.fetch = async (input) => {
const method = String(input).split("/").at(-1);
appendFileSync(markerPath, method + "\\n");
return { json: async () => ({ ok: true, result: { message_id: 100 } }) };
};
const handlers = new Map();
const tools = new Map();
const pi = {
on: (event, handler) => handlers.set(event, handler),
registerCommand: () => {},
registerTool: (definition) => tools.set(definition.name, definition),
sendUserMessage: () => {},
getCommands: () => [],
getThinkingLevel: () => "medium",
};
const extension = (await import(${JSON.stringify(extensionUrl)})).default;
extension(pi);
const ctx = {
cwd: "/repo/non-owner",
hasUI: false,
ui: {
theme: { fg: (_token, text) => text },
setStatus: () => {},
notify: () => {},
},
isIdle: () => true,
hasPendingMessages: () => false,
abort: () => {},
};
await handlers.get("session_start")?.({}, ctx);
const failures = [];
for (const [name, params] of [
["telegram_message", { text: "hello" }],
["telegram_attach", { paths: [attachmentPath] }],
]) {
try {
await tools.get(name)?.execute("tool-call", params);
failures.push(name + ":unexpected-success");
} catch (error) {
failures.push(name + ":" + error.message);
}
}
await handlers.get("session_shutdown")?.({}, ctx);
console.log(failures.join("\\n"));
`;
const result = await runNodeScript(script, {
env: {
PI_CODING_AGENT_DIR: agentDir,
PI_TELEGRAM_TEST_METHOD_MARKER: markerPath,
PI_TELEGRAM_TEST_ATTACHMENT_PATH: attachmentPath,
},
});
assert.equal(result.code, 0, result.stderr);
assert.match(
result.stdout,
/telegram_message:Telegram direct delivery requires this Pi instance to own \/telegram-connect/,
);
assert.match(
result.stdout,
/telegram_attach:Telegram direct delivery requires this Pi instance to own \/telegram-connect/,
);
await assert.rejects(() => readFile(markerPath, "utf8"), {
code: "ENOENT",
});
} finally {
await rm(agentDir, { recursive: true, force: true });
await rm(tempDir, { recursive: true, force: true });
}
});
test(
"pi -p with Telegram extension and no active lock exits",
{ skip: !PI_CLI_AVAILABLE },
async () => {
const tempDir = await mkdtemp(join(tmpdir(), "pi-telegram-pi-print-"));
const fixtureExtension = await createPiPrintFixtureExtension(tempDir);
const agentDir = await createPiPrintAgentDir({
botToken: "123:abc",
allowedUserId: 77,
lastUpdateId: 0,
});
try {
const result = await runPiPrintWithTelegram(agentDir, fixtureExtension);
assert.equal(result.code, 0, result.stderr);
assert.equal(result.signal, null, result.stderr);
assert.match(result.stdout, /fixture ok/);
} finally {
await rm(agentDir, { recursive: true, force: true });
await rm(tempDir, { recursive: true, force: true });
}
},
);
test(
"pi -p with Telegram-owned lock stays passive and does not poll",
{ skip: !PI_CLI_AVAILABLE },
async () => {
const tempDir = await mkdtemp(join(tmpdir(), "pi-telegram-pi-print-"));
const fixtureExtension = await createPiPrintFixtureExtension(tempDir);
const markerPath = join(tempDir, "telegram-methods.log");
const agentDir = await createPiPrintAgentDir({
botToken: "123:abc",
allowedUserId: 77,
lastUpdateId: 0,
});
try {
const result = await runPiPrintWithTelegram(agentDir, fixtureExtension, {
PI_TELEGRAM_TEST_CTX_MODE: "print",
PI_TELEGRAM_TEST_LOCK_MODE: "owner",
PI_TELEGRAM_TEST_METHOD_MARKER: markerPath,
});
assert.equal(result.code, 0, result.stderr);
assert.equal(result.signal, null, result.stderr);
assert.match(result.stdout, /fixture ok/);
const methods = await readFile(markerPath, "utf8").catch(() => "");
assert.doesNotMatch(methods, /deleteWebhook/);
assert.doesNotMatch(methods, /getUpdates/);
} finally {
await rm(agentDir, { recursive: true, force: true });
await rm(tempDir, { recursive: true, force: true });
}
},
);
test(
"pi -p with proactive config and Telegram-owned lock exits",
{ skip: !PI_CLI_AVAILABLE },
async () => {
const tempDir = await mkdtemp(join(tmpdir(), "pi-telegram-pi-print-"));
const fixtureExtension = await createPiPrintFixtureExtension(tempDir);
const agentDir = await createPiPrintAgentDir({
botToken: "123:abc",
allowedUserId: 77,
lastUpdateId: 0,
proactivePush: true,
});
try {
const result = await runPiPrintWithTelegram(agentDir, fixtureExtension, {
PI_TELEGRAM_TEST_LOCK_MODE: "owner",
});
assert.equal(result.code, 0, result.stderr);
assert.equal(result.signal, null, result.stderr);
assert.match(result.stdout, /fixture ok/);
} finally {
await rm(agentDir, { recursive: true, force: true });
await rm(tempDir, { recursive: true, force: true });
}
},
);
test(
"pi -p non-owner skips proactive Telegram result",
{ skip: !PI_CLI_AVAILABLE },
async () => {
const tempDir = await mkdtemp(join(tmpdir(), "pi-telegram-pi-print-"));
const fixtureExtension = await createPiPrintFixtureExtension(tempDir);
const markerPath = join(tempDir, "send-marker.json");
const agentDir = await createPiPrintAgentDir(
{
botToken: "123:abc",
allowedUserId: 77,
lastUpdateId: 0,
proactivePush: true,
},
{
"@llblab/pi-telegram": {
pid: process.pid,
cwd: "/repo/another-live-owner",
},
},
);
try {
const result = await runPiPrintWithTelegram(agentDir, fixtureExtension, {
PI_TELEGRAM_TEST_SEND_MARKER: markerPath,
});
assert.equal(result.code, 0, result.stderr);
assert.equal(result.signal, null, result.stderr);
assert.match(result.stdout, /fixture ok/);
await assert.rejects(() => readFile(markerPath, "utf8"), {
code: "ENOENT",
});
} finally {
await rm(agentDir, { recursive: true, force: true });
await rm(tempDir, { recursive: true, force: true });
}
},
);
test("Extension session shutdown without active lock lets process exit", async () => {
const extensionUrl = new URL("../index.ts", import.meta.url).href;
const result = await runNodeScript(`
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
const agentDir = await mkdtemp(join(tmpdir(), "pi-telegram-process-shutdown-"));
process.env.PI_CODING_AGENT_DIR = agentDir;
await writeFile(
join(agentDir, "telegram.json"),
JSON.stringify({ botToken: "123:abc", allowedUserId: 77, lastUpdateId: 0 }) + "\\n",
"utf8",
);
await writeFile(join(agentDir, "locks.json"), "{}\\n", "utf8");
const handlers = new Map();
const pi = {
on: (event, handler) => handlers.set(event, handler),
registerCommand: () => {},
registerTool: () => {},
sendUserMessage: () => {},
getCommands: () => [],
getThinkingLevel: () => "medium",
};
const extension = (await import(${JSON.stringify(extensionUrl)})).default;
extension(pi);
const ctx = {
cwd: "/repo/process-shutdown-no-lock",
hasUI: false,
ui: {
theme: { fg: (_token, text) => text },
setStatus: () => {},
notify: () => {},
},
isIdle: () => true,
hasPendingMessages: () => false,
abort: () => {},
};
await handlers.get("session_start")?.({}, ctx);
await handlers.get("session_shutdown")?.({}, ctx);
await rm(agentDir, { recursive: true, force: true });
`);
assert.equal(result.code, 0, result.stderr);
assert.equal(result.signal, null, result.stderr);
});
test("Extension session shutdown lets an active polling owner process exit", async () => {
const extensionUrl = new URL("../index.ts", import.meta.url).href;
const result = await runNodeScript(`
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
const agentDir = await mkdtemp(join(tmpdir(), "pi-telegram-process-shutdown-"));
process.env.PI_CODING_AGENT_DIR = agentDir;
const cwd = "/repo/process-shutdown";
await writeFile(
join(agentDir, "telegram.json"),
JSON.stringify({ botToken: "123:abc", allowedUserId: 77, lastUpdateId: 0 }) + "\\n",
"utf8",
);
await writeFile(
join(agentDir, "locks.json"),
JSON.stringify({ "@llblab/pi-telegram": { pid: process.pid, cwd } }) + "\\n",
"utf8",
);
globalThis.fetch = async (input, init = {}) => {
const method = String(input).split("/").at(-1);
if (method === "deleteWebhook") {
return { json: async () => ({ ok: true, result: true }) };
}
if (method === "getUpdates") {
const signal = init.signal;
return new Promise((_, reject) => {
const abort = () => reject(new DOMException("stop", "AbortError"));
if (signal?.aborted) {
abort();
return;
}
signal?.addEventListener("abort", abort, { once: true });
});
}
throw new Error("Unexpected Telegram API method: " + method);
};
const handlers = new Map();
const pi = {
on: (event, handler) => handlers.set(event, handler),
registerCommand: () => {},
registerTool: () => {},
sendUserMessage: () => {},
getCommands: () => [],
getThinkingLevel: () => "medium",
};
const extension = (await import(${JSON.stringify(extensionUrl)})).default;
extension(pi);
const ctx = {
cwd,
hasUI: false,
ui: {
theme: { fg: (_token, text) => text },
setStatus: () => {},
notify: () => {},
},
isIdle: () => true,
hasPendingMessages: () => false,
abort: () => {},
};
await handlers.get("session_start")?.({}, ctx);
await new Promise((resolve) => setImmediate(resolve));
await handlers.get("session_shutdown")?.({}, ctx);
await rm(agentDir, { recursive: true, force: true });
`);
assert.equal(result.code, 0, result.stderr);
assert.equal(result.signal, null, result.stderr);
});