mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-27 08:28:14 +00:00
test: stabilize trigger handling and hook e2e tests
This commit is contained in:
@@ -4,14 +4,12 @@ import path from "node:path";
|
|||||||
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
import { registerLogTransport, resetLogger, setLoggerOverride } from "../logging/logger.js";
|
|
||||||
import { redactIdentifier } from "../logging/redact-identifier.js";
|
import { redactIdentifier } from "../logging/redact-identifier.js";
|
||||||
import type { AuthProfileFailureReason } from "./auth-profiles.js";
|
import type { AuthProfileFailureReason } from "./auth-profiles.js";
|
||||||
import type { EmbeddedRunAttemptResult } from "./pi-embedded-runner/run/types.js";
|
import type { EmbeddedRunAttemptResult } from "./pi-embedded-runner/run/types.js";
|
||||||
|
|
||||||
const runEmbeddedAttemptMock = vi.fn<(params: unknown) => Promise<EmbeddedRunAttemptResult>>();
|
const runEmbeddedAttemptMock = vi.fn<(params: unknown) => Promise<EmbeddedRunAttemptResult>>();
|
||||||
const resolveCopilotApiTokenMock = vi.fn();
|
const resolveCopilotApiTokenMock = vi.fn();
|
||||||
const COPILOT_TOKEN_URL = "https://api.github.com/copilot_internal/v2/token";
|
|
||||||
const { computeBackoffMock, sleepWithAbortMock } = vi.hoisted(() => ({
|
const { computeBackoffMock, sleepWithAbortMock } = vi.hoisted(() => ({
|
||||||
computeBackoffMock: vi.fn(
|
computeBackoffMock: vi.fn(
|
||||||
(
|
(
|
||||||
@@ -22,63 +20,121 @@ const { computeBackoffMock, sleepWithAbortMock } = vi.hoisted(() => ({
|
|||||||
sleepWithAbortMock: vi.fn(async (_ms: number, _abortSignal?: AbortSignal) => undefined),
|
sleepWithAbortMock: vi.fn(async (_ms: number, _abortSignal?: AbortSignal) => undefined),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("./pi-embedded-runner/run/attempt.js", () => ({
|
const installRunEmbeddedMocks = () => {
|
||||||
runEmbeddedAttempt: (params: unknown) => runEmbeddedAttemptMock(params),
|
vi.doMock("../plugins/hook-runner-global.js", () => ({
|
||||||
}));
|
getGlobalHookRunner: vi.fn(() => undefined),
|
||||||
|
}));
|
||||||
vi.mock("../infra/backoff.js", () => ({
|
vi.doMock("../context-engine/index.js", () => ({
|
||||||
computeBackoff: (
|
ensureContextEnginesInitialized: vi.fn(),
|
||||||
policy: { initialMs: number; maxMs: number; factor: number; jitter: number },
|
resolveContextEngine: vi.fn(async () => ({
|
||||||
attempt: number,
|
dispose: async () => undefined,
|
||||||
) => computeBackoffMock(policy, attempt),
|
})),
|
||||||
sleepWithAbort: (ms: number, abortSignal?: AbortSignal) => sleepWithAbortMock(ms, abortSignal),
|
}));
|
||||||
}));
|
vi.doMock("./runtime-plugins.js", () => ({
|
||||||
|
ensureRuntimePluginsLoaded: vi.fn(),
|
||||||
vi.mock("../../extensions/github-copilot/token.js", () => ({
|
}));
|
||||||
DEFAULT_COPILOT_API_BASE_URL: "https://api.individual.githubcopilot.com",
|
vi.doMock("./pi-embedded-runner/model.js", () => ({
|
||||||
resolveCopilotApiToken: (...args: unknown[]) => resolveCopilotApiTokenMock(...args),
|
resolveModelAsync: async (provider: string, modelId: string) => ({
|
||||||
}));
|
model: {
|
||||||
|
id: modelId,
|
||||||
vi.mock("./pi-embedded-runner/compact.js", () => ({
|
name: modelId,
|
||||||
compactEmbeddedPiSessionDirect: vi.fn(async () => {
|
api: "openai-responses",
|
||||||
throw new Error("compact should not run in auth profile rotation tests");
|
provider,
|
||||||
}),
|
baseUrl:
|
||||||
}));
|
provider === "github-copilot" ? "https://api.copilot.example" : "https://example.com",
|
||||||
|
reasoning: false,
|
||||||
vi.mock("./models-config.js", async (importOriginal) => {
|
input: ["text"],
|
||||||
const mod = await importOriginal<typeof import("./models-config.js")>();
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||||
return {
|
contextWindow: 16_000,
|
||||||
...mod,
|
maxTokens: 2048,
|
||||||
ensureOpenClawModelsJson: vi.fn(async () => ({ wrote: false })),
|
},
|
||||||
};
|
error: undefined,
|
||||||
});
|
authStorage: {
|
||||||
|
setRuntimeApiKey: vi.fn(),
|
||||||
|
},
|
||||||
|
modelRegistry: {},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
vi.doMock("./pi-embedded-runner/run/attempt.js", () => ({
|
||||||
|
runEmbeddedAttempt: (params: unknown) => runEmbeddedAttemptMock(params),
|
||||||
|
}));
|
||||||
|
vi.doMock("../plugins/provider-runtime.runtime.js", () => ({
|
||||||
|
prepareProviderRuntimeAuth: async (params: {
|
||||||
|
provider: string;
|
||||||
|
context: { apiKey: string };
|
||||||
|
}) => {
|
||||||
|
if (params.provider !== "github-copilot") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const token = await resolveCopilotApiTokenMock(params.context.apiKey);
|
||||||
|
return {
|
||||||
|
apiKey: token.token,
|
||||||
|
baseUrl: token.baseUrl,
|
||||||
|
expiresAt: token.expiresAt,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
vi.doMock("../infra/backoff.js", () => ({
|
||||||
|
computeBackoff: (
|
||||||
|
policy: { initialMs: number; maxMs: number; factor: number; jitter: number },
|
||||||
|
attempt: number,
|
||||||
|
) => computeBackoffMock(policy, attempt),
|
||||||
|
sleepWithAbort: (ms: number, abortSignal?: AbortSignal) => sleepWithAbortMock(ms, abortSignal),
|
||||||
|
}));
|
||||||
|
vi.doMock("./pi-embedded-runner/compact.js", () => ({
|
||||||
|
compactEmbeddedPiSessionDirect: vi.fn(async () => {
|
||||||
|
throw new Error("compact should not run in auth profile rotation tests");
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
vi.doMock("./models-config.js", async (importOriginal) => {
|
||||||
|
const mod = await importOriginal<typeof import("./models-config.js")>();
|
||||||
|
return {
|
||||||
|
...mod,
|
||||||
|
ensureOpenClawModelsJson: vi.fn(async () => ({ wrote: false })),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
let runEmbeddedPiAgent: typeof import("./pi-embedded-runner/run.js").runEmbeddedPiAgent;
|
let runEmbeddedPiAgent: typeof import("./pi-embedded-runner/run.js").runEmbeddedPiAgent;
|
||||||
let unregisterLogTransport: (() => void) | undefined;
|
let unregisterLogTransport: (() => void) | undefined;
|
||||||
|
let registerLogTransportFn: typeof import("../logging/logger.js").registerLogTransport;
|
||||||
|
let resetLoggerFn: typeof import("../logging/logger.js").resetLogger;
|
||||||
|
let setLoggerOverrideFn: typeof import("../logging/logger.js").setLoggerOverride;
|
||||||
const originalFetch = globalThis.fetch;
|
const originalFetch = globalThis.fetch;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
vi.resetModules();
|
||||||
|
installRunEmbeddedMocks();
|
||||||
({ runEmbeddedPiAgent } = await import("./pi-embedded-runner/run.js"));
|
({ runEmbeddedPiAgent } = await import("./pi-embedded-runner/run.js"));
|
||||||
|
({
|
||||||
|
registerLogTransport: registerLogTransportFn,
|
||||||
|
resetLogger: resetLoggerFn,
|
||||||
|
setLoggerOverride: setLoggerOverrideFn,
|
||||||
|
} = await import("../logging/logger.js"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function runEmbeddedPiAgentInline(
|
||||||
|
params: Parameters<typeof runEmbeddedPiAgent>[0],
|
||||||
|
): Promise<Awaited<ReturnType<typeof runEmbeddedPiAgent>>> {
|
||||||
|
return await runEmbeddedPiAgent({
|
||||||
|
...params,
|
||||||
|
enqueue: async (task) => await task(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
runEmbeddedAttemptMock.mockClear();
|
runEmbeddedAttemptMock.mockReset();
|
||||||
|
runEmbeddedAttemptMock.mockImplementation(async () => {
|
||||||
|
throw new Error("unexpected extra runEmbeddedAttempt call");
|
||||||
|
});
|
||||||
resolveCopilotApiTokenMock.mockReset();
|
resolveCopilotApiTokenMock.mockReset();
|
||||||
|
resolveCopilotApiTokenMock.mockImplementation(async () => {
|
||||||
|
throw new Error("unexpected extra Copilot token refresh");
|
||||||
|
});
|
||||||
globalThis.fetch = vi.fn(async (input: string | URL | Request) => {
|
globalThis.fetch = vi.fn(async (input: string | URL | Request) => {
|
||||||
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
||||||
if (url !== COPILOT_TOKEN_URL) {
|
throw new Error(`Unexpected fetch in test: ${url}`);
|
||||||
throw new Error(`Unexpected fetch in test: ${url}`);
|
|
||||||
}
|
|
||||||
const token = await resolveCopilotApiTokenMock();
|
|
||||||
return {
|
|
||||||
ok: true,
|
|
||||||
status: 200,
|
|
||||||
json: async () => ({
|
|
||||||
token: token.token,
|
|
||||||
expires_at: Math.floor(token.expiresAt / 1000),
|
|
||||||
}),
|
|
||||||
} as Response;
|
|
||||||
}) as typeof fetch;
|
}) as typeof fetch;
|
||||||
computeBackoffMock.mockClear();
|
computeBackoffMock.mockClear();
|
||||||
sleepWithAbortMock.mockClear();
|
sleepWithAbortMock.mockClear();
|
||||||
@@ -88,8 +144,8 @@ afterEach(() => {
|
|||||||
globalThis.fetch = originalFetch;
|
globalThis.fetch = originalFetch;
|
||||||
unregisterLogTransport?.();
|
unregisterLogTransport?.();
|
||||||
unregisterLogTransport = undefined;
|
unregisterLogTransport = undefined;
|
||||||
setLoggerOverride(null);
|
setLoggerOverrideFn(null);
|
||||||
resetLogger();
|
resetLoggerFn();
|
||||||
});
|
});
|
||||||
|
|
||||||
const baseUsage = {
|
const baseUsage = {
|
||||||
@@ -324,7 +380,7 @@ async function runAutoPinnedOpenAiTurn(params: {
|
|||||||
runId: string;
|
runId: string;
|
||||||
authProfileId?: string;
|
authProfileId?: string;
|
||||||
}) {
|
}) {
|
||||||
await runEmbeddedPiAgent({
|
await runEmbeddedPiAgentInline({
|
||||||
sessionId: "session:test",
|
sessionId: "session:test",
|
||||||
sessionKey: params.sessionKey,
|
sessionKey: params.sessionKey,
|
||||||
sessionFile: path.join(params.workspaceDir, "session.jsonl"),
|
sessionFile: path.join(params.workspaceDir, "session.jsonl"),
|
||||||
@@ -368,7 +424,7 @@ async function runAutoPinnedRotationCase(params: {
|
|||||||
sessionKey: string;
|
sessionKey: string;
|
||||||
runId: string;
|
runId: string;
|
||||||
}) {
|
}) {
|
||||||
runEmbeddedAttemptMock.mockClear();
|
runEmbeddedAttemptMock.mockReset();
|
||||||
return withAgentWorkspace(async ({ agentDir, workspaceDir }) => {
|
return withAgentWorkspace(async ({ agentDir, workspaceDir }) => {
|
||||||
await writeAuthStore(agentDir);
|
await writeAuthStore(agentDir);
|
||||||
mockFailedThenSuccessfulAttempt(params.errorMessage);
|
mockFailedThenSuccessfulAttempt(params.errorMessage);
|
||||||
@@ -390,7 +446,7 @@ async function runAutoPinnedPromptErrorRotationCase(params: {
|
|||||||
sessionKey: string;
|
sessionKey: string;
|
||||||
runId: string;
|
runId: string;
|
||||||
}) {
|
}) {
|
||||||
runEmbeddedAttemptMock.mockClear();
|
runEmbeddedAttemptMock.mockReset();
|
||||||
return withAgentWorkspace(async ({ agentDir, workspaceDir }) => {
|
return withAgentWorkspace(async ({ agentDir, workspaceDir }) => {
|
||||||
await writeAuthStore(agentDir);
|
await writeAuthStore(agentDir);
|
||||||
mockPromptErrorThenSuccessfulAttempt(params.errorMessage);
|
mockPromptErrorThenSuccessfulAttempt(params.errorMessage);
|
||||||
@@ -486,7 +542,7 @@ async function runTurnWithCooldownSeed(params: {
|
|||||||
});
|
});
|
||||||
mockSingleSuccessfulAttempt();
|
mockSingleSuccessfulAttempt();
|
||||||
|
|
||||||
await runEmbeddedPiAgent({
|
await runEmbeddedPiAgentInline({
|
||||||
sessionId: "session:test",
|
sessionId: "session:test",
|
||||||
sessionKey: params.sessionKey,
|
sessionKey: params.sessionKey,
|
||||||
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
||||||
@@ -518,7 +574,9 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
|
|||||||
resolveCopilotApiTokenMock
|
resolveCopilotApiTokenMock
|
||||||
.mockResolvedValueOnce({
|
.mockResolvedValueOnce({
|
||||||
token: "copilot-initial",
|
token: "copilot-initial",
|
||||||
expiresAt: now + 2 * 60 * 1000,
|
// Keep expiry beyond the runtime refresh margin so the test only
|
||||||
|
// exercises auth-error refresh, not the background scheduler.
|
||||||
|
expiresAt: now + 10 * 60 * 1000,
|
||||||
source: "mock",
|
source: "mock",
|
||||||
baseUrl: "https://api.copilot.example",
|
baseUrl: "https://api.copilot.example",
|
||||||
})
|
})
|
||||||
@@ -549,7 +607,7 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await runEmbeddedPiAgent({
|
await runEmbeddedPiAgentInline({
|
||||||
sessionId: "session:test",
|
sessionId: "session:test",
|
||||||
sessionKey: "agent:test:copilot-auth-error",
|
sessionKey: "agent:test:copilot-auth-error",
|
||||||
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
||||||
@@ -582,13 +640,14 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
|
|||||||
resolveCopilotApiTokenMock
|
resolveCopilotApiTokenMock
|
||||||
.mockResolvedValueOnce({
|
.mockResolvedValueOnce({
|
||||||
token: "copilot-initial",
|
token: "copilot-initial",
|
||||||
expiresAt: now + 2 * 60 * 1000,
|
// Avoid an immediate scheduled refresh racing the explicit auth retry.
|
||||||
|
expiresAt: now + 10 * 60 * 1000,
|
||||||
source: "mock",
|
source: "mock",
|
||||||
baseUrl: "https://api.copilot.example",
|
baseUrl: "https://api.copilot.example",
|
||||||
})
|
})
|
||||||
.mockResolvedValueOnce({
|
.mockResolvedValueOnce({
|
||||||
token: "copilot-refresh-1",
|
token: "copilot-refresh-1",
|
||||||
expiresAt: now + 4 * 60 * 1000,
|
expiresAt: now + 10 * 60 * 1000,
|
||||||
source: "mock",
|
source: "mock",
|
||||||
baseUrl: "https://api.copilot.example",
|
baseUrl: "https://api.copilot.example",
|
||||||
})
|
})
|
||||||
@@ -633,7 +692,7 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await runEmbeddedPiAgent({
|
await runEmbeddedPiAgentInline({
|
||||||
sessionId: "session:test",
|
sessionId: "session:test",
|
||||||
sessionKey: "agent:test:copilot-auth-repeat",
|
sessionKey: "agent:test:copilot-auth-repeat",
|
||||||
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
||||||
@@ -647,7 +706,6 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
|
|||||||
timeoutMs: 5_000,
|
timeoutMs: 5_000,
|
||||||
runId: "run:copilot-auth-repeat",
|
runId: "run:copilot-auth-repeat",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(runEmbeddedAttemptMock).toHaveBeenCalledTimes(4);
|
expect(runEmbeddedAttemptMock).toHaveBeenCalledTimes(4);
|
||||||
expect(resolveCopilotApiTokenMock).toHaveBeenCalledTimes(3);
|
expect(resolveCopilotApiTokenMock).toHaveBeenCalledTimes(3);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -682,7 +740,7 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const runPromise = runEmbeddedPiAgent({
|
const runPromise = runEmbeddedPiAgentInline({
|
||||||
sessionId: "session:test",
|
sessionId: "session:test",
|
||||||
sessionKey: "agent:test:copilot-shutdown",
|
sessionKey: "agent:test:copilot-shutdown",
|
||||||
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
||||||
@@ -744,12 +802,12 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
|
|||||||
|
|
||||||
it("logs structured failover decision metadata for overloaded assistant rotation", async () => {
|
it("logs structured failover decision metadata for overloaded assistant rotation", async () => {
|
||||||
const records: Array<Record<string, unknown>> = [];
|
const records: Array<Record<string, unknown>> = [];
|
||||||
setLoggerOverride({
|
setLoggerOverrideFn({
|
||||||
level: "trace",
|
level: "trace",
|
||||||
consoleLevel: "silent",
|
consoleLevel: "silent",
|
||||||
file: path.join(os.tmpdir(), `openclaw-auth-rotation-${Date.now()}.log`),
|
file: path.join(os.tmpdir(), `openclaw-auth-rotation-${Date.now()}.log`),
|
||||||
});
|
});
|
||||||
unregisterLogTransport = registerLogTransport((record) => {
|
unregisterLogTransport = registerLogTransportFn((record) => {
|
||||||
records.push(record);
|
records.push(record);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -858,7 +916,7 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await runEmbeddedPiAgent({
|
const result = await runEmbeddedPiAgentInline({
|
||||||
sessionId: "session:test",
|
sessionId: "session:test",
|
||||||
sessionKey: "agent:test:compaction-timeout",
|
sessionKey: "agent:test:compaction-timeout",
|
||||||
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
||||||
@@ -887,7 +945,7 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
|
|||||||
|
|
||||||
mockSingleErrorAttempt({ errorMessage: "rate limit" });
|
mockSingleErrorAttempt({ errorMessage: "rate limit" });
|
||||||
|
|
||||||
await runEmbeddedPiAgent({
|
await runEmbeddedPiAgentInline({
|
||||||
sessionId: "session:test",
|
sessionId: "session:test",
|
||||||
sessionKey: "agent:test:user",
|
sessionKey: "agent:test:user",
|
||||||
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
||||||
@@ -935,7 +993,7 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await runEmbeddedPiAgent({
|
await runEmbeddedPiAgentInline({
|
||||||
sessionId: "session:test",
|
sessionId: "session:test",
|
||||||
sessionKey: "agent:test:mismatch",
|
sessionKey: "agent:test:mismatch",
|
||||||
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
||||||
@@ -977,7 +1035,7 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
runEmbeddedPiAgent({
|
runEmbeddedPiAgentInline({
|
||||||
sessionId: "session:test",
|
sessionId: "session:test",
|
||||||
sessionKey: "agent:test:cooldown-failover",
|
sessionKey: "agent:test:cooldown-failover",
|
||||||
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
||||||
@@ -1021,7 +1079,7 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await runEmbeddedPiAgent({
|
const result = await runEmbeddedPiAgentInline({
|
||||||
sessionId: "session:test",
|
sessionId: "session:test",
|
||||||
sessionKey: "agent:test:cooldown-probe",
|
sessionKey: "agent:test:cooldown-probe",
|
||||||
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
||||||
@@ -1069,7 +1127,7 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await runEmbeddedPiAgent({
|
const result = await runEmbeddedPiAgentInline({
|
||||||
sessionId: "session:test",
|
sessionId: "session:test",
|
||||||
sessionKey: "agent:test:overloaded-cooldown-probe",
|
sessionKey: "agent:test:overloaded-cooldown-probe",
|
||||||
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
||||||
@@ -1117,7 +1175,7 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await runEmbeddedPiAgent({
|
const result = await runEmbeddedPiAgentInline({
|
||||||
sessionId: "session:test",
|
sessionId: "session:test",
|
||||||
sessionKey: "agent:test:billing-cooldown-probe-no-fallbacks",
|
sessionKey: "agent:test:billing-cooldown-probe-no-fallbacks",
|
||||||
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
||||||
@@ -1148,7 +1206,7 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
runEmbeddedPiAgent({
|
runEmbeddedPiAgentInline({
|
||||||
sessionId: "session:test",
|
sessionId: "session:test",
|
||||||
sessionKey: "agent:support:cooldown-failover",
|
sessionKey: "agent:support:cooldown-failover",
|
||||||
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
||||||
@@ -1193,7 +1251,7 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
runEmbeddedPiAgent({
|
runEmbeddedPiAgentInline({
|
||||||
sessionId: "session:test",
|
sessionId: "session:test",
|
||||||
sessionKey: "agent:test:disabled-failover",
|
sessionKey: "agent:test:disabled-failover",
|
||||||
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
||||||
@@ -1227,7 +1285,7 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
|
|||||||
await fs.writeFile(authPath, JSON.stringify({ version: 1, profiles: {}, usageStats: {} }));
|
await fs.writeFile(authPath, JSON.stringify({ version: 1, profiles: {}, usageStats: {} }));
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
runEmbeddedPiAgent({
|
runEmbeddedPiAgentInline({
|
||||||
sessionId: "session:test",
|
sessionId: "session:test",
|
||||||
sessionKey: "agent:test:auth-unavailable",
|
sessionKey: "agent:test:auth-unavailable",
|
||||||
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
||||||
@@ -1265,7 +1323,7 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
|
|||||||
|
|
||||||
let thrown: unknown;
|
let thrown: unknown;
|
||||||
try {
|
try {
|
||||||
await runEmbeddedPiAgent({
|
await runEmbeddedPiAgentInline({
|
||||||
sessionId: "session:test",
|
sessionId: "session:test",
|
||||||
sessionKey: "agent:test:billing-failover-active-model",
|
sessionKey: "agent:test:billing-failover-active-model",
|
||||||
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
||||||
|
|||||||
@@ -1,58 +1,71 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { resetDiagnosticSessionStateForTest } from "../logging/diagnostic-session-state.js";
|
import { resetDiagnosticSessionStateForTest } from "../logging/diagnostic-session-state.js";
|
||||||
import { getGlobalHookRunner } from "../plugins/hook-runner-global.js";
|
|
||||||
import { toClientToolDefinitions, toToolDefinitions } from "./pi-tool-definition-adapter.js";
|
|
||||||
import { wrapToolWithAbortSignal } from "./pi-tools.abort.js";
|
|
||||||
import {
|
import {
|
||||||
__testing as beforeToolCallTesting,
|
initializeGlobalHookRunner,
|
||||||
consumeAdjustedParamsForToolCall,
|
resetGlobalHookRunner,
|
||||||
wrapToolWithBeforeToolCallHook,
|
} from "../plugins/hook-runner-global.js";
|
||||||
} from "./pi-tools.before-tool-call.js";
|
import { createMockPluginRegistry } from "../plugins/hooks.test-helpers.js";
|
||||||
|
|
||||||
vi.mock("../plugins/hook-runner-global.js", async (importOriginal) => {
|
type ToolDefinitionAdapterModule = typeof import("./pi-tool-definition-adapter.js");
|
||||||
const actual = await importOriginal<typeof import("../plugins/hook-runner-global.js")>();
|
type PiToolsAbortModule = typeof import("./pi-tools.abort.js");
|
||||||
return {
|
type BeforeToolCallModule = typeof import("./pi-tools.before-tool-call.js");
|
||||||
...actual,
|
|
||||||
getGlobalHookRunner: vi.fn(),
|
type ToClientToolDefinitions = ToolDefinitionAdapterModule["toClientToolDefinitions"];
|
||||||
};
|
type ToToolDefinitions = ToolDefinitionAdapterModule["toToolDefinitions"];
|
||||||
|
type WrapToolWithAbortSignal = PiToolsAbortModule["wrapToolWithAbortSignal"];
|
||||||
|
type BeforeToolCallTesting = BeforeToolCallModule["__testing"];
|
||||||
|
type ConsumeAdjustedParamsForToolCall = BeforeToolCallModule["consumeAdjustedParamsForToolCall"];
|
||||||
|
type WrapToolWithBeforeToolCallHook = BeforeToolCallModule["wrapToolWithBeforeToolCallHook"];
|
||||||
|
|
||||||
|
let toClientToolDefinitions!: ToClientToolDefinitions;
|
||||||
|
let toToolDefinitions!: ToToolDefinitions;
|
||||||
|
let wrapToolWithAbortSignal!: WrapToolWithAbortSignal;
|
||||||
|
let beforeToolCallTesting!: BeforeToolCallTesting;
|
||||||
|
let consumeAdjustedParamsForToolCall!: ConsumeAdjustedParamsForToolCall;
|
||||||
|
let wrapToolWithBeforeToolCallHook!: WrapToolWithBeforeToolCallHook;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
if (!wrapToolWithBeforeToolCallHook) {
|
||||||
|
({ toClientToolDefinitions, toToolDefinitions } =
|
||||||
|
await import("./pi-tool-definition-adapter.js"));
|
||||||
|
({ wrapToolWithAbortSignal } = await import("./pi-tools.abort.js"));
|
||||||
|
({
|
||||||
|
__testing: beforeToolCallTesting,
|
||||||
|
consumeAdjustedParamsForToolCall,
|
||||||
|
wrapToolWithBeforeToolCallHook,
|
||||||
|
} = await import("./pi-tools.before-tool-call.js"));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockGetGlobalHookRunner = vi.mocked(getGlobalHookRunner);
|
type BeforeToolCallHandlerMock = ReturnType<typeof vi.fn>;
|
||||||
|
|
||||||
type HookRunnerMock = {
|
function installBeforeToolCallHook(params?: {
|
||||||
hasHooks: ReturnType<typeof vi.fn>;
|
enabled?: boolean;
|
||||||
runBeforeToolCall: ReturnType<typeof vi.fn>;
|
|
||||||
};
|
|
||||||
|
|
||||||
function installMockHookRunner(params?: {
|
|
||||||
hasHooksReturn?: boolean;
|
|
||||||
runBeforeToolCallImpl?: (...args: unknown[]) => unknown;
|
runBeforeToolCallImpl?: (...args: unknown[]) => unknown;
|
||||||
}) {
|
}): BeforeToolCallHandlerMock {
|
||||||
const hookRunner: HookRunnerMock = {
|
resetGlobalHookRunner();
|
||||||
hasHooks:
|
const handler = params?.runBeforeToolCallImpl
|
||||||
params?.hasHooksReturn === undefined
|
? vi.fn(params.runBeforeToolCallImpl)
|
||||||
? vi.fn()
|
: vi.fn(async () => undefined);
|
||||||
: vi.fn(() => params.hasHooksReturn as boolean),
|
if (params?.enabled === false) {
|
||||||
runBeforeToolCall: params?.runBeforeToolCallImpl
|
return handler;
|
||||||
? vi.fn(params.runBeforeToolCallImpl)
|
}
|
||||||
: vi.fn(),
|
initializeGlobalHookRunner(createMockPluginRegistry([{ hookName: "before_tool_call", handler }]));
|
||||||
};
|
return handler;
|
||||||
// oxlint-disable-next-line typescript/no-explicit-any
|
|
||||||
mockGetGlobalHookRunner.mockReturnValue(hookRunner as any);
|
|
||||||
return hookRunner;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("before_tool_call hook integration", () => {
|
describe("before_tool_call hook integration", () => {
|
||||||
let hookRunner: HookRunnerMock;
|
let beforeToolCallHook: BeforeToolCallHandlerMock;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
resetGlobalHookRunner();
|
||||||
resetDiagnosticSessionStateForTest();
|
resetDiagnosticSessionStateForTest();
|
||||||
beforeToolCallTesting.adjustedParamsByToolCallId.clear();
|
beforeToolCallTesting.adjustedParamsByToolCallId.clear();
|
||||||
hookRunner = installMockHookRunner();
|
beforeToolCallHook = installBeforeToolCallHook();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("executes tool normally when no hook is registered", async () => {
|
it("executes tool normally when no hook is registered", async () => {
|
||||||
hookRunner.hasHooks.mockReturnValue(false);
|
beforeToolCallHook = installBeforeToolCallHook({ enabled: false });
|
||||||
const execute = vi.fn().mockResolvedValue({ content: [], details: { ok: true } });
|
const execute = vi.fn().mockResolvedValue({ content: [], details: { ok: true } });
|
||||||
// oxlint-disable-next-line typescript/no-explicit-any
|
// oxlint-disable-next-line typescript/no-explicit-any
|
||||||
const tool = wrapToolWithBeforeToolCallHook({ name: "Read", execute } as any, {
|
const tool = wrapToolWithBeforeToolCallHook({ name: "Read", execute } as any, {
|
||||||
@@ -63,7 +76,7 @@ describe("before_tool_call hook integration", () => {
|
|||||||
|
|
||||||
await tool.execute("call-1", { path: "/tmp/file" }, undefined, extensionContext);
|
await tool.execute("call-1", { path: "/tmp/file" }, undefined, extensionContext);
|
||||||
|
|
||||||
expect(hookRunner.runBeforeToolCall).not.toHaveBeenCalled();
|
expect(beforeToolCallHook).not.toHaveBeenCalled();
|
||||||
expect(execute).toHaveBeenCalledWith(
|
expect(execute).toHaveBeenCalledWith(
|
||||||
"call-1",
|
"call-1",
|
||||||
{ path: "/tmp/file" },
|
{ path: "/tmp/file" },
|
||||||
@@ -73,8 +86,9 @@ describe("before_tool_call hook integration", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("allows hook to modify parameters", async () => {
|
it("allows hook to modify parameters", async () => {
|
||||||
hookRunner.hasHooks.mockReturnValue(true);
|
beforeToolCallHook = installBeforeToolCallHook({
|
||||||
hookRunner.runBeforeToolCall.mockResolvedValue({ params: { mode: "safe" } });
|
runBeforeToolCallImpl: async () => ({ params: { mode: "safe" } }),
|
||||||
|
});
|
||||||
const execute = vi.fn().mockResolvedValue({ content: [], details: { ok: true } });
|
const execute = vi.fn().mockResolvedValue({ content: [], details: { ok: true } });
|
||||||
// oxlint-disable-next-line typescript/no-explicit-any
|
// oxlint-disable-next-line typescript/no-explicit-any
|
||||||
const tool = wrapToolWithBeforeToolCallHook({ name: "exec", execute } as any);
|
const tool = wrapToolWithBeforeToolCallHook({ name: "exec", execute } as any);
|
||||||
@@ -91,10 +105,11 @@ describe("before_tool_call hook integration", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("blocks tool execution when hook returns block=true", async () => {
|
it("blocks tool execution when hook returns block=true", async () => {
|
||||||
hookRunner.hasHooks.mockReturnValue(true);
|
beforeToolCallHook = installBeforeToolCallHook({
|
||||||
hookRunner.runBeforeToolCall.mockResolvedValue({
|
runBeforeToolCallImpl: async () => ({
|
||||||
block: true,
|
block: true,
|
||||||
blockReason: "blocked",
|
blockReason: "blocked",
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
const execute = vi.fn().mockResolvedValue({ content: [], details: { ok: true } });
|
const execute = vi.fn().mockResolvedValue({ content: [], details: { ok: true } });
|
||||||
// oxlint-disable-next-line typescript/no-explicit-any
|
// oxlint-disable-next-line typescript/no-explicit-any
|
||||||
@@ -108,8 +123,11 @@ describe("before_tool_call hook integration", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("continues execution when hook throws", async () => {
|
it("continues execution when hook throws", async () => {
|
||||||
hookRunner.hasHooks.mockReturnValue(true);
|
beforeToolCallHook = installBeforeToolCallHook({
|
||||||
hookRunner.runBeforeToolCall.mockRejectedValue(new Error("boom"));
|
runBeforeToolCallImpl: async () => {
|
||||||
|
throw new Error("boom");
|
||||||
|
},
|
||||||
|
});
|
||||||
const execute = vi.fn().mockResolvedValue({ content: [], details: { ok: true } });
|
const execute = vi.fn().mockResolvedValue({ content: [], details: { ok: true } });
|
||||||
// oxlint-disable-next-line typescript/no-explicit-any
|
// oxlint-disable-next-line typescript/no-explicit-any
|
||||||
const tool = wrapToolWithBeforeToolCallHook({ name: "read", execute } as any);
|
const tool = wrapToolWithBeforeToolCallHook({ name: "read", execute } as any);
|
||||||
@@ -126,8 +144,9 @@ describe("before_tool_call hook integration", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("normalizes non-object params for hook contract", async () => {
|
it("normalizes non-object params for hook contract", async () => {
|
||||||
hookRunner.hasHooks.mockReturnValue(true);
|
beforeToolCallHook = installBeforeToolCallHook({
|
||||||
hookRunner.runBeforeToolCall.mockResolvedValue(undefined);
|
runBeforeToolCallImpl: async () => undefined,
|
||||||
|
});
|
||||||
const execute = vi.fn().mockResolvedValue({ content: [], details: { ok: true } });
|
const execute = vi.fn().mockResolvedValue({ content: [], details: { ok: true } });
|
||||||
// oxlint-disable-next-line typescript/no-explicit-any
|
// oxlint-disable-next-line typescript/no-explicit-any
|
||||||
const tool = wrapToolWithBeforeToolCallHook({ name: "ReAd", execute } as any, {
|
const tool = wrapToolWithBeforeToolCallHook({ name: "ReAd", execute } as any, {
|
||||||
@@ -140,7 +159,7 @@ describe("before_tool_call hook integration", () => {
|
|||||||
|
|
||||||
await tool.execute("call-5", "not-an-object", undefined, extensionContext);
|
await tool.execute("call-5", "not-an-object", undefined, extensionContext);
|
||||||
|
|
||||||
expect(hookRunner.runBeforeToolCall).toHaveBeenCalledWith(
|
expect(beforeToolCallHook).toHaveBeenCalledWith(
|
||||||
{
|
{
|
||||||
toolName: "read",
|
toolName: "read",
|
||||||
params: {},
|
params: {},
|
||||||
@@ -159,10 +178,12 @@ describe("before_tool_call hook integration", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("keeps adjusted params isolated per run when toolCallId collides", async () => {
|
it("keeps adjusted params isolated per run when toolCallId collides", async () => {
|
||||||
hookRunner.hasHooks.mockReturnValue(true);
|
beforeToolCallHook = installBeforeToolCallHook({
|
||||||
hookRunner.runBeforeToolCall
|
runBeforeToolCallImpl: vi
|
||||||
.mockResolvedValueOnce({ params: { marker: "A" } })
|
.fn()
|
||||||
.mockResolvedValueOnce({ params: { marker: "B" } });
|
.mockResolvedValueOnce({ params: { marker: "A" } })
|
||||||
|
.mockResolvedValueOnce({ params: { marker: "B" } }),
|
||||||
|
});
|
||||||
const execute = vi.fn().mockResolvedValue({ content: [], details: { ok: true } });
|
const execute = vi.fn().mockResolvedValue({ content: [], details: { ok: true } });
|
||||||
// oxlint-disable-next-line typescript/no-explicit-any
|
// oxlint-disable-next-line typescript/no-explicit-any
|
||||||
const toolA = wrapToolWithBeforeToolCallHook({ name: "Read", execute } as any, {
|
const toolA = wrapToolWithBeforeToolCallHook({ name: "Read", execute } as any, {
|
||||||
@@ -192,12 +213,12 @@ describe("before_tool_call hook integration", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("before_tool_call hook deduplication (#15502)", () => {
|
describe("before_tool_call hook deduplication (#15502)", () => {
|
||||||
let hookRunner: HookRunnerMock;
|
let beforeToolCallHook: BeforeToolCallHandlerMock;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
resetGlobalHookRunner();
|
||||||
resetDiagnosticSessionStateForTest();
|
resetDiagnosticSessionStateForTest();
|
||||||
hookRunner = installMockHookRunner({
|
beforeToolCallHook = installBeforeToolCallHook({
|
||||||
hasHooksReturn: true,
|
|
||||||
runBeforeToolCallImpl: async () => undefined,
|
runBeforeToolCallImpl: async () => undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -221,7 +242,7 @@ describe("before_tool_call hook deduplication (#15502)", () => {
|
|||||||
extensionContext,
|
extensionContext,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(hookRunner.runBeforeToolCall).toHaveBeenCalledTimes(1);
|
expect(beforeToolCallHook).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("fires hook exactly once when tool goes through wrap + abort + toToolDefinitions", async () => {
|
it("fires hook exactly once when tool goes through wrap + abort + toToolDefinitions", async () => {
|
||||||
@@ -246,21 +267,21 @@ describe("before_tool_call hook deduplication (#15502)", () => {
|
|||||||
extensionContext,
|
extensionContext,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(hookRunner.runBeforeToolCall).toHaveBeenCalledTimes(1);
|
expect(beforeToolCallHook).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("before_tool_call hook integration for client tools", () => {
|
describe("before_tool_call hook integration for client tools", () => {
|
||||||
let hookRunner: HookRunnerMock;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
resetGlobalHookRunner();
|
||||||
resetDiagnosticSessionStateForTest();
|
resetDiagnosticSessionStateForTest();
|
||||||
hookRunner = installMockHookRunner();
|
installBeforeToolCallHook();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("passes modified params to client tool callbacks", async () => {
|
it("passes modified params to client tool callbacks", async () => {
|
||||||
hookRunner.hasHooks.mockReturnValue(true);
|
installBeforeToolCallHook({
|
||||||
hookRunner.runBeforeToolCall.mockResolvedValue({ params: { extra: true } });
|
runBeforeToolCallImpl: async () => ({ params: { extra: true } }),
|
||||||
|
});
|
||||||
const onClientToolCall = vi.fn();
|
const onClientToolCall = vi.fn();
|
||||||
const [tool] = toClientToolDefinitions(
|
const [tool] = toClientToolDefinitions(
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
import { readFile } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { resolveSessionKey } from "../config/sessions.js";
|
|
||||||
import {
|
import {
|
||||||
getProviderUsageMocks,
|
getProviderUsageMocks,
|
||||||
getRunEmbeddedPiAgentMock,
|
getRunEmbeddedPiAgentMock,
|
||||||
@@ -28,191 +27,132 @@ function getReplyFromConfigNow(getReplyFromConfig: () => GetReplyFromConfig): Ge
|
|||||||
return getReplyFromConfig();
|
return getReplyFromConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function seedUsageSummary(): void {
|
||||||
|
usageMocks.loadProviderUsageSummary.mockClear();
|
||||||
|
usageMocks.loadProviderUsageSummary.mockResolvedValue({
|
||||||
|
updatedAt: 0,
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provider: "anthropic",
|
||||||
|
displayName: "Anthropic",
|
||||||
|
windows: [
|
||||||
|
{
|
||||||
|
label: "5h",
|
||||||
|
usedPercent: 20,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function registerTriggerHandlingUsageSummaryCases(params: {
|
export function registerTriggerHandlingUsageSummaryCases(params: {
|
||||||
getReplyFromConfig: () => GetReplyFromConfig;
|
getReplyFromConfig: () => GetReplyFromConfig;
|
||||||
}): void {
|
}): void {
|
||||||
describe("usage and status command handling", () => {
|
describe("usage and status command handling", () => {
|
||||||
it("handles status, usage cycles, and auth-profile status details", async () => {
|
it("shows status without invoking the agent", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
|
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
|
||||||
const getReplyFromConfig = getReplyFromConfigNow(params.getReplyFromConfig);
|
const getReplyFromConfig = getReplyFromConfigNow(params.getReplyFromConfig);
|
||||||
usageMocks.loadProviderUsageSummary.mockClear();
|
seedUsageSummary();
|
||||||
usageMocks.loadProviderUsageSummary.mockResolvedValue({
|
|
||||||
updatedAt: 0,
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provider: "anthropic",
|
|
||||||
displayName: "Anthropic",
|
|
||||||
windows: [
|
|
||||||
{
|
|
||||||
label: "5h",
|
|
||||||
usedPercent: 20,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
{
|
const res = await getReplyFromConfig(
|
||||||
const res = await getReplyFromConfig(
|
{
|
||||||
{
|
Body: "/status",
|
||||||
Body: "/status",
|
From: "+1000",
|
||||||
From: "+1000",
|
|
||||||
To: "+2000",
|
|
||||||
Provider: "whatsapp",
|
|
||||||
SenderE164: "+1000",
|
|
||||||
CommandAuthorized: true,
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
makeCfg(home),
|
|
||||||
);
|
|
||||||
|
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
|
||||||
expect(text).toContain("Model:");
|
|
||||||
expect(text).toContain("OpenClaw");
|
|
||||||
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const cfg = makeCfg(home);
|
|
||||||
cfg.session = { ...cfg.session, store: join(home, "usage-cycle.sessions.json") };
|
|
||||||
const usageStorePath = requireSessionStorePath(cfg);
|
|
||||||
const r0 = await getReplyFromConfig(
|
|
||||||
{
|
|
||||||
Body: "/usage on",
|
|
||||||
From: "+1000",
|
|
||||||
To: "+2000",
|
|
||||||
Provider: "whatsapp",
|
|
||||||
SenderE164: "+1000",
|
|
||||||
CommandAuthorized: true,
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
cfg,
|
|
||||||
);
|
|
||||||
expect(String((Array.isArray(r0) ? r0[0]?.text : r0?.text) ?? "")).toContain(
|
|
||||||
"Usage footer: tokens",
|
|
||||||
);
|
|
||||||
|
|
||||||
const r1 = await getReplyFromConfig(
|
|
||||||
{
|
|
||||||
Body: "/usage",
|
|
||||||
From: "+1000",
|
|
||||||
To: "+2000",
|
|
||||||
Provider: "whatsapp",
|
|
||||||
SenderE164: "+1000",
|
|
||||||
CommandAuthorized: true,
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
cfg,
|
|
||||||
);
|
|
||||||
expect(String((Array.isArray(r1) ? r1[0]?.text : r1?.text) ?? "")).toContain(
|
|
||||||
"Usage footer: full",
|
|
||||||
);
|
|
||||||
|
|
||||||
const r2 = await getReplyFromConfig(
|
|
||||||
{
|
|
||||||
Body: "/usage",
|
|
||||||
From: "+1000",
|
|
||||||
To: "+2000",
|
|
||||||
Provider: "whatsapp",
|
|
||||||
SenderE164: "+1000",
|
|
||||||
CommandAuthorized: true,
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
cfg,
|
|
||||||
);
|
|
||||||
expect(String((Array.isArray(r2) ? r2[0]?.text : r2?.text) ?? "")).toContain(
|
|
||||||
"Usage footer: off",
|
|
||||||
);
|
|
||||||
|
|
||||||
const r3 = await getReplyFromConfig(
|
|
||||||
{
|
|
||||||
Body: "/usage",
|
|
||||||
From: "+1000",
|
|
||||||
To: "+2000",
|
|
||||||
Provider: "whatsapp",
|
|
||||||
SenderE164: "+1000",
|
|
||||||
CommandAuthorized: true,
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
cfg,
|
|
||||||
);
|
|
||||||
expect(String((Array.isArray(r3) ? r3[0]?.text : r3?.text) ?? "")).toContain(
|
|
||||||
"Usage footer: tokens",
|
|
||||||
);
|
|
||||||
const finalStore = await readSessionStore(usageStorePath);
|
|
||||||
expect(pickFirstStoreEntry<{ responseUsage?: string }>(finalStore)?.responseUsage).toBe(
|
|
||||||
"tokens",
|
|
||||||
);
|
|
||||||
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
runEmbeddedPiAgentMock.mockClear();
|
|
||||||
const cfg = makeCfg(home);
|
|
||||||
cfg.session = { ...cfg.session, store: join(home, "auth-profile-status.sessions.json") };
|
|
||||||
const agentDir = join(home, ".openclaw", "agents", "main", "agent");
|
|
||||||
await mkdir(agentDir, { recursive: true });
|
|
||||||
await writeFile(
|
|
||||||
join(agentDir, "auth-profiles.json"),
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
version: 1,
|
|
||||||
profiles: {
|
|
||||||
"anthropic:work": {
|
|
||||||
type: "api_key",
|
|
||||||
provider: "anthropic",
|
|
||||||
key: "sk-test-1234567890abcdef",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
lastGood: { anthropic: "anthropic:work" },
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const sessionKey = resolveSessionKey("per-sender", {
|
|
||||||
From: "+1002",
|
|
||||||
To: "+2000",
|
To: "+2000",
|
||||||
Provider: "whatsapp",
|
Provider: "whatsapp",
|
||||||
} as Parameters<typeof resolveSessionKey>[1]);
|
SenderE164: "+1000",
|
||||||
await writeFile(
|
CommandAuthorized: true,
|
||||||
requireSessionStorePath(cfg),
|
},
|
||||||
JSON.stringify(
|
{},
|
||||||
{
|
makeCfg(home),
|
||||||
[sessionKey]: {
|
);
|
||||||
sessionId: "session-auth",
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
authProfileOverride: "anthropic:work",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const res = await getReplyFromConfig(
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
{
|
expect(text).toContain("Model:");
|
||||||
Body: "/status",
|
expect(text).toContain("OpenClaw");
|
||||||
From: "+1002",
|
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
|
||||||
To: "+2000",
|
});
|
||||||
Provider: "whatsapp",
|
});
|
||||||
SenderE164: "+1002",
|
|
||||||
CommandAuthorized: true,
|
it("cycles usage footer modes and persists the final selection", async () => {
|
||||||
},
|
await withTempHome(async (home) => {
|
||||||
{},
|
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
|
||||||
cfg,
|
const getReplyFromConfig = getReplyFromConfigNow(params.getReplyFromConfig);
|
||||||
);
|
const cfg = makeCfg(home);
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
cfg.session = { ...cfg.session, store: join(home, "usage-cycle.sessions.json") };
|
||||||
expect(text).toContain("api-key");
|
const usageStorePath = requireSessionStorePath(cfg);
|
||||||
expect(text).not.toContain("sk-test");
|
|
||||||
expect(text).not.toContain("abcdef");
|
const r0 = await getReplyFromConfig(
|
||||||
expect(text).not.toContain("1234567890abcdef"); // pragma: allowlist secret
|
{
|
||||||
expect(text).toContain("(anthropic:work)");
|
Body: "/usage on",
|
||||||
expect(text).not.toContain("mixed");
|
From: "+1000",
|
||||||
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
|
To: "+2000",
|
||||||
}
|
Provider: "whatsapp",
|
||||||
|
SenderE164: "+1000",
|
||||||
|
CommandAuthorized: true,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
expect(String((Array.isArray(r0) ? r0[0]?.text : r0?.text) ?? "")).toContain(
|
||||||
|
"Usage footer: tokens",
|
||||||
|
);
|
||||||
|
|
||||||
|
const r1 = await getReplyFromConfig(
|
||||||
|
{
|
||||||
|
Body: "/usage",
|
||||||
|
From: "+1000",
|
||||||
|
To: "+2000",
|
||||||
|
Provider: "whatsapp",
|
||||||
|
SenderE164: "+1000",
|
||||||
|
CommandAuthorized: true,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
expect(String((Array.isArray(r1) ? r1[0]?.text : r1?.text) ?? "")).toContain(
|
||||||
|
"Usage footer: full",
|
||||||
|
);
|
||||||
|
|
||||||
|
const r2 = await getReplyFromConfig(
|
||||||
|
{
|
||||||
|
Body: "/usage",
|
||||||
|
From: "+1000",
|
||||||
|
To: "+2000",
|
||||||
|
Provider: "whatsapp",
|
||||||
|
SenderE164: "+1000",
|
||||||
|
CommandAuthorized: true,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
expect(String((Array.isArray(r2) ? r2[0]?.text : r2?.text) ?? "")).toContain(
|
||||||
|
"Usage footer: off",
|
||||||
|
);
|
||||||
|
|
||||||
|
const r3 = await getReplyFromConfig(
|
||||||
|
{
|
||||||
|
Body: "/usage",
|
||||||
|
From: "+1000",
|
||||||
|
To: "+2000",
|
||||||
|
Provider: "whatsapp",
|
||||||
|
SenderE164: "+1000",
|
||||||
|
CommandAuthorized: true,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
expect(String((Array.isArray(r3) ? r3[0]?.text : r3?.text) ?? "")).toContain(
|
||||||
|
"Usage footer: tokens",
|
||||||
|
);
|
||||||
|
|
||||||
|
const finalStore = await readSessionStore(usageStorePath);
|
||||||
|
expect(pickFirstStoreEntry<{ responseUsage?: string }>(finalStore)?.responseUsage).toBe(
|
||||||
|
"tokens",
|
||||||
|
);
|
||||||
|
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import fs from "node:fs/promises";
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { loadSessionStore, resolveSessionKey } from "../config/sessions.js";
|
import { loadSessionStore, resolveSessionKey } from "../config/sessions.js";
|
||||||
import { getReplyFromConfig } from "./reply.js";
|
|
||||||
import { registerGroupIntroPromptCases } from "./reply.triggers.group-intro-prompts.cases.js";
|
import { registerGroupIntroPromptCases } from "./reply.triggers.group-intro-prompts.cases.js";
|
||||||
import { registerTriggerHandlingUsageSummaryCases } from "./reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.cases.js";
|
import { registerTriggerHandlingUsageSummaryCases } from "./reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.cases.js";
|
||||||
import {
|
import {
|
||||||
@@ -10,7 +9,7 @@ import {
|
|||||||
getAbortEmbeddedPiRunMock,
|
getAbortEmbeddedPiRunMock,
|
||||||
getCompactEmbeddedPiSessionMock,
|
getCompactEmbeddedPiSessionMock,
|
||||||
getRunEmbeddedPiAgentMock,
|
getRunEmbeddedPiAgentMock,
|
||||||
installTriggerHandlingE2eTestHooks,
|
installTriggerHandlingReplyHarness,
|
||||||
MAIN_SESSION_KEY,
|
MAIN_SESSION_KEY,
|
||||||
makeCfg,
|
makeCfg,
|
||||||
mockRunEmbeddedPiAgentOk,
|
mockRunEmbeddedPiAgentOk,
|
||||||
@@ -21,6 +20,8 @@ import {
|
|||||||
import { enqueueFollowupRun, getFollowupQueueDepth, type FollowupRun } from "./reply/queue.js";
|
import { enqueueFollowupRun, getFollowupQueueDepth, type FollowupRun } from "./reply/queue.js";
|
||||||
import { HEARTBEAT_TOKEN } from "./tokens.js";
|
import { HEARTBEAT_TOKEN } from "./tokens.js";
|
||||||
|
|
||||||
|
type GetReplyFromConfig = typeof import("./reply.js").getReplyFromConfig;
|
||||||
|
|
||||||
vi.mock("./reply/agent-runner.runtime.js", () => ({
|
vi.mock("./reply/agent-runner.runtime.js", () => ({
|
||||||
runReplyAgent: async (params: {
|
runReplyAgent: async (params: {
|
||||||
commandBody: string;
|
commandBody: string;
|
||||||
@@ -75,7 +76,10 @@ vi.mock("./reply/agent-runner.runtime.js", () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
installTriggerHandlingE2eTestHooks();
|
let getReplyFromConfig!: GetReplyFromConfig;
|
||||||
|
installTriggerHandlingReplyHarness((impl) => {
|
||||||
|
getReplyFromConfig = impl;
|
||||||
|
});
|
||||||
|
|
||||||
const BASE_MESSAGE = {
|
const BASE_MESSAGE = {
|
||||||
Body: "hello",
|
Body: "hello",
|
||||||
@@ -83,7 +87,7 @@ const BASE_MESSAGE = {
|
|||||||
To: "+2000",
|
To: "+2000",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
function maybeReplyText(reply: Awaited<ReturnType<typeof getReplyFromConfig>>) {
|
function maybeReplyText(reply: Awaited<ReturnType<GetReplyFromConfig>>) {
|
||||||
return Array.isArray(reply) ? reply[0]?.text : reply?.text;
|
return Array.isArray(reply) ? reply[0]?.text : reply?.text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import fs from "node:fs/promises";
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { afterAll, afterEach, beforeAll, expect, vi } from "vitest";
|
import { afterAll, afterEach, beforeAll, expect, vi } from "vitest";
|
||||||
|
import { clearRuntimeAuthProfileStoreSnapshots } from "../agents/auth-profiles.js";
|
||||||
|
import { resetCliCredentialCachesForTest } from "../agents/cli-credentials.js";
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
|
import { resetProviderRuntimeHookCacheForTest } from "../plugins/provider-runtime.js";
|
||||||
|
|
||||||
// Avoid exporting vitest mock types (TS2742 under pnpm + d.ts emit).
|
// Avoid exporting vitest mock types (TS2742 under pnpm + d.ts emit).
|
||||||
// oxlint-disable-next-line typescript/no-explicit-any
|
// oxlint-disable-next-line typescript/no-explicit-any
|
||||||
@@ -107,6 +110,20 @@ const installModelCatalogMock = () =>
|
|||||||
|
|
||||||
installModelCatalogMock();
|
installModelCatalogMock();
|
||||||
|
|
||||||
|
vi.doMock("../agents/model-catalog.runtime.js", () => ({
|
||||||
|
loadModelCatalog: (...args: unknown[]) => modelCatalogMocks.loadModelCatalog(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.doMock("../plugins/provider-runtime.runtime.js", () => ({
|
||||||
|
augmentModelCatalogWithProviderPlugins: async (params: { catalog?: unknown[] }) =>
|
||||||
|
params.catalog ?? [],
|
||||||
|
buildProviderAuthDoctorHintWithPlugin: () => undefined,
|
||||||
|
buildProviderMissingAuthMessageWithPlugin: () => undefined,
|
||||||
|
formatProviderAuthProfileApiKeyWithPlugin: (params: { apiKey?: string }) => params.apiKey,
|
||||||
|
prepareProviderRuntimeAuth: async () => undefined,
|
||||||
|
refreshProviderOAuthCredentialWithPlugin: async () => undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
const modelFallbackMocks = getSharedMocks("openclaw.trigger-handling.model-fallback-mocks", () => ({
|
const modelFallbackMocks = getSharedMocks("openclaw.trigger-handling.model-fallback-mocks", () => ({
|
||||||
runWithModelFallback: vi.fn(
|
runWithModelFallback: vi.fn(
|
||||||
async (params: {
|
async (params: {
|
||||||
@@ -131,6 +148,10 @@ const installModelFallbackMock = () =>
|
|||||||
|
|
||||||
installModelFallbackMock();
|
installModelFallbackMock();
|
||||||
|
|
||||||
|
vi.doMock("../infra/git-commit.js", () => ({
|
||||||
|
resolveCommitHash: vi.fn(() => "abcdef0"),
|
||||||
|
}));
|
||||||
|
|
||||||
const webSessionMocks = getSharedMocks("openclaw.trigger-handling.web-session-mocks", () => ({
|
const webSessionMocks = getSharedMocks("openclaw.trigger-handling.web-session-mocks", () => ({
|
||||||
webAuthExists: vi.fn().mockResolvedValue(true),
|
webAuthExists: vi.fn().mockResolvedValue(true),
|
||||||
getWebAuthAgeMs: vi.fn().mockReturnValue(120_000),
|
getWebAuthAgeMs: vi.fn().mockReturnValue(120_000),
|
||||||
@@ -419,6 +440,9 @@ export async function runGreetingPromptForBareNewOrReset(params: {
|
|||||||
|
|
||||||
export function installTriggerHandlingE2eTestHooks() {
|
export function installTriggerHandlingE2eTestHooks() {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
clearRuntimeAuthProfileStoreSnapshots();
|
||||||
|
resetCliCredentialCachesForTest();
|
||||||
|
resetProviderRuntimeHookCacheForTest();
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,29 @@ import type { CommandContext } from "./commands-types.js";
|
|||||||
import { getFollowupQueueDepth, resolveQueueSettings } from "./queue.js";
|
import { getFollowupQueueDepth, resolveQueueSettings } from "./queue.js";
|
||||||
import { resolveSubagentLabel } from "./subagents-utils.js";
|
import { resolveSubagentLabel } from "./subagents-utils.js";
|
||||||
|
|
||||||
|
// Some usage endpoints only work with CLI/session OAuth tokens, not API keys.
|
||||||
|
// Skip those probes when the active auth mode cannot satisfy the endpoint.
|
||||||
|
const USAGE_OAUTH_ONLY_PROVIDERS = new Set([
|
||||||
|
"anthropic",
|
||||||
|
"github-copilot",
|
||||||
|
"google-gemini-cli",
|
||||||
|
"openai-codex",
|
||||||
|
]);
|
||||||
|
|
||||||
|
function shouldLoadUsageSummary(params: {
|
||||||
|
provider?: string;
|
||||||
|
selectedModelAuth?: string;
|
||||||
|
}): boolean {
|
||||||
|
if (!params.provider) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!USAGE_OAUTH_ONLY_PROVIDERS.has(params.provider)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const auth = params.selectedModelAuth?.trim().toLowerCase();
|
||||||
|
return Boolean(auth?.startsWith("oauth") || auth?.startsWith("token"));
|
||||||
|
}
|
||||||
|
|
||||||
export async function buildStatusReply(params: {
|
export async function buildStatusReply(params: {
|
||||||
cfg: OpenClawConfig;
|
cfg: OpenClawConfig;
|
||||||
command: CommandContext;
|
command: CommandContext;
|
||||||
@@ -78,6 +101,25 @@ export async function buildStatusReply(params: {
|
|||||||
? resolveSessionAgentId({ sessionKey, config: cfg })
|
? resolveSessionAgentId({ sessionKey, config: cfg })
|
||||||
: resolveDefaultAgentId(cfg);
|
: resolveDefaultAgentId(cfg);
|
||||||
const statusAgentDir = resolveAgentDir(cfg, statusAgentId);
|
const statusAgentDir = resolveAgentDir(cfg, statusAgentId);
|
||||||
|
const modelRefs = resolveSelectedAndActiveModel({
|
||||||
|
selectedProvider: provider,
|
||||||
|
selectedModel: model,
|
||||||
|
sessionEntry,
|
||||||
|
});
|
||||||
|
const selectedModelAuth = resolveModelAuthLabel({
|
||||||
|
provider,
|
||||||
|
cfg,
|
||||||
|
sessionEntry,
|
||||||
|
agentDir: statusAgentDir,
|
||||||
|
});
|
||||||
|
const activeModelAuth = modelRefs.activeDiffers
|
||||||
|
? resolveModelAuthLabel({
|
||||||
|
provider: modelRefs.active.provider,
|
||||||
|
cfg,
|
||||||
|
sessionEntry,
|
||||||
|
agentDir: statusAgentDir,
|
||||||
|
})
|
||||||
|
: selectedModelAuth;
|
||||||
const currentUsageProvider = (() => {
|
const currentUsageProvider = (() => {
|
||||||
try {
|
try {
|
||||||
return resolveUsageProviderId(provider);
|
return resolveUsageProviderId(provider);
|
||||||
@@ -86,12 +128,32 @@ export async function buildStatusReply(params: {
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
let usageLine: string | null = null;
|
let usageLine: string | null = null;
|
||||||
if (currentUsageProvider) {
|
if (
|
||||||
|
currentUsageProvider &&
|
||||||
|
shouldLoadUsageSummary({
|
||||||
|
provider: currentUsageProvider,
|
||||||
|
selectedModelAuth,
|
||||||
|
})
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const usageSummary = await loadProviderUsageSummary({
|
const usageSummaryTimeoutMs = 3500;
|
||||||
timeoutMs: 3500,
|
let usageTimeout: NodeJS.Timeout | undefined;
|
||||||
providers: [currentUsageProvider],
|
const usageSummary = await Promise.race([
|
||||||
agentDir: statusAgentDir,
|
loadProviderUsageSummary({
|
||||||
|
timeoutMs: usageSummaryTimeoutMs,
|
||||||
|
providers: [currentUsageProvider],
|
||||||
|
agentDir: statusAgentDir,
|
||||||
|
}),
|
||||||
|
new Promise<never>((_, reject) => {
|
||||||
|
usageTimeout = setTimeout(
|
||||||
|
() => reject(new Error("usage summary timeout")),
|
||||||
|
usageSummaryTimeoutMs,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
]).finally(() => {
|
||||||
|
if (usageTimeout) {
|
||||||
|
clearTimeout(usageTimeout);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
const usageEntry = usageSummary.providers[0];
|
const usageEntry = usageSummary.providers[0];
|
||||||
if (usageEntry && !usageEntry.error && usageEntry.windows.length > 0) {
|
if (usageEntry && !usageEntry.error && usageEntry.windows.length > 0) {
|
||||||
@@ -143,25 +205,6 @@ export async function buildStatusReply(params: {
|
|||||||
const groupActivation = isGroup
|
const groupActivation = isGroup
|
||||||
? (normalizeGroupActivation(sessionEntry?.groupActivation) ?? defaultGroupActivation())
|
? (normalizeGroupActivation(sessionEntry?.groupActivation) ?? defaultGroupActivation())
|
||||||
: undefined;
|
: undefined;
|
||||||
const modelRefs = resolveSelectedAndActiveModel({
|
|
||||||
selectedProvider: provider,
|
|
||||||
selectedModel: model,
|
|
||||||
sessionEntry,
|
|
||||||
});
|
|
||||||
const selectedModelAuth = resolveModelAuthLabel({
|
|
||||||
provider,
|
|
||||||
cfg,
|
|
||||||
sessionEntry,
|
|
||||||
agentDir: statusAgentDir,
|
|
||||||
});
|
|
||||||
const activeModelAuth = modelRefs.activeDiffers
|
|
||||||
? resolveModelAuthLabel({
|
|
||||||
provider: modelRefs.active.provider,
|
|
||||||
cfg,
|
|
||||||
sessionEntry,
|
|
||||||
agentDir: statusAgentDir,
|
|
||||||
})
|
|
||||||
: selectedModelAuth;
|
|
||||||
const agentDefaults = cfg.agents?.defaults ?? {};
|
const agentDefaults = cfg.agents?.defaults ?? {};
|
||||||
const effectiveFastMode =
|
const effectiveFastMode =
|
||||||
resolvedFastMode ??
|
resolvedFastMode ??
|
||||||
|
|||||||
@@ -314,88 +314,100 @@ export async function handleDirectiveOnly(
|
|||||||
directives.elevatedLevel !== undefined &&
|
directives.elevatedLevel !== undefined &&
|
||||||
elevatedEnabled &&
|
elevatedEnabled &&
|
||||||
elevatedAllowed;
|
elevatedAllowed;
|
||||||
|
const shouldPersistSessionEntry =
|
||||||
|
(directives.hasThinkDirective && Boolean(directives.thinkLevel)) ||
|
||||||
|
(directives.hasFastDirective && directives.fastMode !== undefined) ||
|
||||||
|
(directives.hasVerboseDirective && Boolean(directives.verboseLevel)) ||
|
||||||
|
(directives.hasReasoningDirective && Boolean(directives.reasoningLevel)) ||
|
||||||
|
(directives.hasElevatedDirective && Boolean(directives.elevatedLevel)) ||
|
||||||
|
(directives.hasExecDirective && directives.hasExecOptions && allowInternalExecPersistence) ||
|
||||||
|
Boolean(modelSelection) ||
|
||||||
|
directives.hasQueueDirective ||
|
||||||
|
shouldDowngradeXHigh;
|
||||||
const fastModeChanged =
|
const fastModeChanged =
|
||||||
directives.hasFastDirective &&
|
directives.hasFastDirective &&
|
||||||
directives.fastMode !== undefined &&
|
directives.fastMode !== undefined &&
|
||||||
directives.fastMode !== currentFastMode;
|
directives.fastMode !== currentFastMode;
|
||||||
let reasoningChanged =
|
let reasoningChanged =
|
||||||
directives.hasReasoningDirective && directives.reasoningLevel !== undefined;
|
directives.hasReasoningDirective && directives.reasoningLevel !== undefined;
|
||||||
if (directives.hasThinkDirective && directives.thinkLevel) {
|
if (shouldPersistSessionEntry) {
|
||||||
sessionEntry.thinkingLevel = directives.thinkLevel;
|
if (directives.hasThinkDirective && directives.thinkLevel) {
|
||||||
}
|
sessionEntry.thinkingLevel = directives.thinkLevel;
|
||||||
if (directives.hasFastDirective && directives.fastMode !== undefined) {
|
|
||||||
sessionEntry.fastMode = directives.fastMode;
|
|
||||||
}
|
|
||||||
if (shouldDowngradeXHigh) {
|
|
||||||
sessionEntry.thinkingLevel = "high";
|
|
||||||
}
|
|
||||||
if (directives.hasVerboseDirective && directives.verboseLevel) {
|
|
||||||
applyVerboseOverride(sessionEntry, directives.verboseLevel);
|
|
||||||
}
|
|
||||||
if (directives.hasReasoningDirective && directives.reasoningLevel) {
|
|
||||||
if (directives.reasoningLevel === "off") {
|
|
||||||
// Persist explicit off so it overrides model-capability defaults.
|
|
||||||
sessionEntry.reasoningLevel = "off";
|
|
||||||
} else {
|
|
||||||
sessionEntry.reasoningLevel = directives.reasoningLevel;
|
|
||||||
}
|
}
|
||||||
reasoningChanged =
|
if (directives.hasFastDirective && directives.fastMode !== undefined) {
|
||||||
directives.reasoningLevel !== prevReasoningLevel && directives.reasoningLevel !== undefined;
|
sessionEntry.fastMode = directives.fastMode;
|
||||||
}
|
|
||||||
if (directives.hasElevatedDirective && directives.elevatedLevel) {
|
|
||||||
// Unlike other toggles, elevated defaults can be "on".
|
|
||||||
// Persist "off" explicitly so `/elevated off` actually overrides defaults.
|
|
||||||
sessionEntry.elevatedLevel = directives.elevatedLevel;
|
|
||||||
elevatedChanged =
|
|
||||||
elevatedChanged ||
|
|
||||||
(directives.elevatedLevel !== prevElevatedLevel && directives.elevatedLevel !== undefined);
|
|
||||||
}
|
|
||||||
if (directives.hasExecDirective && directives.hasExecOptions && allowInternalExecPersistence) {
|
|
||||||
if (directives.execHost) {
|
|
||||||
sessionEntry.execHost = directives.execHost;
|
|
||||||
}
|
}
|
||||||
if (directives.execSecurity) {
|
if (shouldDowngradeXHigh) {
|
||||||
sessionEntry.execSecurity = directives.execSecurity;
|
sessionEntry.thinkingLevel = "high";
|
||||||
}
|
}
|
||||||
if (directives.execAsk) {
|
if (directives.hasVerboseDirective && directives.verboseLevel) {
|
||||||
sessionEntry.execAsk = directives.execAsk;
|
applyVerboseOverride(sessionEntry, directives.verboseLevel);
|
||||||
}
|
}
|
||||||
if (directives.execNode) {
|
if (directives.hasReasoningDirective && directives.reasoningLevel) {
|
||||||
sessionEntry.execNode = directives.execNode;
|
if (directives.reasoningLevel === "off") {
|
||||||
|
// Persist explicit off so it overrides model-capability defaults.
|
||||||
|
sessionEntry.reasoningLevel = "off";
|
||||||
|
} else {
|
||||||
|
sessionEntry.reasoningLevel = directives.reasoningLevel;
|
||||||
|
}
|
||||||
|
reasoningChanged =
|
||||||
|
directives.reasoningLevel !== prevReasoningLevel && directives.reasoningLevel !== undefined;
|
||||||
}
|
}
|
||||||
}
|
if (directives.hasElevatedDirective && directives.elevatedLevel) {
|
||||||
if (modelSelection) {
|
// Unlike other toggles, elevated defaults can be "on".
|
||||||
applyModelOverrideToSessionEntry({
|
// Persist "off" explicitly so `/elevated off` actually overrides defaults.
|
||||||
entry: sessionEntry,
|
sessionEntry.elevatedLevel = directives.elevatedLevel;
|
||||||
selection: modelSelection,
|
elevatedChanged =
|
||||||
profileOverride,
|
elevatedChanged ||
|
||||||
});
|
(directives.elevatedLevel !== prevElevatedLevel && directives.elevatedLevel !== undefined);
|
||||||
}
|
|
||||||
if (directives.hasQueueDirective && directives.queueReset) {
|
|
||||||
delete sessionEntry.queueMode;
|
|
||||||
delete sessionEntry.queueDebounceMs;
|
|
||||||
delete sessionEntry.queueCap;
|
|
||||||
delete sessionEntry.queueDrop;
|
|
||||||
} else if (directives.hasQueueDirective) {
|
|
||||||
if (directives.queueMode) {
|
|
||||||
sessionEntry.queueMode = directives.queueMode;
|
|
||||||
}
|
}
|
||||||
if (typeof directives.debounceMs === "number") {
|
if (directives.hasExecDirective && directives.hasExecOptions && allowInternalExecPersistence) {
|
||||||
sessionEntry.queueDebounceMs = directives.debounceMs;
|
if (directives.execHost) {
|
||||||
|
sessionEntry.execHost = directives.execHost;
|
||||||
|
}
|
||||||
|
if (directives.execSecurity) {
|
||||||
|
sessionEntry.execSecurity = directives.execSecurity;
|
||||||
|
}
|
||||||
|
if (directives.execAsk) {
|
||||||
|
sessionEntry.execAsk = directives.execAsk;
|
||||||
|
}
|
||||||
|
if (directives.execNode) {
|
||||||
|
sessionEntry.execNode = directives.execNode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (typeof directives.cap === "number") {
|
if (modelSelection) {
|
||||||
sessionEntry.queueCap = directives.cap;
|
applyModelOverrideToSessionEntry({
|
||||||
|
entry: sessionEntry,
|
||||||
|
selection: modelSelection,
|
||||||
|
profileOverride,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (directives.dropPolicy) {
|
if (directives.hasQueueDirective && directives.queueReset) {
|
||||||
sessionEntry.queueDrop = directives.dropPolicy;
|
delete sessionEntry.queueMode;
|
||||||
|
delete sessionEntry.queueDebounceMs;
|
||||||
|
delete sessionEntry.queueCap;
|
||||||
|
delete sessionEntry.queueDrop;
|
||||||
|
} else if (directives.hasQueueDirective) {
|
||||||
|
if (directives.queueMode) {
|
||||||
|
sessionEntry.queueMode = directives.queueMode;
|
||||||
|
}
|
||||||
|
if (typeof directives.debounceMs === "number") {
|
||||||
|
sessionEntry.queueDebounceMs = directives.debounceMs;
|
||||||
|
}
|
||||||
|
if (typeof directives.cap === "number") {
|
||||||
|
sessionEntry.queueCap = directives.cap;
|
||||||
|
}
|
||||||
|
if (directives.dropPolicy) {
|
||||||
|
sessionEntry.queueDrop = directives.dropPolicy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sessionEntry.updatedAt = Date.now();
|
||||||
|
sessionStore[sessionKey] = sessionEntry;
|
||||||
|
if (storePath) {
|
||||||
|
await updateSessionStore(storePath, (store) => {
|
||||||
|
store[sessionKey] = sessionEntry;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
sessionEntry.updatedAt = Date.now();
|
|
||||||
sessionStore[sessionKey] = sessionEntry;
|
|
||||||
if (storePath) {
|
|
||||||
await updateSessionStore(storePath, (store) => {
|
|
||||||
store[sessionKey] = sessionEntry;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (modelSelection) {
|
if (modelSelection) {
|
||||||
const nextLabel = `${modelSelection.provider}/${modelSelection.model}`;
|
const nextLabel = `${modelSelection.provider}/${modelSelection.model}`;
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ function createToolHandlerCtx(params: {
|
|||||||
sessionId: params.sessionId,
|
sessionId: params.sessionId,
|
||||||
onBlockReplyFlush: params.onBlockReplyFlush,
|
onBlockReplyFlush: params.onBlockReplyFlush,
|
||||||
},
|
},
|
||||||
|
hookRunner: hookMocks.runner,
|
||||||
state: {
|
state: {
|
||||||
toolMetaById: new Map<string, string | undefined>(),
|
toolMetaById: new Map<string, string | undefined>(),
|
||||||
...createBaseToolHandlerState(),
|
...createBaseToolHandlerState(),
|
||||||
|
|||||||
Reference in New Issue
Block a user