mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-16 15:43:57 -06:00
41c00a65d6
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
723 lines
23 KiB
TypeScript
723 lines
23 KiB
TypeScript
// Status summary tests cover aggregate status text for channels, sessions, tasks, and audit findings.
|
|
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type { TaskAuditFinding } from "../tasks/task-registry.audit.js";
|
|
import type { TaskRecord, TaskRegistrySummary } from "../tasks/task-registry.types.js";
|
|
|
|
const statusSummaryMocks = vi.hoisted(() => ({
|
|
hasConfiguredChannelsForReadOnlyScope: vi.fn(() => true),
|
|
buildChannelSummary: vi.fn(async () => ["ok"]),
|
|
resolveProviderStaticModel: vi.fn(),
|
|
listSessionEntries: vi.fn<
|
|
(scope?: { agentId?: string; storePath?: string }) => Array<{
|
|
sessionKey: string;
|
|
entry: Record<string, unknown>;
|
|
}>
|
|
>(() => []),
|
|
configureTaskRegistryMaintenance: vi.fn(),
|
|
taskRegistrySummary: {
|
|
total: 0,
|
|
active: 0,
|
|
terminal: 0,
|
|
failures: 0,
|
|
byStatus: {
|
|
queued: 0,
|
|
running: 0,
|
|
succeeded: 0,
|
|
failed: 0,
|
|
timed_out: 0,
|
|
cancelled: 0,
|
|
lost: 0,
|
|
},
|
|
byRuntime: {
|
|
subagent: 0,
|
|
acp: 0,
|
|
cli: 0,
|
|
cron: 0,
|
|
},
|
|
} as TaskRegistrySummary,
|
|
inspectableTasks: [] as TaskRecord[],
|
|
reconcileInspectableTasks: vi.fn(() => statusSummaryMocks.inspectableTasks),
|
|
getInspectableTaskRegistrySummary: vi.fn(
|
|
(_tasks?: TaskRecord[]) => statusSummaryMocks.taskRegistrySummary,
|
|
),
|
|
taskAuditFindings: [
|
|
{
|
|
severity: "warn",
|
|
code: "delivery_failed",
|
|
detail: "terminal update delivery failed",
|
|
task: {
|
|
taskId: "task-delivery",
|
|
runtime: "subagent",
|
|
ownerKey: "agent:main:main",
|
|
requesterSessionKey: "agent:main:main",
|
|
scopeKind: "session",
|
|
task: "Deliver update",
|
|
status: "failed",
|
|
deliveryStatus: "failed",
|
|
notifyPolicy: "done_only",
|
|
createdAt: 1,
|
|
},
|
|
},
|
|
] as TaskAuditFinding[],
|
|
getInspectableTaskAuditFindings: vi.fn(
|
|
(_tasks?: TaskRecord[]) => statusSummaryMocks.taskAuditFindings,
|
|
),
|
|
}));
|
|
|
|
vi.mock("../plugins/channel-plugin-ids.js", () => ({
|
|
hasConfiguredChannelsForReadOnlyScope: statusSummaryMocks.hasConfiguredChannelsForReadOnlyScope,
|
|
}));
|
|
|
|
vi.mock("./status.summary.runtime.js", () => ({
|
|
statusSummaryRuntime: {
|
|
classifySessionKey: vi.fn(() => "direct"),
|
|
resolveConfiguredStatusModelRef: vi.fn(() => ({
|
|
provider: "openai",
|
|
model: "gpt-5.5",
|
|
})),
|
|
resolveSessionModelRef: vi.fn(() => ({
|
|
provider: "openai",
|
|
model: "gpt-5.5",
|
|
})),
|
|
resolveSessionRuntimeLabel: vi.fn(() => "OpenClaw Default"),
|
|
resolveContextTokensForModel: vi.fn(() => 200_000),
|
|
},
|
|
}));
|
|
|
|
vi.mock("../agents/defaults.js", () => ({
|
|
DEFAULT_CONTEXT_TOKENS: 200_000,
|
|
DEFAULT_MODEL: "gpt-5.5",
|
|
DEFAULT_PROVIDER: "openai",
|
|
}));
|
|
|
|
vi.mock("../agents/embedded-agent-runner/model.static-catalog.js", () => ({
|
|
createBundledStaticCatalogModelResolver: vi.fn(() =>
|
|
vi.fn(({ provider, modelId }) =>
|
|
provider === "openai" && modelId === "gpt-5.5"
|
|
? { contextWindow: 1_000_000, contextTokens: 272_000 }
|
|
: undefined,
|
|
),
|
|
),
|
|
createBundledProviderStaticCatalogModelResolver: vi.fn(
|
|
() => statusSummaryMocks.resolveProviderStaticModel,
|
|
),
|
|
createBundledProviderStaticCatalogContextResolver: vi.fn(
|
|
() => statusSummaryMocks.resolveProviderStaticModel,
|
|
),
|
|
}));
|
|
|
|
vi.mock("../config/io.js", () => ({
|
|
loadConfig: vi.fn(() => ({})),
|
|
}));
|
|
|
|
vi.mock("../config/config.js", () => ({
|
|
getRuntimeConfig: vi.fn(() => ({})),
|
|
projectConfigOntoRuntimeSourceSnapshot: vi.fn((config) => config),
|
|
}));
|
|
|
|
vi.mock("../config/sessions/paths.js", () => ({
|
|
resolveStorePath: vi.fn(() => "/tmp/sessions.json"),
|
|
}));
|
|
|
|
vi.mock("../config/sessions/session-accessor.js", () => ({
|
|
listSessionEntries: statusSummaryMocks.listSessionEntries,
|
|
}));
|
|
|
|
vi.mock("../gateway/agent-list.js", () => ({
|
|
listGatewayAgentsBasic: vi.fn(() => ({
|
|
defaultId: "main",
|
|
agents: [{ id: "main" }],
|
|
})),
|
|
}));
|
|
|
|
vi.mock("../infra/channel-summary.js", () => ({
|
|
buildChannelSummary: statusSummaryMocks.buildChannelSummary,
|
|
}));
|
|
|
|
vi.mock("../infra/heartbeat-summary.js", () => ({
|
|
resolveHeartbeatSummaryForAgent: vi.fn(() => ({
|
|
enabled: true,
|
|
every: "5m",
|
|
everyMs: 300_000,
|
|
})),
|
|
}));
|
|
|
|
vi.mock("../infra/system-events.js", () => ({
|
|
peekSystemEvents: vi.fn(() => []),
|
|
}));
|
|
|
|
vi.mock("../tasks/task-registry.maintenance.js", () => ({
|
|
configureTaskRegistryMaintenance: statusSummaryMocks.configureTaskRegistryMaintenance,
|
|
reconcileInspectableTasks: statusSummaryMocks.reconcileInspectableTasks,
|
|
getInspectableTaskRegistrySummary: statusSummaryMocks.getInspectableTaskRegistrySummary,
|
|
getInspectableTaskAuditFindings: statusSummaryMocks.getInspectableTaskAuditFindings,
|
|
}));
|
|
|
|
vi.mock("../routing/session-key.js", () => ({
|
|
normalizeAgentId: vi.fn((value: string) => value),
|
|
normalizeMainKey: vi.fn((value?: string) => value ?? "main"),
|
|
parseAgentSessionKey: vi.fn(() => null),
|
|
}));
|
|
|
|
vi.mock("../version.js", async () => {
|
|
const actual = await vi.importActual<typeof import("../version.js")>("../version.js");
|
|
return {
|
|
...actual,
|
|
resolveRuntimeServiceVersion: vi.fn(() => "2026.3.8"),
|
|
};
|
|
});
|
|
|
|
vi.mock("./status.link-channel.js", () => ({
|
|
resolveLinkChannelContext: vi.fn(async () => undefined),
|
|
}));
|
|
|
|
const { buildChannelSummary } = await import("../infra/channel-summary.js");
|
|
const { resolveStorePath } = await import("../config/sessions/paths.js");
|
|
const { listGatewayAgentsBasic } = await import("../gateway/agent-list.js");
|
|
const { resolveLinkChannelContext } = await import("./status.link-channel.js");
|
|
let getStatusSummary: typeof import("./status.summary.js").getStatusSummary;
|
|
let statusSummaryRuntime: typeof import("./status.summary.runtime.js").statusSummaryRuntime;
|
|
|
|
function toSessionEntrySummaries(store: Record<string, Record<string, unknown>>) {
|
|
return Object.entries(store).map(([sessionKey, entry]) => ({ sessionKey, entry }));
|
|
}
|
|
|
|
describe("getStatusSummary", () => {
|
|
beforeAll(async () => {
|
|
({ getStatusSummary } = await import("./status.summary.js"));
|
|
({ statusSummaryRuntime } = await import("./status.summary.runtime.js"));
|
|
});
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
statusSummaryMocks.taskRegistrySummary = {
|
|
total: 0,
|
|
active: 0,
|
|
terminal: 0,
|
|
failures: 0,
|
|
byStatus: {
|
|
queued: 0,
|
|
running: 0,
|
|
succeeded: 0,
|
|
failed: 0,
|
|
timed_out: 0,
|
|
cancelled: 0,
|
|
lost: 0,
|
|
},
|
|
byRuntime: {
|
|
subagent: 0,
|
|
acp: 0,
|
|
cli: 0,
|
|
cron: 0,
|
|
},
|
|
};
|
|
statusSummaryMocks.inspectableTasks = [];
|
|
statusSummaryMocks.taskAuditFindings = [
|
|
{
|
|
severity: "warn",
|
|
code: "delivery_failed",
|
|
detail: "terminal update delivery failed",
|
|
task: {
|
|
taskId: "task-delivery",
|
|
runtime: "subagent",
|
|
ownerKey: "agent:main:main",
|
|
requesterSessionKey: "agent:main:main",
|
|
scopeKind: "session",
|
|
task: "Deliver update",
|
|
status: "failed",
|
|
deliveryStatus: "failed",
|
|
notifyPolicy: "done_only",
|
|
createdAt: 1,
|
|
},
|
|
},
|
|
];
|
|
statusSummaryMocks.hasConfiguredChannelsForReadOnlyScope.mockReturnValue(true);
|
|
statusSummaryMocks.buildChannelSummary.mockResolvedValue(["ok"]);
|
|
statusSummaryMocks.resolveProviderStaticModel.mockReset();
|
|
statusSummaryMocks.resolveProviderStaticModel.mockImplementation(
|
|
async ({ provider, modelId }) =>
|
|
provider === "google" && modelId === "gemini-3.1-pro-preview"
|
|
? { contextWindow: 1_048_576 }
|
|
: undefined,
|
|
);
|
|
statusSummaryMocks.listSessionEntries.mockReturnValue([]);
|
|
vi.mocked(resolveStorePath).mockReturnValue("/tmp/sessions.json");
|
|
vi.mocked(listGatewayAgentsBasic).mockReturnValue({
|
|
defaultId: "main",
|
|
mainKey: "main",
|
|
scope: "per-sender",
|
|
agents: [{ id: "main" }],
|
|
});
|
|
});
|
|
|
|
it("includes runtimeVersion in the status payload", async () => {
|
|
const summary = await getStatusSummary();
|
|
|
|
expect(summary.runtimeVersion).toBe("2026.3.8");
|
|
expect(summary.heartbeat.defaultAgentId).toBe("main");
|
|
expect(summary.channelSummary).toEqual(["ok"]);
|
|
expect(summary.tasks.active).toBe(0);
|
|
expect(summary.taskAudit.warnings).toBe(1);
|
|
});
|
|
|
|
it("reuses one reconciled task snapshot for task summaries and audit findings", async () => {
|
|
const inspectableTasks: TaskRecord[] = [];
|
|
statusSummaryMocks.inspectableTasks = inspectableTasks;
|
|
|
|
await getStatusSummary();
|
|
|
|
expect(statusSummaryMocks.reconcileInspectableTasks).toHaveBeenCalledTimes(1);
|
|
expect(statusSummaryMocks.getInspectableTaskRegistrySummary).toHaveBeenCalledWith(
|
|
inspectableTasks,
|
|
);
|
|
expect(statusSummaryMocks.getInspectableTaskAuditFindings).toHaveBeenCalledWith(
|
|
inspectableTasks,
|
|
);
|
|
});
|
|
|
|
it("keeps retained lost tasks out of default status audit counts", async () => {
|
|
const cleanupAfter = Date.now() + 60_000;
|
|
statusSummaryMocks.taskRegistrySummary = {
|
|
...statusSummaryMocks.taskRegistrySummary,
|
|
total: 1,
|
|
terminal: 1,
|
|
failures: 1,
|
|
byStatus: {
|
|
...statusSummaryMocks.taskRegistrySummary.byStatus,
|
|
lost: 1,
|
|
},
|
|
};
|
|
statusSummaryMocks.taskAuditFindings = [
|
|
{
|
|
severity: "warn",
|
|
code: "lost",
|
|
detail: "task lost its backing session and is retained until cleanupAfter",
|
|
task: {
|
|
taskId: "task-lost-retained",
|
|
runtime: "subagent",
|
|
ownerKey: "agent:main:main",
|
|
requesterSessionKey: "agent:main:main",
|
|
scopeKind: "session",
|
|
task: "Retained lost",
|
|
status: "lost",
|
|
deliveryStatus: "pending",
|
|
notifyPolicy: "done_only",
|
|
createdAt: cleanupAfter - 60_000,
|
|
endedAt: cleanupAfter - 60_000,
|
|
cleanupAfter,
|
|
},
|
|
},
|
|
];
|
|
|
|
const summary = await getStatusSummary();
|
|
|
|
expect(summary.tasks.failures).toBe(0);
|
|
expect(summary.tasks.byStatus.lost).toBe(1);
|
|
expect(summary.taskAudit).toEqual({
|
|
total: 0,
|
|
warnings: 0,
|
|
errors: 0,
|
|
byCode: {
|
|
stale_queued: 0,
|
|
stale_running: 0,
|
|
lost: 0,
|
|
delivery_failed: 0,
|
|
missing_cleanup: 0,
|
|
inconsistent_timestamps: 0,
|
|
},
|
|
});
|
|
expect(summary.taskAuditRetainedLost).toEqual({
|
|
count: 1,
|
|
nextCleanupAfter: cleanupAfter,
|
|
});
|
|
});
|
|
|
|
it("skips channel summary imports when no channels are configured", async () => {
|
|
statusSummaryMocks.hasConfiguredChannelsForReadOnlyScope.mockReturnValue(false);
|
|
|
|
const summary = await getStatusSummary();
|
|
|
|
expect(summary.channelSummary).toStrictEqual([]);
|
|
expect(summary.linkChannel).toBeUndefined();
|
|
expect(statusSummaryMocks.hasConfiguredChannelsForReadOnlyScope).toHaveBeenCalledWith({
|
|
config: {},
|
|
});
|
|
expect(buildChannelSummary).not.toHaveBeenCalled();
|
|
expect(resolveLinkChannelContext).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("skips channel summary imports when explicitly disabled", async () => {
|
|
const summary = await getStatusSummary({ includeChannelSummary: false });
|
|
|
|
expect(summary.channelSummary).toStrictEqual([]);
|
|
expect(summary.linkChannel).toBeUndefined();
|
|
expect(statusSummaryMocks.hasConfiguredChannelsForReadOnlyScope).not.toHaveBeenCalled();
|
|
expect(buildChannelSummary).not.toHaveBeenCalled();
|
|
expect(resolveLinkChannelContext).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not trigger async context warmup while building status summaries", async () => {
|
|
await getStatusSummary();
|
|
|
|
const contextCall = vi.mocked(statusSummaryRuntime.resolveContextTokensForModel).mock
|
|
.calls[0]?.[0];
|
|
expect(contextCall?.allowAsyncLoad).toBe(false);
|
|
expect(contextCall).toMatchObject({
|
|
modelContextWindow: 1_000_000,
|
|
modelContextTokens: 272_000,
|
|
});
|
|
});
|
|
|
|
it("does not pass stale session contextTokens as status row overrides", async () => {
|
|
statusSummaryMocks.listSessionEntries.mockReturnValue(
|
|
toSessionEntrySummaries({
|
|
"agent:main:main": {
|
|
sessionId: "stale-context",
|
|
updatedAt: Date.now(),
|
|
modelProvider: "openai",
|
|
model: "gpt-5.4",
|
|
contextTokens: 1_000_000,
|
|
},
|
|
}),
|
|
);
|
|
|
|
await getStatusSummary();
|
|
|
|
expect(
|
|
vi
|
|
.mocked(statusSummaryRuntime.resolveContextTokensForModel)
|
|
.mock.calls.some((call) => call[0]?.contextTokensOverride === 1_000_000),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("uses bundled provider static catalogs for cold status context", async () => {
|
|
vi.mocked(statusSummaryRuntime.resolveConfiguredStatusModelRef).mockReturnValue({
|
|
provider: "google",
|
|
model: "gemini-3.1-pro-preview",
|
|
});
|
|
|
|
await getStatusSummary();
|
|
|
|
expect(
|
|
vi.mocked(statusSummaryRuntime.resolveContextTokensForModel).mock.calls[0]?.[0],
|
|
).toMatchObject({
|
|
provider: "google",
|
|
model: "gemini-3.1-pro-preview",
|
|
modelContextWindow: 1_048_576,
|
|
allowAsyncLoad: false,
|
|
});
|
|
});
|
|
|
|
it("uses context-only static metadata for nested provider-owned model refs", async () => {
|
|
vi.mocked(statusSummaryRuntime.resolveConfiguredStatusModelRef).mockReturnValue({
|
|
provider: "google-gemini-cli",
|
|
model: "google/gemini-3.1-pro-preview",
|
|
});
|
|
statusSummaryMocks.resolveProviderStaticModel.mockResolvedValueOnce({
|
|
contextWindow: 1_048_576,
|
|
});
|
|
|
|
await getStatusSummary();
|
|
|
|
expect(statusSummaryMocks.resolveProviderStaticModel).toHaveBeenCalledWith({
|
|
provider: "google-gemini-cli",
|
|
modelId: "google/gemini-3.1-pro-preview",
|
|
});
|
|
expect(
|
|
vi.mocked(statusSummaryRuntime.resolveContextTokensForModel).mock.calls[0]?.[0],
|
|
).toMatchObject({
|
|
provider: "google-gemini-cli",
|
|
model: "google/gemini-3.1-pro-preview",
|
|
modelContextWindow: 1_048_576,
|
|
allowAsyncLoad: false,
|
|
});
|
|
});
|
|
|
|
it("keeps status available when static catalog lookup fails", async () => {
|
|
vi.mocked(statusSummaryRuntime.resolveConfiguredStatusModelRef).mockReturnValue({
|
|
provider: "broken-provider",
|
|
model: "broken-model",
|
|
});
|
|
statusSummaryMocks.resolveProviderStaticModel.mockRejectedValueOnce(
|
|
new Error("static catalog unavailable"),
|
|
);
|
|
|
|
await expect(getStatusSummary()).resolves.toMatchObject({
|
|
sessions: {
|
|
defaults: {
|
|
model: "broken-model",
|
|
contextTokens: 200_000,
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it("includes the selected agent runtime on recent sessions", async () => {
|
|
vi.mocked(statusSummaryRuntime.resolveSessionRuntimeLabel).mockReturnValue("OpenAI Codex");
|
|
statusSummaryMocks.listSessionEntries.mockReturnValue(
|
|
toSessionEntrySummaries({
|
|
"agent:main:main": {
|
|
sessionId: "session-1",
|
|
updatedAt: Date.now(),
|
|
},
|
|
}),
|
|
);
|
|
|
|
const summary = await getStatusSummary();
|
|
|
|
expect(summary.sessions.recent[0]?.runtime).toBe("OpenAI Codex");
|
|
});
|
|
|
|
it("hydrates only recent session rows while preserving total counts", async () => {
|
|
const store = Object.fromEntries(
|
|
Array.from({ length: 12 }, (_, index) => {
|
|
const number = index + 1;
|
|
return [
|
|
`agent:main:session-${number}`,
|
|
{
|
|
sessionId: `session-${number}`,
|
|
updatedAt: number,
|
|
},
|
|
];
|
|
}),
|
|
);
|
|
statusSummaryMocks.listSessionEntries.mockReturnValue(toSessionEntrySummaries(store));
|
|
|
|
const summary = await getStatusSummary();
|
|
|
|
expect(summary.sessions.count).toBe(12);
|
|
expect(summary.sessions.byAgent[0]?.count).toBe(12);
|
|
expect(summary.sessions.recent.map((session) => session.key)).toEqual([
|
|
"agent:main:session-12",
|
|
"agent:main:session-11",
|
|
"agent:main:session-10",
|
|
"agent:main:session-9",
|
|
"agent:main:session-8",
|
|
"agent:main:session-7",
|
|
"agent:main:session-6",
|
|
"agent:main:session-5",
|
|
"agent:main:session-4",
|
|
"agent:main:session-3",
|
|
]);
|
|
expect(summary.sessions.byAgent[0]?.recent.map((session) => session.key)).toEqual(
|
|
summary.sessions.recent.map((session) => session.key),
|
|
);
|
|
|
|
const hydratedKeys = vi
|
|
.mocked(statusSummaryRuntime.resolveSessionRuntimeLabel)
|
|
.mock.calls.map(([params]) => params.sessionKey);
|
|
expect(hydratedKeys).not.toContain("agent:main:session-1");
|
|
expect(hydratedKeys).not.toContain("agent:main:session-2");
|
|
});
|
|
|
|
it("preserves store order for tied recent session timestamps", async () => {
|
|
const store = Object.fromEntries(
|
|
Array.from({ length: 11 }, (_, index) => {
|
|
const number = index + 1;
|
|
return [
|
|
`agent:main:session-${number}`,
|
|
{
|
|
sessionId: `session-${number}`,
|
|
updatedAt: 1,
|
|
},
|
|
];
|
|
}),
|
|
);
|
|
statusSummaryMocks.listSessionEntries.mockReturnValue(toSessionEntrySummaries(store));
|
|
|
|
const summary = await getStatusSummary();
|
|
|
|
expect(summary.sessions.recent.map((session) => session.key)).toEqual([
|
|
"agent:main:session-1",
|
|
"agent:main:session-2",
|
|
"agent:main:session-3",
|
|
"agent:main:session-4",
|
|
"agent:main:session-5",
|
|
"agent:main:session-6",
|
|
"agent:main:session-7",
|
|
"agent:main:session-8",
|
|
"agent:main:session-9",
|
|
"agent:main:session-10",
|
|
]);
|
|
expect(summary.sessions.byAgent[0]?.recent.map((session) => session.key)).toEqual(
|
|
summary.sessions.recent.map((session) => session.key),
|
|
);
|
|
});
|
|
|
|
it("passes agent scope when listing configured agent session stores", async () => {
|
|
vi.mocked(listGatewayAgentsBasic).mockReturnValue({
|
|
defaultId: "main",
|
|
mainKey: "main",
|
|
scope: "per-sender",
|
|
agents: [{ id: "main" }, { id: "ops" }],
|
|
});
|
|
vi.mocked(resolveStorePath).mockImplementation((_store, opts) => {
|
|
return `/tmp/${opts?.agentId ?? "main"}/sessions.json`;
|
|
});
|
|
statusSummaryMocks.listSessionEntries.mockImplementation((scope) =>
|
|
scope?.agentId === "ops"
|
|
? toSessionEntrySummaries({
|
|
main: { sessionId: "ops-session", updatedAt: 2 },
|
|
})
|
|
: toSessionEntrySummaries({
|
|
main: { sessionId: "main-session", updatedAt: 1 },
|
|
}),
|
|
);
|
|
|
|
const summary = await getStatusSummary({ includeChannelSummary: false });
|
|
|
|
expect(statusSummaryMocks.listSessionEntries).toHaveBeenCalledWith({
|
|
agentId: "main",
|
|
storePath: "/tmp/main/sessions.json",
|
|
});
|
|
expect(statusSummaryMocks.listSessionEntries).toHaveBeenCalledWith({
|
|
agentId: "ops",
|
|
storePath: "/tmp/ops/sessions.json",
|
|
});
|
|
expect(summary.sessions.count).toBe(2);
|
|
expect(summary.sessions.byAgent.map((agent) => [agent.agentId, agent.count])).toEqual([
|
|
["main", 1],
|
|
["ops", 1],
|
|
]);
|
|
});
|
|
|
|
it("aggregates shared file session stores only once", async () => {
|
|
vi.mocked(listGatewayAgentsBasic).mockReturnValue({
|
|
defaultId: "main",
|
|
mainKey: "main",
|
|
scope: "per-sender",
|
|
agents: [{ id: "main" }, { id: "ops" }],
|
|
});
|
|
vi.mocked(resolveStorePath).mockReturnValue("/tmp/shared-sessions.json");
|
|
statusSummaryMocks.listSessionEntries.mockReturnValue(
|
|
toSessionEntrySummaries({
|
|
main: { sessionId: "shared-session", updatedAt: 1 },
|
|
}),
|
|
);
|
|
|
|
const summary = await getStatusSummary({ includeChannelSummary: false });
|
|
|
|
expect(summary.sessions.count).toBe(1);
|
|
expect(summary.sessions.byAgent.map((agent) => [agent.agentId, agent.count])).toEqual([
|
|
["main", 1],
|
|
["ops", 1],
|
|
]);
|
|
expect(statusSummaryMocks.listSessionEntries).toHaveBeenCalledWith({
|
|
storePath: "/tmp/shared-sessions.json",
|
|
});
|
|
});
|
|
|
|
it("includes configured and selected model labels for pinned sessions", async () => {
|
|
vi.mocked(statusSummaryRuntime.resolveConfiguredStatusModelRef).mockReturnValue({
|
|
provider: "zhipu",
|
|
model: "glm-4.5-air",
|
|
});
|
|
vi.mocked(statusSummaryRuntime.resolveSessionModelRef).mockReturnValue({
|
|
provider: "deepseek",
|
|
model: "deepseek-v4-flash",
|
|
});
|
|
statusSummaryMocks.listSessionEntries.mockReturnValue(
|
|
toSessionEntrySummaries({
|
|
"agent:main:main": {
|
|
sessionId: "session-1",
|
|
updatedAt: Date.now(),
|
|
providerOverride: "deepseek",
|
|
modelOverride: "deepseek-v4-flash",
|
|
modelOverrideSource: "user",
|
|
},
|
|
}),
|
|
);
|
|
|
|
const summary = await getStatusSummary();
|
|
|
|
expect(summary.sessions.recent[0]?.configuredModel).toBe("zhipu/glm-4.5-air");
|
|
expect(summary.sessions.recent[0]?.selectedModel).toBe("deepseek/deepseek-v4-flash");
|
|
expect(summary.sessions.recent[0]?.modelSelectionReason).toBe("session override");
|
|
});
|
|
|
|
it("does not mark runtime-only model snapshots as pinned session selections", async () => {
|
|
vi.mocked(statusSummaryRuntime.resolveConfiguredStatusModelRef).mockReturnValue({
|
|
provider: "zhipu",
|
|
model: "glm-4.5-air",
|
|
});
|
|
vi.mocked(statusSummaryRuntime.resolveSessionModelRef).mockReturnValue({
|
|
provider: "deepseek",
|
|
model: "deepseek-v4-flash",
|
|
});
|
|
statusSummaryMocks.listSessionEntries.mockReturnValue(
|
|
toSessionEntrySummaries({
|
|
"agent:main:main": {
|
|
sessionId: "session-1",
|
|
updatedAt: Date.now(),
|
|
modelProvider: "deepseek",
|
|
model: "deepseek-v4-flash",
|
|
},
|
|
}),
|
|
);
|
|
|
|
const summary = await getStatusSummary();
|
|
|
|
expect(summary.sessions.recent[0]?.configuredModel).toBe("zhipu/glm-4.5-air");
|
|
expect(summary.sessions.recent[0]?.selectedModel).toBe("deepseek/deepseek-v4-flash");
|
|
expect(summary.sessions.recent[0]?.modelSelectionReason).toBeNull();
|
|
});
|
|
|
|
it("does not mark auto fallback model overrides as pinned session selections", async () => {
|
|
vi.mocked(statusSummaryRuntime.resolveConfiguredStatusModelRef).mockReturnValue({
|
|
provider: "zhipu",
|
|
model: "glm-4.5-air",
|
|
});
|
|
vi.mocked(statusSummaryRuntime.resolveSessionModelRef).mockReturnValue({
|
|
provider: "deepseek",
|
|
model: "deepseek-v4-flash",
|
|
});
|
|
statusSummaryMocks.listSessionEntries.mockReturnValue(
|
|
toSessionEntrySummaries({
|
|
"agent:main:main": {
|
|
sessionId: "session-1",
|
|
updatedAt: Date.now(),
|
|
providerOverride: "deepseek",
|
|
modelOverride: "deepseek-v4-flash",
|
|
modelOverrideSource: "auto",
|
|
modelOverrideFallbackOriginProvider: "zhipu",
|
|
modelOverrideFallbackOriginModel: "glm-4.5-air",
|
|
},
|
|
}),
|
|
);
|
|
|
|
const summary = await getStatusSummary();
|
|
|
|
expect(summary.sessions.recent[0]?.configuredModel).toBe("zhipu/glm-4.5-air");
|
|
expect(summary.sessions.recent[0]?.selectedModel).toBe("deepseek/deepseek-v4-flash");
|
|
expect(summary.sessions.recent[0]?.modelSelectionReason).toBeNull();
|
|
});
|
|
|
|
it("does not mark runtime-equivalent provider aliases as pinned mismatches", async () => {
|
|
vi.mocked(statusSummaryRuntime.resolveConfiguredStatusModelRef).mockReturnValue({
|
|
provider: "openai",
|
|
model: "gpt-5.5-codex",
|
|
});
|
|
vi.mocked(statusSummaryRuntime.resolveSessionModelRef).mockReturnValue({
|
|
provider: "openai",
|
|
model: "gpt-5.5-codex",
|
|
});
|
|
statusSummaryMocks.listSessionEntries.mockReturnValue(
|
|
toSessionEntrySummaries({
|
|
"agent:main:main": {
|
|
sessionId: "session-1",
|
|
updatedAt: Date.now(),
|
|
providerOverride: "openai",
|
|
modelOverride: "gpt-5.5-codex",
|
|
modelOverrideSource: "user",
|
|
},
|
|
}),
|
|
);
|
|
|
|
const summary = await getStatusSummary();
|
|
|
|
expect(summary.sessions.recent[0]?.configuredModel).toBe("openai/gpt-5.5-codex");
|
|
expect(summary.sessions.recent[0]?.selectedModel).toBe("openai/gpt-5.5-codex");
|
|
expect(summary.sessions.recent[0]?.modelSelectionReason).toBeNull();
|
|
});
|
|
});
|