mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-27 09:02:15 +00:00
refactor(test): dedupe agent and status command fixtures
This commit is contained in:
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -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()],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user