Files
moltbot/src/cli/pairing-cli.test.ts
pashpashpash 6ce1058296 Wire diagnostics through the core chat command (#72936)
* feat: wire codex diagnostics feedback

* fix: harden codex diagnostics hints

* fix: neutralize codex diagnostics output

* fix: tighten codex diagnostics safeguards

* fix: bound codex diagnostics feedback output

* fix: tighten codex diagnostics throttling

* fix: confirm codex diagnostics uploads

* docs: clarify codex diagnostics add-on

* fix: route diagnostics through core command

* fix: tighten diagnostics authorization

* fix: pin diagnostics to bundled codex command

* fix: limit owner status in plugin commands

* fix: scope diagnostics confirmations

* fix: scope codex diagnostics cooldowns

* fix: harden codex diagnostics ownership scopes

* fix: harden diagnostics command trust and display

* fix: keep diagnostics command trust internal

* fix: clarify diagnostics exec boundary

* fix: consume codex diagnostics confirmations atomically

* test: include codex diagnostics binding metadata

* test: use string codex binding timestamps

* fix: keep reserved command trust host-only

* fix: harden diagnostics trust and resume hints

* wire diagnostics through exec approval

* fix: keep diagnostics tests aligned with bundled root trust

* fix telegram diagnostics owner auth

* route trajectory exports through exec approval

* fix trajectory exec command encoding

* fix telegram group owner auth

* fix export trajectory approval hardening

* fix pairing command owner bootstrap

* fix telegram owner exec approvals

* fix: make diagnostics approval flow pasteable

* fix: route native sensitive command followups

* fix: invoke diagnostics exports with current cli

* fix: refresh exec approval protocol models

* fix: list codex diagnostics from thread bindings

* fix: fold codex diagnostics into exec approval

* fix: preserve diagnostics approval line breaks

* docs: clarify diagnostics codex workflow
2026-04-29 07:40:37 +09:00

302 lines
8.5 KiB
TypeScript

import { Command } from "commander";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { registerPairingCli } from "./pairing-cli.js";
const mocks = vi.hoisted(() => ({
listChannelPairingRequests: vi.fn(),
approveChannelPairingCode: vi.fn(),
notifyPairingApproved: vi.fn(),
readConfigFileSnapshotForWrite: vi.fn(),
replaceConfigFile: vi.fn(),
normalizeChannelId: vi.fn((raw: string) => {
if (!raw) {
return null;
}
if (raw === "imsg") {
return "imessage";
}
if (["telegram", "discord", "imessage"].includes(raw)) {
return raw;
}
return null;
}),
getPairingAdapter: vi.fn((channel: string) => ({
idLabel: pairingIdLabels[channel] ?? "userId",
})),
listPairingChannels: vi.fn(() => ["telegram", "discord", "imessage"]),
}));
const {
listChannelPairingRequests,
approveChannelPairingCode,
notifyPairingApproved,
readConfigFileSnapshotForWrite,
replaceConfigFile,
normalizeChannelId,
getPairingAdapter,
listPairingChannels,
} = mocks;
const pairingIdLabels: Record<string, string> = {
telegram: "telegramUserId",
discord: "discordUserId",
};
vi.mock("../pairing/pairing-store.js", () => ({
listChannelPairingRequests: mocks.listChannelPairingRequests,
approveChannelPairingCode: mocks.approveChannelPairingCode,
}));
vi.mock("../channels/plugins/pairing.js", () => ({
listPairingChannels: mocks.listPairingChannels,
notifyPairingApproved: mocks.notifyPairingApproved,
getPairingAdapter: mocks.getPairingAdapter,
}));
vi.mock("../channels/plugins/index.js", () => ({
normalizeChannelId: mocks.normalizeChannelId,
}));
vi.mock("../config/config.js", () => ({
getRuntimeConfig: vi.fn().mockReturnValue({}),
loadConfig: vi.fn().mockReturnValue({}),
readConfigFileSnapshotForWrite: mocks.readConfigFileSnapshotForWrite,
replaceConfigFile: mocks.replaceConfigFile,
}));
describe("pairing cli", () => {
beforeEach(() => {
listChannelPairingRequests.mockClear();
listChannelPairingRequests.mockResolvedValue([]);
approveChannelPairingCode.mockClear();
approveChannelPairingCode.mockResolvedValue({
id: "123",
entry: {
id: "123",
code: "ABCDEFGH",
createdAt: "2026-01-08T00:00:00Z",
lastSeenAt: "2026-01-08T00:00:00Z",
},
});
notifyPairingApproved.mockClear();
readConfigFileSnapshotForWrite.mockClear();
readConfigFileSnapshotForWrite.mockResolvedValue({
snapshot: {
path: "/tmp/openclaw.json",
exists: true,
raw: "{}",
parsed: {},
valid: true,
issues: [],
legacyIssues: [],
sourceConfig: {},
runtimeConfig: {},
},
writeOptions: {},
});
replaceConfigFile.mockClear();
replaceConfigFile.mockResolvedValue(undefined);
normalizeChannelId.mockClear();
getPairingAdapter.mockClear();
listPairingChannels.mockClear();
notifyPairingApproved.mockResolvedValue(undefined);
});
function createProgram() {
const program = new Command();
program.name("test");
registerPairingCli(program);
return program;
}
async function runPairing(args: string[]) {
const program = createProgram();
await program.parseAsync(args, { from: "user" });
}
function mockApprovedPairing() {
approveChannelPairingCode.mockResolvedValueOnce({
id: "123",
entry: {
id: "123",
code: "ABCDEFGH",
createdAt: "2026-01-08T00:00:00Z",
lastSeenAt: "2026-01-08T00:00:00Z",
},
});
}
it("evaluates pairing channels when registering the CLI (not at import)", async () => {
expect(listPairingChannels).not.toHaveBeenCalled();
createProgram();
expect(listPairingChannels).toHaveBeenCalledTimes(1);
});
it.each([
{
name: "telegram ids",
channel: "telegram",
id: "123",
label: "telegramUserId",
meta: { username: "peter" },
},
{
name: "discord ids",
channel: "discord",
id: "999",
label: "discordUserId",
meta: { tag: "Ada#0001" },
},
])("labels $name correctly", async ({ channel, id, label, meta }) => {
listChannelPairingRequests.mockResolvedValueOnce([
{
id,
code: "ABC123",
createdAt: "2026-01-08T00:00:00Z",
lastSeenAt: "2026-01-08T00:00:00Z",
meta,
},
]);
const log = vi.spyOn(console, "log").mockImplementation(() => {});
try {
await runPairing(["pairing", "list", "--channel", channel]);
const output = log.mock.calls.map((call) => call.join(" ")).join("\n");
expect(output).toContain(label);
expect(output).toContain(id);
} finally {
log.mockRestore();
}
});
it("accepts channel as positional for list", async () => {
listChannelPairingRequests.mockResolvedValueOnce([]);
await runPairing(["pairing", "list", "telegram"]);
expect(listChannelPairingRequests).toHaveBeenCalledWith("telegram");
});
it("forwards --account for list", async () => {
listChannelPairingRequests.mockResolvedValueOnce([]);
await runPairing(["pairing", "list", "--channel", "telegram", "--account", "yy"]);
expect(listChannelPairingRequests).toHaveBeenCalledWith("telegram", process.env, "yy");
});
it("normalizes channel aliases", async () => {
listChannelPairingRequests.mockResolvedValueOnce([]);
await runPairing(["pairing", "list", "imsg"]);
expect(normalizeChannelId).toHaveBeenCalledWith("imsg");
expect(listChannelPairingRequests).toHaveBeenCalledWith("imessage");
});
it("accepts extension channels outside the registry", async () => {
listChannelPairingRequests.mockResolvedValueOnce([]);
await runPairing(["pairing", "list", "zalo"]);
expect(normalizeChannelId).toHaveBeenCalledWith("zalo");
expect(listChannelPairingRequests).toHaveBeenCalledWith("zalo");
});
it("defaults list to the sole available channel", async () => {
listPairingChannels.mockReturnValueOnce(["slack"]);
listChannelPairingRequests.mockResolvedValueOnce([]);
await runPairing(["pairing", "list"]);
expect(listChannelPairingRequests).toHaveBeenCalledWith("slack");
});
it("accepts channel as positional for approve (npm-run compatible)", async () => {
mockApprovedPairing();
const log = vi.spyOn(console, "log").mockImplementation(() => {});
try {
await runPairing(["pairing", "approve", "telegram", "ABCDEFGH"]);
expect(approveChannelPairingCode).toHaveBeenCalledWith({
channel: "telegram",
code: "ABCDEFGH",
});
expect(replaceConfigFile).toHaveBeenCalledWith(
expect.objectContaining({
nextConfig: {
commands: {
ownerAllowFrom: ["telegram:123"],
},
},
}),
);
expect(log).toHaveBeenCalledWith(expect.stringContaining("Approved"));
expect(log).toHaveBeenCalledWith(expect.stringContaining("Command owner configured"));
} finally {
log.mockRestore();
}
});
it("does not overwrite an existing command owner when approving pairing", async () => {
readConfigFileSnapshotForWrite.mockResolvedValueOnce({
snapshot: {
path: "/tmp/openclaw.json",
exists: true,
raw: "{}",
parsed: {},
valid: true,
issues: [],
legacyIssues: [],
sourceConfig: { commands: { ownerAllowFrom: ["discord:999"] } },
runtimeConfig: { commands: { ownerAllowFrom: ["discord:999"] } },
},
writeOptions: {},
});
mockApprovedPairing();
await runPairing(["pairing", "approve", "telegram", "ABCDEFGH"]);
expect(replaceConfigFile).not.toHaveBeenCalled();
});
it("forwards --account for approve", async () => {
mockApprovedPairing();
await runPairing([
"pairing",
"approve",
"--channel",
"telegram",
"--account",
"yy",
"ABCDEFGH",
]);
expect(approveChannelPairingCode).toHaveBeenCalledWith({
channel: "telegram",
code: "ABCDEFGH",
accountId: "yy",
});
});
it("defaults approve to the sole available channel when only code is provided", async () => {
listPairingChannels.mockReturnValueOnce(["slack"]);
mockApprovedPairing();
await runPairing(["pairing", "approve", "ABCDEFGH"]);
expect(approveChannelPairingCode).toHaveBeenCalledWith({
channel: "slack",
code: "ABCDEFGH",
});
});
it("keeps approve usage error when multiple channels exist and channel is omitted", async () => {
await expect(runPairing(["pairing", "approve", "ABCDEFGH"])).rejects.toThrow("Usage:");
});
});