openclaw/src/agents/openclaw-tools.session-stat...

1393 lines
42 KiB
TypeScript

import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { SessionEntry } from "../config/sessions.js";
import { resolvePreferredSessionKeyForSessionIdMatches } from "../sessions/session-id-resolution.js";
import type { TaskRecord } from "../tasks/task-registry.types.js";
import { buildTaskStatusSnapshot } from "../tasks/task-status.js";
const loadSessionStoreMock = vi.fn();
const updateSessionStoreMock = vi.fn();
const callGatewayMock = vi.fn();
const loadCombinedSessionStoreForGatewayMock = vi.fn();
const buildStatusMessageMock = vi.hoisted(() => vi.fn(() => "OpenClaw\n🧠 Model: GPT-5.4"));
const resolveQueueSettingsMock = vi.hoisted(() => vi.fn(() => ({ mode: "interrupt" })));
const listTasksForRelatedSessionKeyForOwnerMock = vi.hoisted(() =>
vi.fn(
(_: { relatedSessionKey: string; callerOwnerKey: string }) =>
[] as Array<Record<string, unknown>>,
),
);
const resolveEnvApiKeyMock = vi.hoisted(() =>
vi.fn((_provider?: string, _env?: NodeJS.ProcessEnv) => null),
);
const resolveUsableCustomProviderApiKeyMock = vi.hoisted(() =>
vi.fn((_params?: { provider?: string }) => null as { apiKey: string; source: string } | null),
);
const createMockConfig = () => ({
session: { mainKey: "main", scope: "per-sender" },
agents: {
defaults: {
model: { primary: "openai/gpt-5.4" },
models: {},
},
},
tools: {
agentToAgent: { enabled: false },
},
});
let mockConfig: Record<string, unknown> = createMockConfig();
const TASK_STATUS_SNAPSHOT_NOW = 1_000_000_000_000;
function createScopedSessionStores() {
return new Map<string, Record<string, unknown>>([
[
"/tmp/main/sessions.json",
{
"agent:main:main": { sessionId: "s-main", updatedAt: 10 },
},
],
[
"/tmp/support/sessions.json",
{
main: { sessionId: "s-support", updatedAt: 20 },
},
],
]);
}
function installScopedSessionStores(syncUpdates = false) {
const stores = createScopedSessionStores();
loadSessionStoreMock.mockClear();
updateSessionStoreMock.mockClear();
callGatewayMock.mockClear();
loadCombinedSessionStoreForGatewayMock.mockClear();
loadSessionStoreMock.mockImplementation((storePath: string) => stores.get(storePath) ?? {});
loadCombinedSessionStoreForGatewayMock.mockReturnValue({
storePath: "(multiple)",
store: Object.fromEntries([...stores.values()].flatMap((store) => Object.entries(store))),
});
if (syncUpdates) {
updateSessionStoreMock.mockImplementation(
(storePath: string, store: Record<string, unknown>) => {
if (storePath) {
stores.set(storePath, store);
}
},
);
}
return stores;
}
async function createSessionsModuleMock() {
const actual =
await vi.importActual<typeof import("../config/sessions.js")>("../config/sessions.js");
return {
...actual,
loadSessionStore: (storePath: string) => loadSessionStoreMock(storePath),
updateSessionStore: async (
storePath: string,
mutator: (store: Record<string, unknown>) => Promise<void> | void,
) => {
const store = loadSessionStoreMock(storePath) as Record<string, unknown>;
await mutator(store);
updateSessionStoreMock(storePath, store);
return store;
},
resolveStorePath: (_store: string | undefined, opts?: { agentId?: string }) =>
opts?.agentId === "support" ? "/tmp/support/sessions.json" : "/tmp/main/sessions.json",
};
}
function createGatewayCallModuleMock() {
return {
callGateway: (opts: unknown) => callGatewayMock(opts),
};
}
async function createGatewaySessionUtilsModuleMock() {
const actual = await vi.importActual<typeof import("../gateway/session-utils.js")>(
"../gateway/session-utils.js",
);
return {
...actual,
loadCombinedSessionStoreForGateway: (cfg: unknown) =>
loadCombinedSessionStoreForGatewayMock(cfg),
};
}
async function createConfigModuleMock() {
const actual = await vi.importActual<typeof import("../config/config.js")>("../config/config.js");
return {
...actual,
loadConfig: () => mockConfig,
};
}
function createModelCatalogModuleMock() {
return {
loadModelCatalog: async () => [
{
provider: "anthropic",
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
contextWindow: 200000,
},
{
provider: "openai",
id: "gpt-5.4",
name: "GPT-5.4",
contextWindow: 400000,
},
],
};
}
function createAuthProfilesModuleMock() {
return {
ensureAuthProfileStore: () => ({ profiles: {} }),
resolveAuthProfileDisplayLabel: () => undefined,
resolveAuthProfileOrder: () => [],
};
}
function createModelAuthModuleMock() {
return {
resolveEnvApiKey: resolveEnvApiKeyMock,
resolveUsableCustomProviderApiKey: resolveUsableCustomProviderApiKeyMock,
resolveModelAuthMode: () => "api-key",
};
}
function createProviderUsageModuleMock() {
return {
resolveUsageProviderId: () => undefined,
loadProviderUsageSummary: async () => ({
updatedAt: Date.now(),
providers: [],
}),
formatUsageSummaryLine: () => null,
};
}
vi.mock("../config/sessions.js", createSessionsModuleMock);
vi.mock("../gateway/call.js", createGatewayCallModuleMock);
vi.mock("../gateway/session-utils.js", createGatewaySessionUtilsModuleMock);
vi.mock("../config/config.js", createConfigModuleMock);
vi.mock("../agents/model-catalog.js", createModelCatalogModuleMock);
vi.mock("../agents/auth-profiles.js", createAuthProfilesModuleMock);
vi.mock("../agents/model-auth.js", createModelAuthModuleMock);
vi.mock("../infra/provider-usage.js", createProviderUsageModuleMock);
vi.mock("../auto-reply/group-activation.js", () => ({
normalizeGroupActivation: (value: unknown) => value ?? "always",
}));
vi.mock("../auto-reply/reply/queue.js", () => ({
getFollowupQueueDepth: () => 0,
resolveQueueSettings: resolveQueueSettingsMock,
}));
vi.mock("../auto-reply/status.js", () => ({
buildStatusMessage: buildStatusMessageMock,
}));
vi.mock("../tasks/task-owner-access.js", () => ({
listTasksForRelatedSessionKeyForOwner: (params: {
relatedSessionKey: string;
callerOwnerKey: string;
}) => listTasksForRelatedSessionKeyForOwnerMock(params),
buildTaskStatusSnapshotForRelatedSessionKeyForOwner: (params: {
relatedSessionKey: string;
callerOwnerKey: string;
}) =>
buildTaskStatusSnapshot(listTasksForRelatedSessionKeyForOwnerMock(params) as TaskRecord[], {
now: TASK_STATUS_SNAPSHOT_NOW,
}),
}));
let createSessionStatusTool: typeof import("./tools/session-status-tool.js").createSessionStatusTool;
beforeAll(async () => {
({ createSessionStatusTool } = await import("./tools/session-status-tool.js"));
});
function resetSessionStore(store: Record<string, SessionEntry>) {
buildStatusMessageMock.mockClear();
resolveQueueSettingsMock.mockClear();
resolveQueueSettingsMock.mockReturnValue({ mode: "interrupt" });
resolveEnvApiKeyMock.mockReset();
resolveEnvApiKeyMock.mockReturnValue(null);
resolveUsableCustomProviderApiKeyMock.mockReset();
resolveUsableCustomProviderApiKeyMock.mockReturnValue(null);
loadSessionStoreMock.mockClear();
updateSessionStoreMock.mockClear();
callGatewayMock.mockClear();
loadCombinedSessionStoreForGatewayMock.mockClear();
listTasksForRelatedSessionKeyForOwnerMock.mockClear();
listTasksForRelatedSessionKeyForOwnerMock.mockReturnValue([]);
loadSessionStoreMock.mockReturnValue(store);
loadCombinedSessionStoreForGatewayMock.mockReturnValue({
storePath: "(multiple)",
store,
});
callGatewayMock.mockImplementation(async (opts: unknown) => {
const request = opts as { method?: string; params?: Record<string, unknown> };
if (request.method === "sessions.resolve") {
const key = typeof request.params?.key === "string" ? request.params.key.trim() : "";
if (key && store[key]) {
return { key };
}
const sessionId =
typeof request.params?.sessionId === "string" ? request.params.sessionId.trim() : "";
if (!sessionId) {
return {};
}
const spawnedBy =
typeof request.params?.spawnedBy === "string" ? request.params.spawnedBy.trim() : "";
const matches = Object.entries(store).filter((entry): entry is [string, SessionEntry] => {
return (
entry[1].sessionId === sessionId &&
(!spawnedBy ||
entry[1].spawnedBy === spawnedBy ||
entry[1].parentSessionKey === spawnedBy)
);
});
return { key: resolvePreferredSessionKeyForSessionIdMatches(matches, sessionId) };
}
if (request.method === "sessions.list") {
return { sessions: [] };
}
return {};
});
mockConfig = createMockConfig();
}
function installSandboxedSessionStatusConfig() {
mockConfig = {
session: { mainKey: "main", scope: "per-sender" },
tools: {
sessions: { visibility: "all" },
agentToAgent: { enabled: true, allow: ["*"] },
},
agents: {
defaults: {
model: { primary: "openai/gpt-5.4" },
models: {},
sandbox: { sessionToolsVisibility: "spawned" },
},
},
};
}
function mockSpawnedSessionList(
resolveSessions: (spawnedBy: string | undefined) => Array<Record<string, unknown>>,
) {
callGatewayMock.mockImplementation(async (opts: unknown) => {
const request = opts as { method?: string; params?: Record<string, unknown> };
if (request.method === "sessions.list") {
return { sessions: resolveSessions(request.params?.spawnedBy as string | undefined) };
}
return {};
});
}
function expectSpawnedSessionLookupCalls(spawnedBy: string) {
const expectedCall = {
method: "sessions.list",
params: {
includeGlobal: false,
includeUnknown: false,
spawnedBy,
},
};
expect(callGatewayMock).toHaveBeenCalledTimes(2);
expect(callGatewayMock).toHaveBeenNthCalledWith(1, expectedCall);
expect(callGatewayMock).toHaveBeenNthCalledWith(2, expectedCall);
}
function getSessionStatusTool(agentSessionKey = "main", options?: { sandboxed?: boolean }) {
const tool = createSessionStatusTool({
agentSessionKey,
sandboxed: options?.sandboxed,
config: mockConfig as never,
});
expect(tool.name).toBe("session_status");
return tool;
}
describe("session_status tool", () => {
beforeEach(() => {
buildStatusMessageMock.mockClear();
});
it("returns a status card for the current session", async () => {
resetSessionStore({
main: {
sessionId: "s1",
updatedAt: 10,
},
});
const tool = getSessionStatusTool();
const result = await tool.execute("call1", {});
const details = result.details as { ok?: boolean; statusText?: string };
expect(details.ok).toBe(true);
expect(details.statusText).toContain("OpenClaw");
expect(details.statusText).toContain("🧠 Model:");
expect(details.statusText).not.toContain("OAuth/token status");
});
it("errors for unknown session keys", async () => {
resetSessionStore({
main: { sessionId: "s1", updatedAt: 10 },
});
const tool = getSessionStatusTool();
await expect(tool.execute("call2", { sessionKey: "nope" })).rejects.toThrow(
"Unknown sessionId",
);
expect(updateSessionStoreMock).not.toHaveBeenCalled();
});
it("resolves sessionKey=current to the requester session", async () => {
resetSessionStore({
main: {
sessionId: "s1",
updatedAt: 10,
},
});
const tool = getSessionStatusTool();
const result = await tool.execute("call-current", { sessionKey: "current" });
const details = result.details as { ok?: boolean; sessionKey?: string };
expect(details.ok).toBe(true);
expect(details.sessionKey).toBe("main");
});
it("resolves sessionKey=current to the requester agent session", async () => {
installScopedSessionStores();
const tool = getSessionStatusTool("agent:support:main");
// "current" resolves to the support agent's own session via the "main" alias.
const result = await tool.execute("call-current-child", { sessionKey: "current" });
const details = result.details as { ok?: boolean; sessionKey?: string };
expect(details.ok).toBe(true);
expect(details.sessionKey).toBe("main");
});
it("prefers a literal current session key in session_status", async () => {
resetSessionStore({
main: {
sessionId: "s-main",
updatedAt: 10,
},
"agent:main:current": {
sessionId: "s-current",
updatedAt: 20,
},
});
const tool = getSessionStatusTool();
const result = await tool.execute("call-current-literal-key", { sessionKey: "current" });
const details = result.details as { ok?: boolean; sessionKey?: string };
expect(details.ok).toBe(true);
expect(details.sessionKey).toBe("agent:main:current");
});
it("includes background task context in session_status output", async () => {
resetSessionStore({
"agent:main:main": {
sessionId: "sess-main",
updatedAt: Date.now(),
},
});
listTasksForRelatedSessionKeyForOwnerMock.mockReturnValue([
{
taskId: "task-1",
runtime: "acp",
requesterSessionKey: "agent:main:main",
task: "Summarize inbox backlog",
status: "running",
deliveryStatus: "pending",
notifyPolicy: "done_only",
createdAt: Date.now() - 5_000,
progressSummary: "Indexing the latest threads",
},
]);
const tool = createSessionStatusTool({ agentSessionKey: "agent:main:main" });
const result = await tool.execute("tc-1", { sessionKey: "agent:main:main" });
const firstContent = result.content?.[0];
const text = (firstContent as { text: string } | undefined)?.text ?? "";
expect(text).toContain("📌 Tasks: 1 active");
expect(text).toContain("acp");
expect(text).toContain("Summarize inbox backlog");
expect(text).toContain("Indexing the latest threads");
});
it("hides stale completed task rows from session_status output", async () => {
resetSessionStore({
"agent:main:main": {
sessionId: "sess-main",
updatedAt: Date.now(),
},
});
listTasksForRelatedSessionKeyForOwnerMock.mockReturnValue([
{
taskId: "task-stale",
runtime: "cron",
requesterSessionKey: "agent:main:main",
task: "stale completed task",
status: "succeeded",
deliveryStatus: "delivered",
notifyPolicy: "done_only",
createdAt: Date.now() - 15 * 60_000,
terminalSummary: "finished long ago",
},
{
taskId: "task-live",
runtime: "subagent",
requesterSessionKey: "agent:main:main",
task: "live task",
status: "running",
deliveryStatus: "pending",
notifyPolicy: "done_only",
createdAt: Date.now() - 5_000,
progressSummary: "still working",
},
]);
const tool = createSessionStatusTool({ agentSessionKey: "agent:main:main" });
const result = await tool.execute("tc-stale", { sessionKey: "agent:main:main" });
const firstContent = result.content?.[0];
const text = (firstContent as { text: string } | undefined)?.text ?? "";
expect(text).toContain("📌 Tasks: 1 active");
expect(text).toContain("live task");
expect(text).not.toContain("stale completed task");
expect(text).not.toContain("finished long ago");
});
it("shows recent failure context in session_status output when no task is active", async () => {
resetSessionStore({
"agent:main:main": {
sessionId: "sess-main",
updatedAt: Date.now(),
},
});
listTasksForRelatedSessionKeyForOwnerMock.mockReturnValue([
{
taskId: "task-failed",
runtime: "cron",
requesterSessionKey: "agent:main:main",
task: "failing task",
status: "failed",
deliveryStatus: "pending",
notifyPolicy: "done_only",
createdAt: Date.now() - 5_000,
error: "permission denied",
},
]);
const tool = createSessionStatusTool({ agentSessionKey: "agent:main:main" });
const result = await tool.execute("tc-failed", { sessionKey: "agent:main:main" });
const firstContent = result.content?.[0];
const text = (firstContent as { text: string } | undefined)?.text ?? "";
expect(text).toContain("📌 Tasks: 1 recent failure");
expect(text).toContain("failing task");
expect(text).toContain("permission denied");
});
it("truncates long task titles and details in session_status output", async () => {
resetSessionStore({
"agent:main:main": {
sessionId: "sess-main",
updatedAt: Date.now(),
},
});
listTasksForRelatedSessionKeyForOwnerMock.mockReturnValue([
{
taskId: "task-long",
runtime: "subagent",
requesterSessionKey: "agent:main:main",
task: "This is a deliberately long task prompt that should never be emitted in full by session_status because it can include internal instructions and file paths that are not appropriate for user-visible task summaries.",
status: "running",
deliveryStatus: "pending",
notifyPolicy: "done_only",
createdAt: Date.now() - 5_000,
progressSummary:
"This progress detail is also intentionally long so the session_status tool proves it truncates verbose task context instead of dumping a long internal update into the tool response.",
},
]);
const tool = createSessionStatusTool({ agentSessionKey: "agent:main:main" });
const result = await tool.execute("tc-truncated", { sessionKey: "agent:main:main" });
const firstContent = result.content?.[0];
const text = (firstContent as { text: string } | undefined)?.text ?? "";
expect(text).toContain(
"This is a deliberately long task prompt that should never be emitted in full by…",
);
expect(text).toContain(
"This progress detail is also intentionally long so the session_status tool proves it truncates verbose task context ins…",
);
expect(text).not.toContain("internal instructions and file paths");
expect(text).not.toContain("dumping a long internal update");
});
it("prefers failure context over newer success context in session_status output", async () => {
resetSessionStore({
"agent:main:main": {
sessionId: "sess-main",
updatedAt: Date.now(),
},
});
listTasksForRelatedSessionKeyForOwnerMock.mockReturnValue([
{
taskId: "task-failed",
runtime: "cron",
requesterSessionKey: "agent:main:main",
task: "failing task",
status: "failed",
deliveryStatus: "pending",
notifyPolicy: "done_only",
createdAt: Date.now() - 60_000,
endedAt: Date.now() - 30_000,
error: "permission denied",
},
{
taskId: "task-succeeded",
runtime: "subagent",
requesterSessionKey: "agent:main:main",
task: "successful task",
status: "succeeded",
deliveryStatus: "delivered",
notifyPolicy: "done_only",
createdAt: Date.now() - 10_000,
endedAt: Date.now(),
terminalSummary: "all done",
},
]);
const tool = createSessionStatusTool({ agentSessionKey: "agent:main:main" });
const result = await tool.execute("tc-failed-priority", { sessionKey: "agent:main:main" });
const firstContent = result.content?.[0];
const text = (firstContent as { text: string } | undefined)?.text ?? "";
expect(text).toContain("📌 Tasks: 1 recent failure");
expect(text).toContain("failing task");
expect(text).toContain("permission denied");
expect(text).not.toContain("successful task");
expect(text).not.toContain("all done");
});
it("resolves a literal current sessionId in session_status", async () => {
resetSessionStore({
main: {
sessionId: "s-main",
updatedAt: 10,
},
"agent:main:other": {
sessionId: "current",
updatedAt: 20,
},
});
mockConfig = {
session: { mainKey: "main", scope: "per-sender" },
tools: {
sessions: { visibility: "all" },
agentToAgent: { enabled: true, allow: ["*"] },
},
agents: {
defaults: {
model: { primary: "openai/gpt-5.4" },
models: {},
},
},
};
const tool = getSessionStatusTool();
const result = await tool.execute("call-current-literal-id", { sessionKey: "current" });
const details = result.details as { ok?: boolean; sessionKey?: string };
expect(details.ok).toBe(true);
expect(details.sessionKey).toBe("agent:main:other");
});
it("keeps sessionKey=current bound to the requester subagent session", async () => {
resetSessionStore({
"agent:main:main": {
sessionId: "s-parent",
updatedAt: 10,
},
"agent:main:subagent:child": {
sessionId: "s-child",
updatedAt: 20,
providerOverride: "openai",
modelOverride: "gpt-5.4",
},
});
const tool = getSessionStatusTool("agent:main:subagent:child");
const result = await tool.execute("call-current-subagent", {
sessionKey: "current",
model: "anthropic/claude-sonnet-4-6",
});
const details = result.details as { ok?: boolean; sessionKey?: string };
expect(details.ok).toBe(true);
expect(details.sessionKey).toBe("agent:main:subagent:child");
expect(updateSessionStoreMock).toHaveBeenCalledWith(
"/tmp/main/sessions.json",
expect.objectContaining({
"agent:main:subagent:child": expect.objectContaining({
modelOverride: "claude-sonnet-4-6",
}),
}),
);
});
it("uses the runtime session model as the selected card model when no override is set", async () => {
resetSessionStore({
main: {
sessionId: "runtime-model",
updatedAt: 10,
modelProvider: "anthropic",
model: "claude-opus-4-6",
},
});
const tool = getSessionStatusTool();
await tool.execute("call-runtime-model", {});
expect(buildStatusMessageMock).toHaveBeenCalledWith(
expect.objectContaining({
agent: expect.objectContaining({
model: expect.objectContaining({
primary: "anthropic/claude-opus-4-6",
}),
}),
}),
);
});
it("infers configured custom providers for runtime-only models in session_status", async () => {
resetSessionStore({
main: {
sessionId: "runtime-custom-provider",
updatedAt: 10,
model: "qwen-max",
},
});
mockConfig = {
session: { mainKey: "main", scope: "per-sender" },
agents: {
defaults: {
model: { primary: "openai/gpt-5.4" },
models: {},
},
},
models: {
providers: {
"qwen-dashscope": {
apiKey: "DASHSCOPE_API_KEY",
models: [{ id: "qwen-max" }],
},
},
},
tools: {
agentToAgent: { enabled: false },
},
};
resolveUsableCustomProviderApiKeyMock.mockImplementation((params) =>
params?.provider === "qwen-dashscope" ? { apiKey: "sk-test", source: "models.json" } : null,
);
const tool = getSessionStatusTool();
await tool.execute("call-runtime-custom-provider", {});
expect(buildStatusMessageMock).toHaveBeenCalledWith(
expect.objectContaining({
agent: expect.objectContaining({
model: expect.objectContaining({
primary: "qwen-dashscope/qwen-max",
}),
}),
modelAuth: "api-key (models.json)",
}),
);
});
it("preserves an unknown runtime provider in the selected status card model", async () => {
resetSessionStore({
main: {
sessionId: "legacy-runtime-model",
updatedAt: 10,
model: "legacy-runtime-model",
},
});
const tool = getSessionStatusTool();
await tool.execute("call-legacy-runtime-model", {});
expect(buildStatusMessageMock).toHaveBeenCalledWith(
expect.objectContaining({
agent: expect.objectContaining({
model: expect.objectContaining({
primary: "legacy-runtime-model",
}),
}),
sessionEntry: expect.objectContaining({
model: "legacy-runtime-model",
providerOverride: "",
}),
modelAuth: undefined,
}),
);
});
it("passes per-agent thinkingDefault through to the status card", async () => {
resetSessionStore({
"agent:kira:main": {
sessionId: "agent-thinking",
updatedAt: 10,
},
});
const savedConfig = mockConfig;
try {
mockConfig = {
session: { mainKey: "main", scope: "per-sender" },
agents: {
defaults: {
model: { primary: "openai/gpt-5.4" },
models: {},
},
list: [
{
id: "kira",
model: "openai/gpt-5.4",
thinkingDefault: "xhigh",
},
],
},
tools: {
agentToAgent: { enabled: false },
},
};
const tool = getSessionStatusTool("agent:kira:main");
await tool.execute("call-agent-thinking", {});
expect(buildStatusMessageMock).toHaveBeenCalledWith(
expect.objectContaining({
agentId: "kira",
agent: expect.objectContaining({
thinkingDefault: "xhigh",
}),
}),
);
} finally {
mockConfig = savedConfig;
}
});
it("falls back to origin.provider when resolving queue settings", async () => {
resetSessionStore({
main: {
sessionId: "status-origin-provider",
updatedAt: 10,
origin: { provider: "discord" },
},
});
const tool = getSessionStatusTool();
await tool.execute("call-origin-provider", {});
expect(resolveQueueSettingsMock).toHaveBeenCalledWith(
expect.objectContaining({
channel: "discord",
sessionEntry: expect.objectContaining({
origin: { provider: "discord" },
}),
}),
);
});
it("resolves sessionId inputs", async () => {
const sessionId = "sess-main";
resetSessionStore({
"agent:main:main": {
sessionId,
updatedAt: 10,
},
});
const tool = getSessionStatusTool();
const result = await tool.execute("call3", { sessionKey: sessionId });
const details = result.details as { ok?: boolean; sessionKey?: string };
expect(details.ok).toBe(true);
expect(details.sessionKey).toBe("agent:main:main");
});
it("resolves duplicate sessionId inputs deterministically", async () => {
resetSessionStore({
"agent:main:main": {
sessionId: "current",
updatedAt: 10,
},
"agent:main:other": {
sessionId: "run-dup",
updatedAt: 999,
},
"agent:main:acp:run-dup": {
sessionId: "run-dup",
updatedAt: 100,
},
});
mockConfig = {
session: { mainKey: "main", scope: "per-sender" },
tools: {
sessions: { visibility: "all" },
agentToAgent: { enabled: true, allow: ["*"] },
},
agents: {
defaults: {
model: { primary: "openai/gpt-5.4" },
models: {},
},
},
};
const tool = getSessionStatusTool();
const result = await tool.execute("call-dup", { sessionKey: "run-dup" });
const details = result.details as { ok?: boolean; sessionKey?: string };
expect(details.ok).toBe(true);
expect(details.sessionKey).toBe("agent:main:acp:run-dup");
});
it("uses non-standard session keys without sessionId resolution", async () => {
resetSessionStore({
"temp:slug-generator": {
sessionId: "sess-temp",
updatedAt: 10,
},
});
const tool = getSessionStatusTool();
const result = await tool.execute("call4", { sessionKey: "temp:slug-generator" });
const details = result.details as { ok?: boolean; sessionKey?: string };
expect(details.ok).toBe(true);
expect(details.sessionKey).toBe("temp:slug-generator");
});
it("blocks cross-agent session_status without agent-to-agent access", async () => {
resetSessionStore({
"agent:other:main": {
sessionId: "s2",
updatedAt: 10,
},
});
const tool = getSessionStatusTool("agent:main:main");
await expect(tool.execute("call5", { sessionKey: "agent:other:main" })).rejects.toThrow(
"Agent-to-agent status is disabled",
);
});
it("blocks unsandboxed same-agent session_status outside self visibility", async () => {
resetSessionStore({
"agent:main:main": {
sessionId: "s-parent",
updatedAt: 10,
providerOverride: "anthropic",
modelOverride: "claude-sonnet-4-6",
},
"agent:main:subagent:child": {
sessionId: "s-child",
updatedAt: 20,
},
});
mockConfig = {
session: { mainKey: "main", scope: "per-sender" },
tools: {
sessions: { visibility: "self" },
agentToAgent: { enabled: true, allow: ["*"] },
},
agents: {
defaults: {
model: { primary: "openai/gpt-5.4" },
models: {},
},
},
};
const tool = getSessionStatusTool("agent:main:subagent:child");
await expect(
tool.execute("call-self-visibility", {
sessionKey: "agent:main:main",
model: "default",
}),
).rejects.toThrow(
"Session status visibility is restricted to the current session (tools.sessions.visibility=self).",
);
expect(loadSessionStoreMock).not.toHaveBeenCalled();
expect(updateSessionStoreMock).not.toHaveBeenCalled();
});
it("blocks unsandboxed same-agent bare main session_status outside self visibility", async () => {
resetSessionStore({
"agent:main:main": {
sessionId: "s-parent",
updatedAt: 10,
providerOverride: "anthropic",
modelOverride: "claude-sonnet-4-6",
},
"agent:main:subagent:child": {
sessionId: "s-child",
updatedAt: 20,
},
});
mockConfig = {
session: { mainKey: "main", scope: "per-sender" },
tools: {
sessions: { visibility: "self" },
agentToAgent: { enabled: true, allow: ["*"] },
},
agents: {
defaults: {
model: { primary: "openai/gpt-5.4" },
models: {},
},
},
};
const tool = getSessionStatusTool("agent:main:subagent:child");
await expect(
tool.execute("call-self-visibility-bare-main", {
sessionKey: "main",
model: "default",
}),
).rejects.toThrow(
"Session status visibility is restricted to the current session (tools.sessions.visibility=self).",
);
expect(updateSessionStoreMock).not.toHaveBeenCalled();
});
it("blocks unsandboxed same-agent session_status outside tree visibility before mutation", async () => {
resetSessionStore({
"agent:main:main": {
sessionId: "s-parent",
updatedAt: 10,
providerOverride: "anthropic",
modelOverride: "claude-sonnet-4-6",
},
"agent:main:subagent:child": {
sessionId: "s-child",
updatedAt: 20,
},
});
mockConfig = {
session: { mainKey: "main", scope: "per-sender" },
tools: {
sessions: { visibility: "tree" },
agentToAgent: { enabled: true, allow: ["*"] },
},
agents: {
defaults: {
model: { primary: "openai/gpt-5.4" },
models: {},
},
},
};
mockSpawnedSessionList(() => []);
const tool = getSessionStatusTool("agent:main:subagent:child");
await expect(
tool.execute("call-tree-visibility", {
sessionKey: "agent:main:main",
model: "default",
}),
).rejects.toThrow(
"Session status visibility is restricted to the current session tree (tools.sessions.visibility=tree).",
);
expect(loadSessionStoreMock).not.toHaveBeenCalled();
expect(updateSessionStoreMock).not.toHaveBeenCalled();
expect(callGatewayMock).toHaveBeenCalledTimes(1);
expect(callGatewayMock).toHaveBeenCalledWith({
method: "sessions.list",
params: {
includeGlobal: false,
includeUnknown: false,
spawnedBy: "agent:main:subagent:child",
},
});
});
it("allows unsandboxed same-agent session_status under agent visibility", async () => {
resetSessionStore({
"agent:main:main": {
sessionId: "s-parent",
updatedAt: 10,
providerOverride: "anthropic",
modelOverride: "claude-sonnet-4-6",
},
"agent:main:subagent:child": {
sessionId: "s-child",
updatedAt: 20,
},
});
mockConfig = {
session: { mainKey: "main", scope: "per-sender" },
tools: {
sessions: { visibility: "agent" },
agentToAgent: { enabled: true, allow: ["*"] },
},
agents: {
defaults: {
model: { primary: "openai/gpt-5.4" },
models: {},
},
},
};
const tool = getSessionStatusTool("agent:main:subagent:child");
const result = await tool.execute("call-agent-visibility", {
sessionKey: "agent:main:main",
model: "default",
});
const details = result.details as { ok?: boolean; sessionKey?: string };
expect(details.ok).toBe(true);
expect(details.sessionKey).toBe("agent:main:main");
expect(updateSessionStoreMock).toHaveBeenCalled();
});
it("blocks unsandboxed sessionId session_status outside tree visibility before mutation", async () => {
resetSessionStore({
"agent:main:main": {
sessionId: "s-parent",
updatedAt: 10,
providerOverride: "anthropic",
modelOverride: "claude-sonnet-4-6",
},
"agent:main:subagent:child": {
sessionId: "s-child",
updatedAt: 20,
},
});
mockConfig = {
session: { mainKey: "main", scope: "per-sender" },
tools: {
sessions: { visibility: "tree" },
agentToAgent: { enabled: true, allow: ["*"] },
},
agents: {
defaults: {
model: { primary: "openai/gpt-5.4" },
models: {},
},
},
};
callGatewayMock.mockImplementation(async (opts: unknown) => {
const request = opts as { method?: string; params?: Record<string, unknown> };
if (request.method === "sessions.resolve") {
if (request.params?.sessionId === "s-parent") {
return { key: "agent:main:main" };
}
return {};
}
if (request.method === "sessions.list") {
return { sessions: [] };
}
return {};
});
const tool = getSessionStatusTool("agent:main:subagent:child");
await expect(
tool.execute("call-tree-session-id-visibility", {
sessionKey: "s-parent",
model: "default",
}),
).rejects.toThrow(
"Session status visibility is restricted to the current session tree (tools.sessions.visibility=tree).",
);
expect(updateSessionStoreMock).not.toHaveBeenCalled();
});
it("blocks sandboxed child session_status access outside its tree before store lookup", async () => {
resetSessionStore({
"agent:main:subagent:child": {
sessionId: "s-child",
updatedAt: 20,
},
"agent:main:main": {
sessionId: "s-parent",
updatedAt: 10,
},
});
installSandboxedSessionStatusConfig();
mockSpawnedSessionList(() => []);
const tool = getSessionStatusTool("agent:main:subagent:child", {
sandboxed: true,
});
const expectedError = "Session status visibility is restricted to the current session tree";
await expect(
tool.execute("call6", {
sessionKey: "agent:main:main",
model: "anthropic/claude-sonnet-4-6",
}),
).rejects.toThrow(expectedError);
await expect(
tool.execute("call7", {
sessionKey: "agent:main:subagent:missing",
}),
).rejects.toThrow(expectedError);
expect(loadSessionStoreMock).not.toHaveBeenCalled();
expect(updateSessionStoreMock).not.toHaveBeenCalled();
expectSpawnedSessionLookupCalls("agent:main:subagent:child");
});
it("blocks sandboxed child bare main session_status access outside its tree", async () => {
resetSessionStore({
"agent:main:subagent:child": {
sessionId: "s-child",
updatedAt: 20,
},
"agent:main:main": {
sessionId: "s-parent",
updatedAt: 10,
providerOverride: "anthropic",
modelOverride: "claude-sonnet-4-6",
},
});
installSandboxedSessionStatusConfig();
mockSpawnedSessionList(() => []);
const tool = getSessionStatusTool("agent:main:subagent:child", {
sandboxed: true,
});
const expectedError = "Session status visibility is restricted to the current session tree";
await expect(
tool.execute("call6-bare-main", {
sessionKey: "main",
model: "default",
}),
).rejects.toThrow(expectedError);
expect(updateSessionStoreMock).not.toHaveBeenCalled();
expect(callGatewayMock).toHaveBeenCalledTimes(1);
expect(callGatewayMock).toHaveBeenCalledWith({
method: "sessions.list",
params: {
includeGlobal: false,
includeUnknown: false,
spawnedBy: "agent:main:subagent:child",
},
});
});
it("blocks sandboxed child session_status sessionId access outside its tree before store lookup", async () => {
resetSessionStore({
"agent:main:subagent:child": {
sessionId: "s-child",
updatedAt: 20,
},
"agent:main:main": {
sessionId: "s-parent",
updatedAt: 10,
},
"agent:other:main": {
sessionId: "s-other",
updatedAt: 30,
},
});
installSandboxedSessionStatusConfig();
mockSpawnedSessionList(() => []);
const tool = getSessionStatusTool("agent:main:subagent:child", {
sandboxed: true,
});
const expectedError = "Session status visibility is restricted to the current session tree";
await expect(
tool.execute("call6-session-id", {
sessionKey: "s-other",
}),
).rejects.toThrow(expectedError);
expect(loadSessionStoreMock).toHaveBeenCalledTimes(1);
expect(loadSessionStoreMock).toHaveBeenCalledWith("/tmp/main/sessions.json");
expect(updateSessionStoreMock).not.toHaveBeenCalled();
expect(callGatewayMock).toHaveBeenCalledTimes(3);
expect(callGatewayMock.mock.calls).toContainEqual([
{
method: "sessions.resolve",
params: {
sessionId: "s-other",
spawnedBy: "agent:main:subagent:child",
includeGlobal: false,
includeUnknown: false,
},
},
]);
expect(callGatewayMock.mock.calls).toContainEqual([
{
method: "sessions.list",
params: {
includeGlobal: false,
includeUnknown: false,
spawnedBy: "agent:main:subagent:child",
},
},
]);
});
it("blocks sandboxed child session_status parent sessionId access outside its tree", async () => {
resetSessionStore({
"agent:main:subagent:child": {
sessionId: "s-child",
updatedAt: 20,
},
"agent:main:main": {
sessionId: "s-parent",
updatedAt: 10,
},
});
installSandboxedSessionStatusConfig();
mockSpawnedSessionList(() => []);
const tool = getSessionStatusTool("agent:main:subagent:child", {
sandboxed: true,
});
await expect(
tool.execute("call7-parent-session-id", {
sessionKey: "s-parent",
}),
).rejects.toThrow("Session status visibility is restricted to the current session tree");
expect(loadSessionStoreMock).toHaveBeenCalledTimes(1);
expect(loadSessionStoreMock).toHaveBeenCalledWith("/tmp/main/sessions.json");
expect(updateSessionStoreMock).not.toHaveBeenCalled();
expect(callGatewayMock).toHaveBeenCalledTimes(3);
expect(callGatewayMock.mock.calls).toContainEqual([
{
method: "sessions.resolve",
params: {
sessionId: "s-parent",
spawnedBy: "agent:main:subagent:child",
includeGlobal: false,
includeUnknown: false,
},
},
]);
expect(callGatewayMock.mock.calls).toContainEqual([
{
method: "sessions.list",
params: {
includeGlobal: false,
includeUnknown: false,
spawnedBy: "agent:main:subagent:child",
},
},
]);
});
it("keeps legacy main requester keys for sandboxed session tree checks", async () => {
resetSessionStore({
"agent:main:main": {
sessionId: "s-main",
updatedAt: 10,
},
"agent:main:subagent:child": {
sessionId: "s-child",
updatedAt: 20,
},
});
installSandboxedSessionStatusConfig();
mockSpawnedSessionList((spawnedBy) =>
spawnedBy === "main" ? [{ key: "agent:main:subagent:child" }] : [],
);
const tool = getSessionStatusTool("main", {
sandboxed: true,
});
const mainResult = await tool.execute("call8", {});
const mainDetails = mainResult.details as { ok?: boolean; sessionKey?: string };
expect(mainDetails.ok).toBe(true);
expect(mainDetails.sessionKey).toBe("agent:main:main");
const childResult = await tool.execute("call9", {
sessionKey: "agent:main:subagent:child",
});
const childDetails = childResult.details as { ok?: boolean; sessionKey?: string };
expect(childDetails.ok).toBe(true);
expect(childDetails.sessionKey).toBe("agent:main:subagent:child");
expectSpawnedSessionLookupCalls("main");
});
it("scopes bare session keys to the requester agent", async () => {
installScopedSessionStores(true);
const tool = getSessionStatusTool("agent:support:main");
const result = await tool.execute("call6", { sessionKey: "main" });
const details = result.details as { ok?: boolean; sessionKey?: string };
expect(details.ok).toBe(true);
expect(details.sessionKey).toBe("main");
});
it("resets per-session model override via model=default", async () => {
resetSessionStore({
main: {
sessionId: "s1",
updatedAt: 10,
providerOverride: "anthropic",
modelOverride: "claude-sonnet-4-6",
authProfileOverride: "p1",
},
});
const tool = getSessionStatusTool();
await tool.execute("call3", { model: "default" });
expect(updateSessionStoreMock).toHaveBeenCalled();
const [, savedStore] = updateSessionStoreMock.mock.calls.at(-1) as [
string,
Record<string, unknown>,
];
const saved = savedStore.main as Record<string, unknown>;
expect(saved.providerOverride).toBeUndefined();
expect(saved.modelOverride).toBeUndefined();
expect(saved.authProfileOverride).toBeUndefined();
});
});