Official
codex
Codex agent provider: runs the Codex CLI's app-server for each account.
The app opens the listing; nothing installs until an agent in your Plugins workspace has read the files and you enable the plugin. In a terminal: cvg install convergence/codex@0.2.0
Permissions in 0.2.0
Files
map.test.ts25.3 KB
// Ported from the Rust plugin's map.rs and agent.rs unit tests.
import { test } from "node:test";
import assert from "node:assert/strict";
import type { History } from "./types.ts";
import type { Turn } from "./wire.ts";
import * as map from "./map.ts";
/// The parent's turns as `thread/read` returns them: a v1 spawn, the
/// `wait` that carries the answer, and a v2 `started` activity.
function parentTurns(): Turn[] {
return [
{
id: "turn-1",
status: "completed",
startedAt: 100,
completedAt: 200,
items: [
{ type: "userMessage", id: "u1", content: [{ type: "text", text: "Count the tests" }] },
{
type: "collabAgentToolCall",
id: "call-spawn",
tool: "spawnAgent",
status: "completed",
senderThreadId: "root",
receiverThreadIds: ["child-1"],
prompt: "Count the tests\nthen report",
model: "gpt-5.5",
reasoningEffort: "low",
agentsStates: { "child-1": { status: "pendingInit", message: null } },
},
{
type: "collabAgentToolCall",
id: "call-wait",
tool: "wait",
status: "completed",
senderThreadId: "root",
receiverThreadIds: ["child-1"],
prompt: null,
model: null,
reasoningEffort: null,
agentsStates: { "child-1": { status: "completed", message: "Final: 42 tests." } },
},
{ type: "subAgentActivity", id: "act-1", kind: "started", agentThreadId: "child-2", agentPath: "/root/scout" },
{
type: "subAgentActivity",
id: "act-2",
kind: "completed",
agentThreadId: "child-2",
agentPath: "/root/scout",
},
{ type: "agentMessage", id: "m1", text: "There are 42 tests." },
],
},
];
}
function children(): Map<string, History> {
return new Map([
[
"child-1",
{
thread: {
id: "child-1",
preview: "Count the tests\nthen report",
parentThreadId: "root",
agentNickname: "Euler",
agentRole: "explorer",
historyMode: "paginated",
source: { subAgent: { thread_spawn: { parent_thread_id: "root", depth: 1 } } },
},
turns: [
{
id: "turn-c1",
status: "completed",
startedAt: 110,
completedAt: 190,
items: [
{ type: "userMessage", id: "cu1", content: [{ type: "text", text: "Count the tests" }] },
{
type: "commandExecution",
id: "cmd-1",
command: "rg -c test",
status: "completed",
commandActions: [{ type: "search", command: "rg -c test", query: "test", path: null }],
},
{
type: "collabAgentToolCall",
id: "call-grand",
tool: "spawnAgent",
status: "completed",
senderThreadId: "child-1",
receiverThreadIds: ["grandchild-1"],
prompt: "Read the parser",
agentsStates: {},
},
{ type: "agentMessage", id: "cm1", text: "42 tests." },
],
},
],
},
],
[
"grandchild-1",
{
thread: { id: "grandchild-1", preview: "Read the parser", parentThreadId: "child-1" },
turns: [
{
id: "turn-g1",
status: "completed",
items: [{ type: "agentMessage", id: "gm1", text: "The parser is fine." }],
},
],
},
],
]);
}
test("background questions preserve choices and free text", () => {
const native = {
type: "agentMessage",
id: "q1",
text: "Checking",
delivery: "async",
questions: [
{ title: "Which target?", options: ["Desktop", "Mobile"] },
{ title: "Anything else?", options: null },
],
};
const request = present(map.backgroundQuestion(native));
assert.equal(request.responseMode, "message");
assert.equal(request.id, "async:q1");
assert.equal(present(request.fields[0].options)[1].value, "Mobile");
assert.equal(request.fields[0].kind, "select");
assert.ok(request.fields.every((field) => field.allowOther && field.required));
assert.equal(request.fields[1].kind, "text");
assert.equal(request.fields[1].options, undefined);
assert.equal(map.backgroundQuestion({ ...native, delivery: null }), null);
assert.equal(map.backgroundQuestion({ ...native, questions: [] }), null);
assert.equal(map.backgroundQuestion({ type: "agentMessage", id: "old", text: "hi" }), null);
});
test("history rebuilds each subagent at its spawn", () => {
const items = map.transcriptWithSubagents(parentTurns(), children(), null);
// user, v1 task, v2 task, assistant: `wait` and the other activity are
// bookkeeping the task rows already show.
assert.equal(items.length, 4);
assert.equal(items[0].role, "user");
assert.equal(items[3].role, "assistant");
const one = items[1];
assert.ok(one.role === "task");
assert.equal(one.id, "task-child-1", "the id a live task gets");
assert.equal(one.role, "task");
const task = one.task;
assert.equal(task.id, "child-1");
assert.equal(task.title, "Count the tests", "the prompt's first line, never a tool name");
assert.equal(task.prompt, "Count the tests\nthen report");
assert.equal(task.name, "Euler");
assert.equal(task.toolCallId, "call-spawn");
assert.deepEqual([task.model, task.effort], ["gpt-5.5", "low"]);
assert.equal(task.status, "completed");
assert.equal(task.summary, "Final: 42 tests.", "the answer `wait` returned");
assert.equal(task.parentTaskId, undefined);
assert.equal(task.toolUses, 1);
assert.equal(Date.parse(present(task.startedAt)) / 1000, 110);
assert.equal(Date.parse(present(task.endedAt)) / 1000, 190);
assert.equal(one.createdAt, task.startedAt);
// The child's own transcript, with the grandchild inside it.
const nested = present(one.items);
assert.ok(nested[2].role === "task");
assert.equal(nested.length, 4);
assert.equal(nested[0].role, "user");
assert.equal(nested[1].role, "tool");
const grand = nested[2].task;
assert.equal(grand.id, "grandchild-1");
assert.equal(grand.parentTaskId, "child-1");
assert.equal(grand.title, "Read the parser");
assert.equal(grand.status, "idle", "a finished turn without a final state can be resumed");
assert.equal(grand.summary, "The parser is fine.", "its last message is its result");
assert.equal(present(nested[2].items).length, 1);
// A v2 child that could not be read still gets a row, named by its
// path, with the status the activity reported.
assert.ok(items[2].role === "task");
const v2 = items[2].task;
assert.equal(v2.id, "child-2");
assert.equal(v2.name, undefined);
assert.equal(v2.title, "scout");
assert.equal(v2.status, "completed");
assert.equal(v2.toolCallId, undefined);
assert.equal(v2.toolUses, undefined, "an unread child has no count");
assert.deepEqual(items[2].items, []);
});
test("a v2 child takes its title from its preview", () => {
const all = children();
all.set("child-2", {
thread: { id: "child-2", preview: "Scout the repo\nfor TODOs", parentThreadId: "root", agentNickname: "Scout" },
turns: [
{
id: "turn-s1",
status: "completed",
items: [
{ type: "subAgentActivity", id: "act-up", kind: "interacted", agentThreadId: "root", agentPath: "/root" },
{ type: "agentMessage", id: "sm1", text: "3 TODOs." },
],
},
],
});
const items = map.transcriptWithSubagents(parentTurns(), all, null);
const v2 = items[2];
assert.ok(v2.role === "task");
assert.equal(v2.task.title, "Scout the repo", "a readable instruction wins over the task name");
assert.equal(v2.task.name, "Scout");
assert.equal(v2.task.summary, "3 TODOs.");
assert.equal(present(v2.items).length, 1, "a message to the parent is not a child");
});
test("a thread that spawns its own ancestor does not loop", () => {
const turns = [
{
id: "t",
status: "completed",
items: [{ type: "collabAgentToolCall", id: "c", tool: "spawnAgent", receiverThreadIds: ["a"], agentsStates: {} }],
},
];
const loop = new Map([["a", { thread: { id: "a" }, turns }]]);
const items = map.transcriptWithSubagents(turns, loop, null);
assert.equal(items.length, 1);
assert.ok(items[0].role === "task");
assert.deepEqual(items[0].items, [], "the child's own spawn of itself is skipped");
});
test("a subagent title is its instruction, then its task name", () => {
assert.equal(map.taskTitle("Read the parser\nthen report", "reader", "Euler"), "Read the parser");
assert.equal(map.taskTitle(null, "pipeline_eval_tools", "Planck"), "pipeline eval tools");
assert.equal(map.taskTitle(" ", null, "Planck"), "Planck");
assert.equal(map.taskTitle(null, null, null), "Subagent");
assert.equal(map.pathTaskName("/root/audit/source_check"), "source_check");
assert.equal(map.pathTaskName("/root"), null);
});
test("history lists every child once, in order", () => {
assert.deepEqual(map.childIds(parentTurns()), ["child-1", "child-2"]);
});
test("read commands classify as read", () => {
assert.equal(map.commandToolKind([{ type: "read", command: "cat a", name: "a", path: "a.rs" }]), "read");
});
test("list and search classify as search", () => {
assert.equal(
map.commandToolKind([
{ type: "listFiles", command: "ls", path: "src" },
{ type: "search", command: "rg x", query: "x", path: "src" },
]),
"search",
);
});
test("unknown or mixed commands execute", () => {
assert.equal(map.commandToolKind([{ type: "unknown", command: "echo hi" }]), "execute");
assert.equal(
map.commandToolKind([
{ type: "read", command: "cat a", name: "a", path: "a.rs" },
{ type: "unknown", command: "rm -rf b" },
]),
"execute",
);
assert.equal(map.commandToolKind([]), "execute");
assert.equal(map.commandToolKind(undefined), "execute");
});
test("a command item carries terminal content", () => {
const call = map.toolCallFromItem({
type: "commandExecution",
id: "exec-1",
command: "/bin/zsh -lc 'echo hi'",
cwd: "/tmp/work",
processId: null,
source: "unifiedExecStartup",
status: "completed",
commandActions: [{ type: "unknown", command: "echo hi" }],
aggregatedOutput: "hi\n",
exitCode: 0,
durationMs: 0,
});
assert.ok(call);
assert.equal(call.kind, "execute");
assert.equal(call.status, "completed");
assert.equal(call.title, "echo hi", "the shell wrapper is not part of the title");
assert.equal(call.name, "shell");
assert.deepEqual(call.input, { command: "/bin/zsh -lc 'echo hi'", cwd: "/tmp/work" });
assert.deepEqual(call.content, [
{ type: "terminal", command: "/bin/zsh -lc 'echo hi'", cwd: "/tmp/work", output: "hi\n", exitCode: 0 },
]);
assert.equal(call.locations, undefined);
});
test("the shell wrapper comes off only a real shell invocation", () => {
assert.equal(map.unwrapped('/bin/bash -c "ls -la"'), "ls -la");
assert.equal(map.unwrapped("sh -lc git status"), "git status");
assert.equal(map.unwrapped("python -c 'print(1)'"), "python -c 'print(1)'");
assert.equal(map.unwrapped("ls"), "ls");
});
// Codex sends an update as a unified diff, but a new or deleted file
// whole (`format_file_change_diff` in its app-server protocol): read as a
// patch, three new files counted +0 -0.
test("a file change item carries an update's patch and a whole file's text", () => {
const call = map.toolCallFromItem({
type: "fileChange",
id: "fc-1",
status: "completed",
changes: [
{ path: "src/a.rs", kind: { type: "update", move_path: null }, diff: "@@ -1 +1 @@\n-a\n+b\n" },
{ path: "src/b.rs", kind: { type: "add" }, diff: "new\n" },
{ path: "src/c.rs", kind: { type: "delete" }, diff: "old\n" },
],
});
assert.ok(call);
assert.equal(call.kind, "edit");
assert.equal(call.title, "src/a.rs and 2 more");
assert.equal(present(call.locations).length, 3);
assert.deepEqual(call.content, [
{ type: "diff", path: "src/a.rs", diff: "@@ -1 +1 @@\n-a\n+b\n" },
{ type: "diff", path: "src/b.rs", oldText: "", newText: "new\n" },
{ type: "diff", path: "src/c.rs", oldText: "old\n", newText: "" },
]);
assert.equal(call.input, undefined);
});
test("unknown item types are ignored, not fatal", () => {
const unknown = { type: "somethingCodexAddedLater", id: "x1" };
assert.equal(map.itemType(unknown), null);
assert.equal(map.itemId(unknown), null);
assert.equal(map.toolCallFromItem(unknown), null);
assert.equal(map.transcriptItem(unknown), null);
});
test("rate limits become named windows", () => {
const limits = map.usageLimits({
rateLimits: {
limitName: "Codex",
primary: { usedPercent: 82.5, windowDurationMins: 300, resetsAt: 1_800_000_000 },
secondary: { usedPercent: 100.0, windowDurationMins: 10080, resetsAt: null },
rateLimitReachedType: "rate_limit_reached",
},
rateLimitResetCredits: { availableCount: 2 },
});
assert.equal(limits.resetCredits, 2);
assert.equal(present(limits.windows).length, 2);
assert.equal(present(limits.windows)[0].label, "5 hours");
assert.equal(present(limits.windows)[0].usedPercent, 82.5);
assert.equal(present(limits.windows)[0].resetsAt, new Date(1_800_000_000 * 1000).toISOString());
assert.equal(present(limits.windows)[0].windowMinutes, 300);
// Only the window that is really full is blocked.
assert.equal(present(limits.windows)[0].blocked, false);
assert.equal(present(limits.windows)[1].label, "Weekly");
assert.equal(present(limits.windows)[1].blocked, true);
assert.equal(present(limits.windows)[1].resetsAt, undefined);
});
test("a snapshot without credits offers none, and windows without a length use the account's name", () => {
const limits = map.usageLimits({
rateLimits: { primary: { usedPercent: 10.0, windowDurationMins: 60 } },
rateLimitResetCredits: { availableCount: 0 },
});
assert.equal(limits.resetCredits, undefined, "zero credits is not an offer");
assert.equal(present(limits.windows)[0].label, "Hourly");
const named = map.usageLimits({
rateLimits: { limitName: "Pro", primary: { usedPercent: 1 }, secondary: { usedPercent: 1 } },
});
assert.deepEqual(
present(named.windows).map((window) => window.label),
["Pro", "Pro"],
);
const bare = map.usageLimits({
rateLimits: { primary: { usedPercent: 1 }, secondary: { usedPercent: 1, windowDurationMins: 1440 } },
});
assert.deepEqual(
present(bare.windows).map((window) => window.label),
["Current limit", "Daily"],
);
});
test("usage recovery never derives permission or identity from meters", () => {
for (const usedPercent of [0, 99, 100]) {
const recovery = map.usageRecovery({ rateLimits: { primary: { usedPercent, resetsAt: 1_900_000_000 } } });
assert.equal(recovery.availability, "unknown");
assert.equal(recovery.identity, undefined);
assert.equal(recovery.resetsAt, undefined);
}
for (const ordinaryUsageAllowed of [true, false, null]) {
const recovery = map.usageRecovery({ ordinaryUsageAllowed, accountId: "native-account" });
assert.equal(
recovery.availability,
ordinaryUsageAllowed === true ? "allowed" : ordinaryUsageAllowed === false ? "blocked" : "unknown",
);
assert.equal(recovery.identity, "native-account");
}
const sparse = map.usageRecovery(
{ ordinaryUsageAllowed: true, accountId: "not-in-notification" },
"codex/account/rateLimits/updated",
);
assert.equal(sparse.availability, "unknown");
assert.equal(sparse.identity, undefined);
});
test("quota recovery waits for every responsible window and never times a credit purchase", () => {
const primary = { usedPercent: 100, resetsAt: 1_900_000_000 };
const secondary = { usedPercent: 100, resetsAt: 1_900_100_000 };
const snapshot = { limitId: "codex", rateLimitReachedType: "rate_limit_reached", primary, secondary };
const latest = map.usageRecovery({ ordinaryUsageAllowed: false, rateLimits: snapshot });
assert.equal(latest.resetsAt, new Date(secondary.resetsAt * 1000).toISOString());
const recovered = map.usageRecovery({
ordinaryUsageAllowed: true,
rateLimits: { ...snapshot, rateLimitReachedType: null },
});
assert.equal(latest.scope, undefined, "native permission/rejection is account-wide");
assert.equal(recovered.scope, latest.scope, "a bucket recovering must not change recovery identity");
for (const missing of [{ usedPercent: 100 }, { usedPercent: 100, resetsAt: null }]) {
assert.equal(map.usageRecovery({ rateLimits: { ...snapshot, secondary: missing } }).resetsAt, undefined);
}
assert.equal(map.usageRecovery({ rateLimits: { ...snapshot, primary: null, secondary: null } }).resetsAt, undefined);
const multiple = map.usageRecovery({
rateLimits: snapshot,
rateLimitsByLimitId: {
codex: snapshot,
other: { ...snapshot, limitId: "other", secondary: { ...secondary, resetsAt: 1_900_200_000 } },
},
});
assert.equal(multiple.resetsAt, new Date(1_900_200_000_000).toISOString());
assert.equal(multiple.scope, undefined);
const missingBucket = map.usageRecovery({
rateLimitsByLimitId: {
codex: snapshot,
other: { ...snapshot, limitId: "other", primary: { usedPercent: 100 }, secondary: null },
},
});
assert.equal(missingBucket.resetsAt, undefined);
for (const rateLimitReachedType of [
"workspace_owner_credits_depleted",
"workspace_member_credits_depleted",
"workspace_owner_usage_limit_reached",
"workspace_member_usage_limit_reached",
]) {
assert.equal(map.usageRecovery({ rateLimits: { ...snapshot, rateLimitReachedType } }).resetsAt, undefined);
}
assert.equal(
map.usageRecovery({
rateLimits: { ...snapshot, spendControlReached: true, individualLimit: { resetsAt: 1_900_200_000 } },
}).resetsAt,
new Date(1_900_200_000_000).toISOString(),
);
assert.equal(map.usageRecovery({ rateLimits: { ...snapshot, spendControlReached: true } }).resetsAt, undefined);
});
test("reasoning prefers the summary over the raw content", () => {
assert.equal(map.reasoningText(["done"], ["raw"]), "done");
assert.equal(map.reasoningText([], ["raw"]), "raw");
assert.equal(map.reasoningText(["a", "b"], []), "a\n\nb");
});
test("the transcript maps every role, and the title comes from the first message", () => {
const turns = [
{
id: "t1",
status: "completed",
error: null,
items: [
{ type: "userMessage", id: "u1", clientId: null, content: [{ type: "text", text: "hi", text_elements: [] }] },
{ type: "reasoning", id: "r1", summary: ["thinking"], content: [] },
{
type: "agentMessage",
id: "m1",
text: "hello",
phase: null,
memoryCitation: null,
delivery: null,
questions: null,
},
{
type: "commandExecution",
id: "e1",
command: "ls",
cwd: "/w",
status: "completed",
commandActions: [{ type: "listFiles", command: "ls", path: "/w" }],
aggregatedOutput: "a\n",
exitCode: 0,
},
{ type: "plan", id: "p1", text: "1. do it" },
{ type: "contextCompaction", id: "cc1" },
],
},
];
const items = map.transcriptWithSubagents(turns, new Map(), null);
assert.deepEqual(
items.map((item) => item.role),
["user", "reasoning", "assistant", "tool", "plan", "notice"],
);
assert.ok(items[3].role === "tool" && items[4].role === "plan" && items[5].role === "notice");
assert.equal(items[3].call.kind, "search");
assert.deepEqual(items[3].call.locations, [{ path: "/w" }]);
assert.deepEqual(items[4].plan, { entries: [{ content: "1. do it", status: "pending" }] });
assert.equal(items[5].text, "Context compacted");
assert.equal(map.threadTitle(null, "", turns), "hi");
assert.equal(map.threadTitle("Named", "preview", turns), "Named");
assert.equal(map.threadTitle(null, " First line\nsecond", turns), "First line");
assert.equal(map.threadTitle(null, "", []), null);
});
test("user messages keep text and paths, and drop inline images", () => {
assert.deepEqual(
map.userBlocks([
{ type: "text", text: "look" },
{ type: "localImage", path: "/a.png" },
{ type: "mention", path: "/b.rs" },
{ type: "skill", path: "/skills/x" },
{ type: "image", url: "data:..." },
{ type: "future" },
]),
[
{ type: "text", text: "look" },
{ type: "file_ref", path: "/a.png" },
{ type: "file_ref", path: "/b.rs" },
{ type: "file_ref", path: "/skills/x" },
],
);
});
test("the stdin notice of codex exec is not reasoning", () => {
assert.equal(
map.transcriptItem({
type: "reasoning",
id: "r",
summary: ["Reading additional input from stdin..."],
content: [],
}),
null,
);
assert.equal(map.isStdinNotice(" Reading additional input from stdin...\n"), true);
assert.equal(map.isStdinNotice("Reading additional input from stdin... and more"), false);
});
test("dynamic tool calls show their input and result", () => {
const started = map.toolCallFromItem({
type: "dynamicToolCall",
id: "call-1",
namespace: null,
tool: "execute",
arguments: { code: "return 1 + 1" },
status: "inProgress",
contentItems: null,
success: null,
durationMs: null,
});
assert.ok(started);
assert.equal(started.title, "execute");
assert.equal(started.name, "execute");
assert.equal(started.status, "running");
assert.deepEqual(started.input, { code: "return 1 + 1" }, "the code stays visible");
assert.equal(started.content, undefined);
const completed = map.toolCallFromItem({
type: "dynamicToolCall",
id: "call-1",
namespace: null,
tool: "execute",
arguments: { code: "return 1 + 1" },
status: "completed",
contentItems: [
{ type: "inputText", text: "2" },
{ type: "inputImage", imageUrl: "data:image/png;base64,AA==" },
],
success: true,
durationMs: 12,
});
assert.ok(completed);
assert.equal(completed.status, "completed");
assert.deepEqual(completed.content, [
{ type: "text", text: "2" },
{ type: "image", mimeType: "image/png", data: "AA==" },
]);
const failed = map.toolCallFromItem({
type: "dynamicToolCall",
id: "call-2",
tool: "now",
arguments: {},
status: "completed",
contentItems: [{ type: "inputText", text: "no clock" }],
success: false,
});
assert.ok(failed);
assert.equal(failed.status, "failed", "success: false is a failure whatever the status says");
});
test("mcp calls, web searches and image views are tool rows", () => {
const mcp = map.toolCallFromItem({
type: "mcpToolCall",
id: "m",
server: "gh",
tool: "pr",
status: "completed",
arguments: { n: 1 },
result: { ok: 1 },
error: null,
});
assert.ok(mcp);
assert.deepEqual([mcp.name, mcp.kind, mcp.status], ["gh.pr", "other", "completed"]);
const mcpContent = present(mcp.content)[0];
assert.ok(mcpContent.type === "text");
assert.equal(mcpContent.text, '{\n "ok": 1\n}');
const broken = map.toolCallFromItem({
type: "mcpToolCall",
id: "m",
server: "gh",
tool: "pr",
status: "completed",
error: "denied",
});
assert.ok(broken);
const brokenContent = present(broken.content)[0];
assert.ok(brokenContent.type === "text");
assert.deepEqual([broken.status, brokenContent.text], ["failed", "denied"]);
const search = map.toolCallFromItem({ type: "webSearch", id: "w", query: "" });
assert.ok(search);
assert.deepEqual([search.kind, search.title], ["fetch", "Web search"]);
const image = map.toolCallFromItem({ type: "imageView", id: "i", path: "/a.png" });
assert.ok(image);
assert.deepEqual([image.kind, image.locations], ["read", [{ path: "/a.png" }]]);
});
test("tool results become codex content items", () => {
assert.deepEqual(
map.dynamicToolResponse({
content: [
{ type: "text", text: "hi" },
{ type: "image", data: "AA==", mimeType: "image/png" },
],
}),
{
contentItems: [
{ type: "inputText", text: "hi" },
{ type: "inputImage", imageUrl: "data:image/png;base64,AA==" },
],
success: true,
},
);
assert.equal(map.dynamicToolResponse({ content: [{ type: "text", text: "boom" }], isError: true }).success, false);
});
test("plan steps map to plan entries", () => {
const plan = map.planFromSteps([
{ step: "one", status: "completed" },
{ step: "two", status: "inProgress" },
{ step: "three", status: "pending" },
]);
assert.deepEqual(
present(plan.entries).map((entry) => entry.status),
["completed", "in_progress", "pending"],
);
});
test("subagent threads are not listed as chats", () => {
const threads = [
{ id: "root", preview: "hi", createdAt: 1, updatedAt: 1, parentThreadId: null },
{ id: "child", preview: "x", createdAt: 1, updatedAt: 1, parentThreadId: "root" },
{
id: "old-child",
preview: "x",
createdAt: 1,
updatedAt: 1,
source: { subagent: { thread_spawn: { parent_thread_id: "root", depth: 1 } } },
},
];
assert.deepEqual(
threads.filter((thread) => map.threadParent(thread) === null).map((thread) => thread.id),
["root"],
);
});
test("a first line is cut at 80 characters", () => {
const long = "é".repeat(100);
assert.equal(map.firstLine(long), `${"é".repeat(80)}…`);
assert.equal(map.firstLine(" a\r\nb"), "a");
});
function present<T>(value: T | null | undefined): T {
assert.ok(value != null);
return value;
}Versions
| Version | Published | Plugin API | Size | Permissions | Status |
|---|---|---|---|---|---|
| 0.2.0latest | Oct 5, 2026 | >=2 <3 | 78.1 KB | 4 permissions | Listed |
No comments yet.