mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-07 16:06:19 +00:00
* 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
302 lines
8.5 KiB
TypeScript
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:");
|
|
});
|
|
});
|