refactor(test): dedupe agent and status command fixtures

This commit is contained in:
Peter Steinberger
2026-02-16 16:48:28 +00:00
parent 44ef045614
commit ac5f6e7c9d
5 changed files with 168 additions and 211 deletions

View File

@@ -57,6 +57,24 @@ async function withTempStore(
}
}
function mockGatewaySuccessReply(text = "hello") {
vi.mocked(callGateway).mockResolvedValue({
runId: "idem-1",
status: "ok",
result: {
payloads: [{ text }],
meta: { stub: true },
},
});
}
function mockLocalAgentReply(text = "local") {
vi.mocked(agentCommand).mockImplementationOnce(async (_opts, rt) => {
rt.log?.(text);
return { payloads: [{ text }], meta: { stub: true } };
});
}
beforeEach(() => {
vi.clearAllMocks();
});
@@ -64,14 +82,7 @@ beforeEach(() => {
describe("agentCliCommand", () => {
it("uses a timer-safe max gateway timeout when --timeout is 0", async () => {
await withTempStore(async () => {
vi.mocked(callGateway).mockResolvedValue({
runId: "idem-1",
status: "ok",
result: {
payloads: [{ text: "hello" }],
meta: { stub: true },
},
});
mockGatewaySuccessReply();
await agentCliCommand({ message: "hi", to: "+1555", timeout: "0" }, runtime);
@@ -83,14 +94,7 @@ describe("agentCliCommand", () => {
it("uses gateway by default", async () => {
await withTempStore(async () => {
vi.mocked(callGateway).mockResolvedValue({
runId: "idem-1",
status: "ok",
result: {
payloads: [{ text: "hello" }],
meta: { stub: true },
},
});
mockGatewaySuccessReply();
await agentCliCommand({ message: "hi", to: "+1555" }, runtime);
@@ -103,10 +107,7 @@ describe("agentCliCommand", () => {
it("falls back to embedded agent when gateway fails", async () => {
await withTempStore(async () => {
vi.mocked(callGateway).mockRejectedValue(new Error("gateway not connected"));
vi.mocked(agentCommand).mockImplementationOnce(async (_opts, rt) => {
rt.log?.("local");
return { payloads: [{ text: "local" }], meta: { stub: true } };
});
mockLocalAgentReply();
await agentCliCommand({ message: "hi", to: "+1555" }, runtime);
@@ -118,10 +119,7 @@ describe("agentCliCommand", () => {
it("skips gateway when --local is set", async () => {
await withTempStore(async () => {
vi.mocked(agentCommand).mockImplementationOnce(async (_opts, rt) => {
rt.log?.("local");
return { payloads: [{ text: "local" }], meta: { stub: true } };
});
mockLocalAgentReply();
await agentCliCommand(
{

View File

@@ -61,6 +61,14 @@ function mockConfig(
});
}
function writeSessionStoreSeed(
storePath: string,
sessions: Record<string, Record<string, unknown>>,
) {
fs.mkdirSync(path.dirname(storePath), { recursive: true });
fs.writeFileSync(storePath, JSON.stringify(sessions, null, 2));
}
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
@@ -114,21 +122,13 @@ describe("agentCommand", () => {
it("resumes when session-id is provided", async () => {
await withTempHome(async (home) => {
const store = path.join(home, "sessions.json");
fs.mkdirSync(path.dirname(store), { recursive: true });
fs.writeFileSync(
store,
JSON.stringify(
{
foo: {
sessionId: "session-123",
updatedAt: Date.now(),
systemSent: true,
},
},
null,
2,
),
);
writeSessionStoreSeed(store, {
foo: {
sessionId: "session-123",
updatedAt: Date.now(),
systemSent: true,
},
});
mockConfig(home, store);
await agentCommand({ message: "resume me", sessionId: "session-123" }, runtime);
@@ -199,22 +199,14 @@ describe("agentCommand", () => {
it("uses default fallback list for session model overrides", async () => {
await withTempHome(async (home) => {
const store = path.join(home, "sessions.json");
fs.mkdirSync(path.dirname(store), { recursive: true });
fs.writeFileSync(
store,
JSON.stringify(
{
"agent:main:subagent:test": {
sessionId: "session-subagent",
updatedAt: Date.now(),
providerOverride: "anthropic",
modelOverride: "claude-opus-4-5",
},
},
null,
2,
),
);
writeSessionStoreSeed(store, {
"agent:main:subagent:test": {
sessionId: "session-subagent",
updatedAt: Date.now(),
providerOverride: "anthropic",
modelOverride: "claude-opus-4-5",
},
});
mockConfig(home, store, {
model: {
@@ -264,20 +256,12 @@ describe("agentCommand", () => {
it("keeps explicit sessionKey even when sessionId exists elsewhere", async () => {
await withTempHome(async (home) => {
const store = path.join(home, "sessions.json");
fs.mkdirSync(path.dirname(store), { recursive: true });
fs.writeFileSync(
store,
JSON.stringify(
{
"agent:main:main": {
sessionId: "sess-main",
updatedAt: Date.now(),
},
},
null,
2,
),
);
writeSessionStoreSeed(store, {
"agent:main:main": {
sessionId: "sess-main",
updatedAt: Date.now(),
},
});
mockConfig(home, store);
await agentCommand(

View File

@@ -43,6 +43,14 @@ function getWrittenMainIdentity() {
return written.agents?.list?.find((entry) => entry.id === "main")?.identity;
}
async function runIdentityCommandFromWorkspace(workspace: string, fromIdentity = true) {
configMocks.readConfigFileSnapshot.mockResolvedValue({
...baseConfigSnapshot,
config: { agents: { list: [{ id: "main", workspace }] } },
});
await agentsSetIdentityCommand({ workspace, fromIdentity }, runtime);
}
describe("agents set-identity command", () => {
beforeEach(() => {
configMocks.readConfigFileSnapshot.mockReset();
@@ -171,12 +179,7 @@ describe("agents set-identity command", () => {
const { workspace } = await createIdentityWorkspace();
await writeIdentityFile(workspace, ["- Avatar: avatars/only.png"]);
configMocks.readConfigFileSnapshot.mockResolvedValue({
...baseConfigSnapshot,
config: { agents: { list: [{ id: "main", workspace }] } },
});
await agentsSetIdentityCommand({ workspace, fromIdentity: true }, runtime);
await runIdentityCommandFromWorkspace(workspace);
expect(getWrittenMainIdentity()).toEqual({
avatar: "avatars/only.png",
@@ -202,12 +205,7 @@ describe("agents set-identity command", () => {
it("errors when identity data is missing", async () => {
const { workspace } = await createIdentityWorkspace();
configMocks.readConfigFileSnapshot.mockResolvedValue({
...baseConfigSnapshot,
config: { agents: { list: [{ id: "main", workspace }] } },
});
await agentsSetIdentityCommand({ workspace, fromIdentity: true }, runtime);
await runIdentityCommandFromWorkspace(workspace);
expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("No identity data found"));
expect(runtime.exit).toHaveBeenCalledWith(1);

View File

@@ -12,20 +12,79 @@ afterAll(() => {
envSnapshot.restore();
});
const mocks = vi.hoisted(() => ({
loadSessionStore: vi.fn().mockReturnValue({
function createDefaultSessionStoreEntry() {
return {
updatedAt: Date.now() - 60_000,
verboseLevel: "on",
thinkingLevel: "low",
inputTokens: 2_000,
outputTokens: 3_000,
totalTokens: 5_000,
contextTokens: 10_000,
model: "pi:opus",
sessionId: "abc123",
systemSent: true,
};
}
function createUnknownUsageSessionStore() {
return {
"+1000": {
updatedAt: Date.now() - 60_000,
verboseLevel: "on",
thinkingLevel: "low",
inputTokens: 2_000,
outputTokens: 3_000,
totalTokens: 5_000,
contextTokens: 10_000,
model: "pi:opus",
sessionId: "abc123",
systemSent: true,
},
};
}
function createChannelIssueCollector(channel: string) {
return (accounts: Array<Record<string, unknown>>) =>
accounts
.filter((account) => typeof account.lastError === "string" && account.lastError)
.map((account) => ({
channel,
accountId: typeof account.accountId === "string" ? account.accountId : "default",
message: `Channel error: ${String(account.lastError)}`,
}));
}
function createErrorChannelPlugin(params: { id: string; label: string; docsPath: string }) {
return {
id: params.id,
meta: {
id: params.id,
label: params.label,
selectionLabel: params.label,
docsPath: params.docsPath,
blurb: "mock",
},
config: {
listAccountIds: () => ["default"],
resolveAccount: () => ({}),
},
status: {
collectStatusIssues: createChannelIssueCollector(params.id),
},
};
}
async function withUnknownUsageStore(run: () => Promise<void>) {
const originalLoadSessionStore = mocks.loadSessionStore.getMockImplementation();
mocks.loadSessionStore.mockReturnValue(createUnknownUsageSessionStore());
try {
await run();
} finally {
if (originalLoadSessionStore) {
mocks.loadSessionStore.mockImplementation(originalLoadSessionStore);
}
}
}
const mocks = vi.hoisted(() => ({
loadSessionStore: vi.fn().mockReturnValue({
"+1000": createDefaultSessionStoreEntry(),
}),
resolveMainSessionKey: vi.fn().mockReturnValue("agent:main:main"),
resolveStorePath: vi.fn().mockReturnValue("/tmp/sessions.json"),
@@ -148,52 +207,18 @@ vi.mock("../channels/plugins/index.js", () => ({
},
},
{
id: "signal",
meta: {
...createErrorChannelPlugin({
id: "signal",
label: "Signal",
selectionLabel: "Signal",
docsPath: "/platforms/signal",
blurb: "mock",
},
config: {
listAccountIds: () => ["default"],
resolveAccount: () => ({}),
},
status: {
collectStatusIssues: (accounts: Array<Record<string, unknown>>) =>
accounts
.filter((account) => typeof account.lastError === "string" && account.lastError)
.map((account) => ({
channel: "signal",
accountId: typeof account.accountId === "string" ? account.accountId : "default",
message: `Channel error: ${String(account.lastError)}`,
})),
},
}),
},
{
id: "imessage",
meta: {
...createErrorChannelPlugin({
id: "imessage",
label: "iMessage",
selectionLabel: "iMessage",
docsPath: "/platforms/mac",
blurb: "mock",
},
config: {
listAccountIds: () => ["default"],
resolveAccount: () => ({}),
},
status: {
collectStatusIssues: (accounts: Array<Record<string, unknown>>) =>
accounts
.filter((account) => typeof account.lastError === "string" && account.lastError)
.map((account) => ({
channel: "imessage",
accountId: typeof account.accountId === "string" ? account.accountId : "default",
message: `Channel error: ${String(account.lastError)}`,
})),
},
}),
},
] as unknown,
}));
@@ -210,9 +235,13 @@ vi.mock("../gateway/call.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../gateway/call.js")>();
return { ...actual, callGateway: mocks.callGateway };
});
vi.mock("../gateway/session-utils.js", () => ({
listAgentsForGateway: mocks.listAgentsForGateway,
}));
vi.mock("../gateway/session-utils.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../gateway/session-utils.js")>();
return {
...actual,
listAgentsForGateway: mocks.listAgentsForGateway,
};
});
vi.mock("../infra/openclaw-root.js", () => ({
resolveOpenClawPackageRoot: vi.fn().mockResolvedValue("/tmp/openclaw"),
}));
@@ -318,52 +347,24 @@ describe("statusCommand", () => {
});
it("surfaces unknown usage when totalTokens is missing", async () => {
const originalLoadSessionStore = mocks.loadSessionStore.getMockImplementation();
mocks.loadSessionStore.mockReturnValue({
"+1000": {
updatedAt: Date.now() - 60_000,
inputTokens: 2_000,
outputTokens: 3_000,
contextTokens: 10_000,
model: "pi:opus",
},
await withUnknownUsageStore(async () => {
(runtime.log as vi.Mock).mockClear();
await statusCommand({ json: true }, runtime as never);
const payload = JSON.parse((runtime.log as vi.Mock).mock.calls.at(-1)?.[0]);
expect(payload.sessions.recent[0].totalTokens).toBeNull();
expect(payload.sessions.recent[0].totalTokensFresh).toBe(false);
expect(payload.sessions.recent[0].percentUsed).toBeNull();
expect(payload.sessions.recent[0].remainingTokens).toBeNull();
});
(runtime.log as vi.Mock).mockClear();
await statusCommand({ json: true }, runtime as never);
const payload = JSON.parse((runtime.log as vi.Mock).mock.calls.at(-1)?.[0]);
expect(payload.sessions.recent[0].totalTokens).toBeNull();
expect(payload.sessions.recent[0].totalTokensFresh).toBe(false);
expect(payload.sessions.recent[0].percentUsed).toBeNull();
expect(payload.sessions.recent[0].remainingTokens).toBeNull();
if (originalLoadSessionStore) {
mocks.loadSessionStore.mockImplementation(originalLoadSessionStore);
}
});
it("prints unknown usage in formatted output when totalTokens is missing", async () => {
const originalLoadSessionStore = mocks.loadSessionStore.getMockImplementation();
mocks.loadSessionStore.mockReturnValue({
"+1000": {
updatedAt: Date.now() - 60_000,
inputTokens: 2_000,
outputTokens: 3_000,
contextTokens: 10_000,
model: "pi:opus",
},
});
try {
await withUnknownUsageStore(async () => {
(runtime.log as vi.Mock).mockClear();
await statusCommand({}, runtime as never);
const logs = (runtime.log as vi.Mock).mock.calls.map((c) => String(c[0]));
expect(logs.some((line) => line.includes("unknown/") && line.includes("(?%)"))).toBe(true);
} finally {
if (originalLoadSessionStore) {
mocks.loadSessionStore.mockImplementation(originalLoadSessionStore);
}
}
});
});
it("prints formatted lines otherwise", async () => {
@@ -501,18 +502,7 @@ describe("statusCommand", () => {
};
}
return {
"+1000": {
updatedAt: Date.now() - 60_000,
verboseLevel: "on",
thinkingLevel: "low",
inputTokens: 2_000,
outputTokens: 3_000,
totalTokens: 5_000,
contextTokens: 10_000,
model: "pi:opus",
sessionId: "abc123",
systemSent: true,
},
"+1000": createDefaultSessionStoreEntry(),
};
});

View File

@@ -2,6 +2,23 @@ import { describe, expect, it } from "vitest";
import type { StatusSummary } from "./status.types.js";
import { redactSensitiveStatusSummary } from "./status.summary.js";
function createRecentSessionRow() {
return {
key: "main",
kind: "direct" as const,
sessionId: "sess-1",
updatedAt: 1,
age: 2,
totalTokens: 3,
totalTokensFresh: true,
remainingTokens: 4,
percentUsed: 5,
model: "gpt-5",
contextTokens: 200_000,
flags: ["id:sess-1"],
};
}
describe("redactSensitiveStatusSummary", () => {
it("removes sensitive session and path details while preserving summary structure", () => {
const input: StatusSummary = {
@@ -15,43 +32,13 @@ describe("redactSensitiveStatusSummary", () => {
paths: ["/tmp/openclaw/sessions.json"],
count: 1,
defaults: { model: "gpt-5", contextTokens: 200_000 },
recent: [
{
key: "main",
kind: "direct",
sessionId: "sess-1",
updatedAt: 1,
age: 2,
totalTokens: 3,
totalTokensFresh: true,
remainingTokens: 4,
percentUsed: 5,
model: "gpt-5",
contextTokens: 200_000,
flags: ["id:sess-1"],
},
],
recent: [createRecentSessionRow()],
byAgent: [
{
agentId: "main",
path: "/tmp/openclaw/main-sessions.json",
count: 1,
recent: [
{
key: "main",
kind: "direct",
sessionId: "sess-1",
updatedAt: 1,
age: 2,
totalTokens: 3,
totalTokensFresh: true,
remainingTokens: 4,
percentUsed: 5,
model: "gpt-5",
contextTokens: 200_000,
flags: ["id:sess-1"],
},
],
recent: [createRecentSessionRow()],
},
],
},