mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
test: eliminate unchecked TypeScript casts (#105804)
This commit is contained in:
committed by
GitHub
parent
f9d7aa286e
commit
774d5249cc
@@ -65,7 +65,7 @@ export function makeBrowserServerState(params?: {
|
||||
},
|
||||
};
|
||||
return {
|
||||
server: null as any,
|
||||
server: null as unknown as BrowserServerState["server"],
|
||||
port: 0,
|
||||
resolved: {
|
||||
...resolvedBase,
|
||||
|
||||
+156
-108
@@ -1,5 +1,11 @@
|
||||
// Copilot tests cover harness plugin behavior.
|
||||
import type { CopilotClient } from "@github/copilot-sdk";
|
||||
import { attachModelProviderRequestTransport } from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import type {
|
||||
AgentHarnessAttemptParams,
|
||||
AgentHarnessAttemptResult,
|
||||
AgentHarnessCompactParams,
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import {
|
||||
initializeGlobalHookRunner,
|
||||
resetGlobalHookRunner,
|
||||
@@ -8,7 +14,9 @@ import { createMockPluginRegistry } from "openclaw/plugin-sdk/plugin-test-runtim
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { CopilotClientPool } from "./harness.js";
|
||||
import { createCopilotAgentHarness, type CopilotSessionBinding } from "./harness.js";
|
||||
import type { resolvePoolAcquire } from "./src/attempt.js";
|
||||
import { COPILOT_BYOK_PROVIDER_ERROR } from "./src/provider-bridge.js";
|
||||
import type { PoolKey } from "./src/runtime.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
runCopilotAttempt: vi.fn(),
|
||||
@@ -22,7 +30,8 @@ const mocks = vi.hoisted(() => ({
|
||||
},
|
||||
key: { agentId: "test", authMode: "useLoggedInUser", copilotHome: "/tmp/copilot" },
|
||||
options: { copilotHome: "/tmp/copilot", useLoggedInUser: true },
|
||||
}) as any,
|
||||
provider: { mode: "github-copilot" },
|
||||
}) as ReturnType<typeof resolvePoolAcquire>,
|
||||
),
|
||||
createCopilotByokProxy: vi.fn(),
|
||||
createCopilotClientPool: vi.fn(),
|
||||
@@ -41,8 +50,24 @@ vi.mock("./src/runtime.js", () => ({
|
||||
createCopilotClientPool: mocks.createCopilotClientPool,
|
||||
}));
|
||||
|
||||
const ATTEMPT_PARAMS = { provider: "github-copilot", model: "gpt-4.1" } as any;
|
||||
const ATTEMPT_RESULT = { ok: true } as any;
|
||||
function asAttemptParams(value: Record<string, unknown>): AgentHarnessAttemptParams {
|
||||
return value as unknown as AgentHarnessAttemptParams;
|
||||
}
|
||||
|
||||
function asAttemptResult(value: Record<string, unknown>): AgentHarnessAttemptResult {
|
||||
return value as unknown as AgentHarnessAttemptResult;
|
||||
}
|
||||
|
||||
const ATTEMPT_PARAMS = asAttemptParams({
|
||||
provider: "github-copilot",
|
||||
model: "gpt-4.1",
|
||||
});
|
||||
const ATTEMPT_RESULT = asAttemptResult({ ok: true });
|
||||
const TEST_POOL_KEY = {
|
||||
agentId: "test",
|
||||
authMode: "useLoggedInUser",
|
||||
copilotHome: "/tmp/copilot",
|
||||
} satisfies PoolKey;
|
||||
const TEST_SESSION_CONFIG = {
|
||||
availableTools: [],
|
||||
model: "gpt-4.1",
|
||||
@@ -50,6 +75,10 @@ const TEST_SESSION_CONFIG = {
|
||||
workingDirectory: "/workspace",
|
||||
};
|
||||
|
||||
function createMockCopilotClient(overrides: Record<string, unknown> = {}): CopilotClient {
|
||||
return overrides as unknown as CopilotClient;
|
||||
}
|
||||
|
||||
function makePoolMock() {
|
||||
return {
|
||||
acquire: vi.fn(),
|
||||
@@ -104,6 +133,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
},
|
||||
key: { agentId: "test", authMode: "useLoggedInUser", copilotHome: "/tmp/copilot" },
|
||||
options: { copilotHome: "/tmp/copilot", useLoggedInUser: true },
|
||||
provider: { mode: "github-copilot" },
|
||||
});
|
||||
mocks.createCopilotClientPool.mockImplementation(() => makePoolMock());
|
||||
mocks.createCopilotByokProxy.mockResolvedValue(undefined);
|
||||
@@ -184,7 +214,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
harness.supports({
|
||||
provider: "github-copilot",
|
||||
modelId: "gpt-4.1",
|
||||
requestedRuntime: " COPILOT " as any,
|
||||
requestedRuntime: " COPILOT ",
|
||||
}),
|
||||
).toEqual({ supported: true, priority: 100 });
|
||||
});
|
||||
@@ -321,8 +351,8 @@ describe("createCopilotAgentHarness", () => {
|
||||
|
||||
it("runAttempt creates one pool lazily and reuses it across two attempts on the same harness", async () => {
|
||||
const pool = makePoolMock();
|
||||
const firstResult = { attempt: 1 } as any;
|
||||
const secondResult = { attempt: 2 } as any;
|
||||
const firstResult = asAttemptResult({ attempt: 1 });
|
||||
const secondResult = asAttemptResult({ attempt: 2 });
|
||||
mocks.createCopilotClientPool.mockReturnValue(pool);
|
||||
mocks.runCopilotAttempt.mockResolvedValueOnce(firstResult).mockResolvedValueOnce(secondResult);
|
||||
const harness = createCopilotAgentHarness();
|
||||
@@ -368,8 +398,8 @@ describe("createCopilotAgentHarness", () => {
|
||||
|
||||
it("runAttempt does not serialize concurrent attempts", async () => {
|
||||
const pool = makePoolMock();
|
||||
const firstResult = { attempt: 1 } as any;
|
||||
const secondResult = { attempt: 2 } as any;
|
||||
const firstResult = asAttemptResult({ attempt: 1 });
|
||||
const secondResult = asAttemptResult({ attempt: 2 });
|
||||
mocks.createCopilotClientPool.mockReturnValue(pool);
|
||||
mocks.runCopilotAttempt.mockResolvedValueOnce(firstResult).mockResolvedValueOnce(secondResult);
|
||||
const harness = createCopilotAgentHarness();
|
||||
@@ -420,7 +450,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
|
||||
it("dispose waits for in-flight runAttempt before disposing", async () => {
|
||||
const pool = makePoolMock();
|
||||
const deferred = createDeferred<any>();
|
||||
const deferred = createDeferred<AgentHarnessAttemptResult>();
|
||||
mocks.createCopilotClientPool.mockReturnValue(pool);
|
||||
mocks.runCopilotAttempt.mockImplementation(() => deferred.promise);
|
||||
const harness = createCopilotAgentHarness();
|
||||
@@ -517,11 +547,11 @@ describe("createCopilotAgentHarness", () => {
|
||||
it("calls deleteSession on the client that created the session", async () => {
|
||||
const pool = makePoolMock();
|
||||
const deleteSession = vi.fn().mockResolvedValue(undefined);
|
||||
const client = { deleteSession } as any;
|
||||
const client = createMockCopilotClient({ deleteSession });
|
||||
mocks.runCopilotAttempt.mockImplementation(async (params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-123",
|
||||
pooledClient: { key: {} as any, client },
|
||||
pooledClient: { key: TEST_POOL_KEY, client },
|
||||
});
|
||||
return ATTEMPT_RESULT;
|
||||
});
|
||||
@@ -549,11 +579,11 @@ describe("createCopilotAgentHarness", () => {
|
||||
it("swallows errors thrown by client.deleteSession", async () => {
|
||||
const pool = makePoolMock();
|
||||
const deleteSession = vi.fn().mockRejectedValue(new Error("session not found"));
|
||||
const client = { deleteSession } as any;
|
||||
const client = createMockCopilotClient({ deleteSession });
|
||||
mocks.runCopilotAttempt.mockImplementation(async (params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-err",
|
||||
pooledClient: { key: {} as any, client },
|
||||
pooledClient: { key: TEST_POOL_KEY, client },
|
||||
});
|
||||
return ATTEMPT_RESULT;
|
||||
});
|
||||
@@ -568,11 +598,11 @@ describe("createCopilotAgentHarness", () => {
|
||||
it("forgets the session after reset; a second reset is a no-op", async () => {
|
||||
const pool = makePoolMock();
|
||||
const deleteSession = vi.fn().mockResolvedValue(undefined);
|
||||
const client = { deleteSession } as any;
|
||||
const client = createMockCopilotClient({ deleteSession });
|
||||
mocks.runCopilotAttempt.mockImplementation(async (params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-x",
|
||||
pooledClient: { key: {} as any, client },
|
||||
pooledClient: { key: TEST_POOL_KEY, client },
|
||||
});
|
||||
return ATTEMPT_RESULT;
|
||||
});
|
||||
@@ -588,11 +618,11 @@ describe("createCopilotAgentHarness", () => {
|
||||
it("does not invoke deleteSession for a session belonging to a different openclawSessionId", async () => {
|
||||
const pool = makePoolMock();
|
||||
const deleteSession = vi.fn().mockResolvedValue(undefined);
|
||||
const client = { deleteSession } as any;
|
||||
const client = createMockCopilotClient({ deleteSession });
|
||||
mocks.runCopilotAttempt.mockImplementation(async (params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-y",
|
||||
pooledClient: { key: {} as any, client },
|
||||
pooledClient: { key: TEST_POOL_KEY, client },
|
||||
});
|
||||
return ATTEMPT_RESULT;
|
||||
});
|
||||
@@ -608,11 +638,11 @@ describe("createCopilotAgentHarness", () => {
|
||||
it("dispose clears tracked sessions so subsequent reset is a no-op", async () => {
|
||||
const pool = makePoolMock();
|
||||
const deleteSession = vi.fn().mockResolvedValue(undefined);
|
||||
const client = { deleteSession } as any;
|
||||
const client = createMockCopilotClient({ deleteSession });
|
||||
mocks.runCopilotAttempt.mockImplementation(async (params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-d",
|
||||
pooledClient: { key: {} as any, client },
|
||||
pooledClient: { key: TEST_POOL_KEY, client },
|
||||
});
|
||||
return ATTEMPT_RESULT;
|
||||
});
|
||||
@@ -631,7 +661,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
mocks.runCopilotAttempt.mockImplementation(async (_params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-pending-cleanup",
|
||||
pooledClient: { key: {} as any, client: {} as any },
|
||||
pooledClient: { key: TEST_POOL_KEY, client: createMockCopilotClient() },
|
||||
sessionConfig: TEST_SESSION_CONFIG,
|
||||
});
|
||||
deps.onDeferredCompaction?.({
|
||||
@@ -655,7 +685,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
mocks.runCopilotAttempt.mockImplementation(async (_params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-reset-cleanup",
|
||||
pooledClient: { key: {} as any, client: {} as any },
|
||||
pooledClient: { key: TEST_POOL_KEY, client: createMockCopilotClient() },
|
||||
sessionConfig: TEST_SESSION_CONFIG,
|
||||
});
|
||||
deps.onDeferredCompaction?.({
|
||||
@@ -685,7 +715,10 @@ describe("createCopilotAgentHarness", () => {
|
||||
if (attempt === 1) {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-before-reset",
|
||||
pooledClient: { key: {} as any, client: { deleteSession: oldDeleteSession } as any },
|
||||
pooledClient: {
|
||||
key: TEST_POOL_KEY,
|
||||
client: createMockCopilotClient({ deleteSession: oldDeleteSession }),
|
||||
},
|
||||
sessionConfig: TEST_SESSION_CONFIG,
|
||||
});
|
||||
deps.onDeferredCompaction?.({
|
||||
@@ -697,8 +730,8 @@ describe("createCopilotAgentHarness", () => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-replacement",
|
||||
pooledClient: {
|
||||
key: {} as any,
|
||||
client: { deleteSession: replacementDeleteSession } as any,
|
||||
key: TEST_POOL_KEY,
|
||||
client: createMockCopilotClient({ deleteSession: replacementDeleteSession }),
|
||||
},
|
||||
sessionConfig: TEST_SESSION_CONFIG,
|
||||
});
|
||||
@@ -734,7 +767,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
if (attempt === 1) {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-before-reset",
|
||||
pooledClient: { key: {} as any, client: {} as any },
|
||||
pooledClient: { key: TEST_POOL_KEY, client: createMockCopilotClient() },
|
||||
sessionConfig: TEST_SESSION_CONFIG,
|
||||
});
|
||||
deps.onDeferredCompaction?.({
|
||||
@@ -746,8 +779,8 @@ describe("createCopilotAgentHarness", () => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-replacement",
|
||||
pooledClient: {
|
||||
key: {} as any,
|
||||
client: { deleteSession: replacementDeleteSession } as any,
|
||||
key: TEST_POOL_KEY,
|
||||
client: createMockCopilotClient({ deleteSession: replacementDeleteSession }),
|
||||
},
|
||||
sessionConfig: TEST_SESSION_CONFIG,
|
||||
});
|
||||
@@ -755,8 +788,8 @@ describe("createCopilotAgentHarness", () => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-during-reset",
|
||||
pooledClient: {
|
||||
key: {} as any,
|
||||
client: { deleteSession: duringResetDeleteSession } as any,
|
||||
key: TEST_POOL_KEY,
|
||||
client: createMockCopilotClient({ deleteSession: duringResetDeleteSession }),
|
||||
},
|
||||
sessionConfig: TEST_SESSION_CONFIG,
|
||||
});
|
||||
@@ -810,11 +843,11 @@ describe("createCopilotAgentHarness", () => {
|
||||
|
||||
it("seeds initialReplayState.sdkSessionId from trackedSessions on the second turn", async () => {
|
||||
const pool = makePoolMock();
|
||||
const client = { deleteSession: vi.fn() } as any;
|
||||
const client = createMockCopilotClient({ deleteSession: vi.fn() });
|
||||
mocks.runCopilotAttempt.mockImplementation(async (_params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-warm",
|
||||
pooledClient: { key: {} as any, client },
|
||||
pooledClient: { key: TEST_POOL_KEY, client },
|
||||
});
|
||||
return ATTEMPT_RESULT;
|
||||
});
|
||||
@@ -842,7 +875,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
if (attempt === 1) {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-compacting",
|
||||
pooledClient: { key: {} as any, client: {} as any },
|
||||
pooledClient: { key: TEST_POOL_KEY, client: createMockCopilotClient() },
|
||||
sessionConfig: TEST_SESSION_CONFIG,
|
||||
});
|
||||
deps.onDeferredCompaction?.({
|
||||
@@ -882,7 +915,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
if (attempt === 1) {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-old",
|
||||
pooledClient: { key: {} as any, client: {} as any },
|
||||
pooledClient: { key: TEST_POOL_KEY, client: createMockCopilotClient() },
|
||||
sessionConfig: TEST_SESSION_CONFIG,
|
||||
});
|
||||
deps.onDeferredCompaction?.({
|
||||
@@ -893,7 +926,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
} else if (attempt === 2) {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-replacement",
|
||||
pooledClient: { key: {} as any, client: {} as any },
|
||||
pooledClient: { key: TEST_POOL_KEY, client: createMockCopilotClient() },
|
||||
sessionConfig: TEST_SESSION_CONFIG,
|
||||
});
|
||||
}
|
||||
@@ -923,7 +956,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
if (attempt === 1) {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-cancelled",
|
||||
pooledClient: { key: {} as any, client: {} as any },
|
||||
pooledClient: { key: TEST_POOL_KEY, client: createMockCopilotClient() },
|
||||
sessionConfig: TEST_SESSION_CONFIG,
|
||||
});
|
||||
deps.onDeferredCompaction?.({
|
||||
@@ -966,7 +999,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
if (attempt === 1) {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-stale",
|
||||
pooledClient: { key: {} as any, client: {} as any },
|
||||
pooledClient: { key: TEST_POOL_KEY, client: createMockCopilotClient() },
|
||||
sessionConfig: TEST_SESSION_CONFIG,
|
||||
});
|
||||
firstAttemptDeps = deps;
|
||||
@@ -974,7 +1007,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
} else if (attempt === 2) {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-current",
|
||||
pooledClient: { key: {} as any, client: {} as any },
|
||||
pooledClient: { key: TEST_POOL_KEY, client: createMockCopilotClient() },
|
||||
sessionConfig: TEST_SESSION_CONFIG,
|
||||
});
|
||||
}
|
||||
@@ -1008,7 +1041,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
mocks.runCopilotAttempt.mockImplementation(async (_params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-cold",
|
||||
pooledClient: { key: {} as any, client: {} as any },
|
||||
pooledClient: { key: TEST_POOL_KEY, client: createMockCopilotClient() },
|
||||
});
|
||||
return ATTEMPT_RESULT;
|
||||
});
|
||||
@@ -1027,7 +1060,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
mocks.runCopilotAttempt.mockImplementation(async (_params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-gpt4",
|
||||
pooledClient: { key: {} as any, client: {} as any },
|
||||
pooledClient: { key: TEST_POOL_KEY, client: createMockCopilotClient() },
|
||||
});
|
||||
return ATTEMPT_RESULT;
|
||||
});
|
||||
@@ -1054,7 +1087,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
mocks.runCopilotAttempt.mockImplementation(async (_params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-api",
|
||||
pooledClient: { key: {} as any, client: {} as any },
|
||||
pooledClient: { key: TEST_POOL_KEY, client: createMockCopilotClient() },
|
||||
});
|
||||
return ATTEMPT_RESULT;
|
||||
});
|
||||
@@ -1084,7 +1117,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
mocks.runCopilotAttempt.mockImplementation(async (_params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-auth1",
|
||||
pooledClient: { key: {} as any, client: {} as any },
|
||||
pooledClient: { key: TEST_POOL_KEY, client: createMockCopilotClient() },
|
||||
});
|
||||
return ATTEMPT_RESULT;
|
||||
});
|
||||
@@ -1125,7 +1158,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
mocks.runCopilotAttempt.mockImplementation(async (_params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-p1",
|
||||
pooledClient: { key: {} as any, client: {} as any },
|
||||
pooledClient: { key: TEST_POOL_KEY, client: createMockCopilotClient() },
|
||||
});
|
||||
return ATTEMPT_RESULT;
|
||||
});
|
||||
@@ -1165,7 +1198,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
mocks.runCopilotAttempt.mockImplementation(async (_params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-tok1",
|
||||
pooledClient: { key: {} as any, client: {} as any },
|
||||
pooledClient: { key: TEST_POOL_KEY, client: createMockCopilotClient() },
|
||||
});
|
||||
return ATTEMPT_RESULT;
|
||||
});
|
||||
@@ -1199,7 +1232,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
mocks.runCopilotAttempt.mockImplementation(async (_params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-tracked",
|
||||
pooledClient: { key: {} as any, client: {} as any },
|
||||
pooledClient: { key: TEST_POOL_KEY, client: createMockCopilotClient() },
|
||||
});
|
||||
return ATTEMPT_RESULT;
|
||||
});
|
||||
@@ -1225,12 +1258,12 @@ describe("createCopilotAgentHarness", () => {
|
||||
it("updates the tracked session when onSessionEstablished reports a new sdkSessionId", async () => {
|
||||
const pool = makePoolMock();
|
||||
const deleteSession = vi.fn();
|
||||
const client = { deleteSession } as any;
|
||||
const client = createMockCopilotClient({ deleteSession });
|
||||
let nextSdkId = "sdk-sess-1";
|
||||
mocks.runCopilotAttempt.mockImplementation(async (_params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: nextSdkId,
|
||||
pooledClient: { key: {} as any, client },
|
||||
pooledClient: { key: TEST_POOL_KEY, client },
|
||||
});
|
||||
return ATTEMPT_RESULT;
|
||||
});
|
||||
@@ -1254,7 +1287,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
mocks.runCopilotAttempt.mockImplementation(async (_params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-sqlite",
|
||||
pooledClient: { key: {} as any, client: {} as any },
|
||||
pooledClient: { key: TEST_POOL_KEY, client: createMockCopilotClient() },
|
||||
});
|
||||
return ATTEMPT_RESULT;
|
||||
});
|
||||
@@ -1288,7 +1321,10 @@ describe("createCopilotAgentHarness", () => {
|
||||
mocks.runCopilotAttempt.mockImplementation(async (_params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-byok",
|
||||
pooledClient: { key: {} as any, client: { deleteSession: vi.fn() } as any },
|
||||
pooledClient: {
|
||||
key: TEST_POOL_KEY,
|
||||
client: createMockCopilotClient({ deleteSession: vi.fn() }),
|
||||
},
|
||||
sessionConfig: TEST_SESSION_CONFIG,
|
||||
});
|
||||
return ATTEMPT_RESULT;
|
||||
@@ -1330,7 +1366,10 @@ describe("createCopilotAgentHarness", () => {
|
||||
mocks.runCopilotAttempt.mockImplementation(async (_params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-byok",
|
||||
pooledClient: { key: {} as any, client: { deleteSession: vi.fn() } as any },
|
||||
pooledClient: {
|
||||
key: TEST_POOL_KEY,
|
||||
client: createMockCopilotClient({ deleteSession: vi.fn() }),
|
||||
},
|
||||
sessionConfig: TEST_SESSION_CONFIG,
|
||||
});
|
||||
return ATTEMPT_RESULT;
|
||||
@@ -1370,7 +1409,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
mocks.runCopilotAttempt.mockImplementation(async (_params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-current",
|
||||
pooledClient: { key: {} as any, client: {} as any },
|
||||
pooledClient: { key: TEST_POOL_KEY, client: createMockCopilotClient() },
|
||||
});
|
||||
return ATTEMPT_RESULT;
|
||||
});
|
||||
@@ -1442,7 +1481,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
mocks.runCopilotAttempt.mockImplementation(async (_params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-memory-only",
|
||||
pooledClient: { key: {} as any, client: {} as any },
|
||||
pooledClient: { key: TEST_POOL_KEY, client: createMockCopilotClient() },
|
||||
});
|
||||
return ATTEMPT_RESULT;
|
||||
});
|
||||
@@ -1467,7 +1506,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
mocks.runCopilotAttempt.mockImplementation(async (_params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-old-model",
|
||||
pooledClient: { key: {} as any, client: {} as any },
|
||||
pooledClient: { key: TEST_POOL_KEY, client: createMockCopilotClient() },
|
||||
});
|
||||
return ATTEMPT_RESULT;
|
||||
});
|
||||
@@ -1501,7 +1540,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
mocks.runCopilotAttempt.mockImplementation(async (_params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-main-home",
|
||||
pooledClient: { key: {} as any, client: {} as any },
|
||||
pooledClient: { key: TEST_POOL_KEY, client: createMockCopilotClient() },
|
||||
});
|
||||
return ATTEMPT_RESULT;
|
||||
});
|
||||
@@ -1544,7 +1583,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
mocks.runCopilotAttempt.mockImplementation(async (_params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-tracked-model",
|
||||
pooledClient: { key: {} as any, client: {} as any },
|
||||
pooledClient: { key: TEST_POOL_KEY, client: createMockCopilotClient() },
|
||||
});
|
||||
return ATTEMPT_RESULT;
|
||||
});
|
||||
@@ -1606,7 +1645,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
mocks.runCopilotAttempt.mockImplementation(async (_params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-reset",
|
||||
pooledClient: { key: {} as any, client: { deleteSession } as any },
|
||||
pooledClient: { key: TEST_POOL_KEY, client: createMockCopilotClient({ deleteSession }) },
|
||||
});
|
||||
return ATTEMPT_RESULT;
|
||||
});
|
||||
@@ -1626,7 +1665,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
mocks.runCopilotAttempt.mockImplementationOnce(async (_params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-before-reset",
|
||||
pooledClient: { key: {} as any, client: {} as any },
|
||||
pooledClient: { key: TEST_POOL_KEY, client: createMockCopilotClient() },
|
||||
});
|
||||
return ATTEMPT_RESULT;
|
||||
});
|
||||
@@ -1674,7 +1713,9 @@ describe("createCopilotAgentHarness", () => {
|
||||
|
||||
it("returns ok:false when sessionId is missing", async () => {
|
||||
const harness = createCopilotAgentHarness({ pool: makePoolMock() });
|
||||
const result = await harness.compact?.({ workspaceDir: "/ws" } as any);
|
||||
const result = await harness.compact?.({
|
||||
workspaceDir: "/ws",
|
||||
} as AgentHarnessCompactParams);
|
||||
expect(result).toEqual({
|
||||
ok: false,
|
||||
compacted: false,
|
||||
@@ -1688,7 +1729,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
sessionId: "oc-sess-compact-1",
|
||||
trigger: "budget",
|
||||
currentTokenCount: 12345,
|
||||
} as any);
|
||||
} as AgentHarnessCompactParams);
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: false,
|
||||
@@ -1704,7 +1745,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
mocks.runCopilotAttempt.mockImplementation(async (_params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-background",
|
||||
pooledClient: { key: {} as any, client: {} as any },
|
||||
pooledClient: { key: TEST_POOL_KEY, client: createMockCopilotClient() },
|
||||
sessionConfig: TEST_SESSION_CONFIG,
|
||||
});
|
||||
deps.onDeferredCompaction?.({
|
||||
@@ -1743,7 +1784,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
: "sdk-sess-replacement";
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId,
|
||||
pooledClient: { key: {} as any, client: {} as any },
|
||||
pooledClient: { key: TEST_POOL_KEY, client: createMockCopilotClient() },
|
||||
sessionConfig: TEST_SESSION_CONFIG,
|
||||
});
|
||||
if (sdkSessionId === "sdk-sess-background") {
|
||||
@@ -1804,8 +1845,8 @@ describe("createCopilotAgentHarness", () => {
|
||||
}));
|
||||
const pool = makePoolMock();
|
||||
pool.acquire = vi.fn(async () => ({
|
||||
key: {} as any,
|
||||
client: { deleteSession: vi.fn(), resumeSession } as any,
|
||||
key: TEST_POOL_KEY,
|
||||
client: createMockCopilotClient({ deleteSession: vi.fn(), resumeSession }),
|
||||
}));
|
||||
const release = vi.fn(async () => undefined);
|
||||
pool.release = release;
|
||||
@@ -1813,8 +1854,8 @@ describe("createCopilotAgentHarness", () => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-compact",
|
||||
pooledClient: {
|
||||
key: {} as any,
|
||||
client: { deleteSession: vi.fn(), resumeSession } as any,
|
||||
key: TEST_POOL_KEY,
|
||||
client: createMockCopilotClient({ deleteSession: vi.fn(), resumeSession }),
|
||||
},
|
||||
sessionConfig: TEST_SESSION_CONFIG,
|
||||
});
|
||||
@@ -1909,8 +1950,8 @@ describe("createCopilotAgentHarness", () => {
|
||||
});
|
||||
const pool = makePoolMock();
|
||||
pool.acquire = vi.fn(async () => ({
|
||||
key: {} as any,
|
||||
client: { deleteSession: vi.fn(), resumeSession } as any,
|
||||
key: TEST_POOL_KEY,
|
||||
client: createMockCopilotClient({ deleteSession: vi.fn(), resumeSession }),
|
||||
}));
|
||||
const release = vi.fn(async () => undefined);
|
||||
pool.release = release;
|
||||
@@ -1918,8 +1959,8 @@ describe("createCopilotAgentHarness", () => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-abort",
|
||||
pooledClient: {
|
||||
key: {} as any,
|
||||
client: { deleteSession: vi.fn(), resumeSession } as any,
|
||||
key: TEST_POOL_KEY,
|
||||
client: createMockCopilotClient({ deleteSession: vi.fn(), resumeSession }),
|
||||
},
|
||||
sessionConfig: TEST_SESSION_CONFIG,
|
||||
});
|
||||
@@ -1969,8 +2010,8 @@ describe("createCopilotAgentHarness", () => {
|
||||
}));
|
||||
const pool = makePoolMock();
|
||||
const acquire = vi.fn(async () => ({
|
||||
key: {} as any,
|
||||
client: { deleteSession: vi.fn(), resumeSession } as any,
|
||||
key: TEST_POOL_KEY,
|
||||
client: createMockCopilotClient({ deleteSession: vi.fn(), resumeSession }),
|
||||
}));
|
||||
pool.acquire = acquire;
|
||||
pool.release = vi.fn(async () => undefined);
|
||||
@@ -1986,6 +2027,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
},
|
||||
key: { agentId: "test", authMode: "gitHubToken", copilotHome: "/copilot-home" },
|
||||
options: { copilotHome: "/copilot-home", gitHubToken: "ghp_test" },
|
||||
provider: { mode: "github-copilot" },
|
||||
})
|
||||
.mockReturnValueOnce({
|
||||
auth: {
|
||||
@@ -1995,6 +2037,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
},
|
||||
key: { agentId: "test", authMode: "useLoggedInUser", copilotHome: "/copilot-home" },
|
||||
options: { copilotHome: "/copilot-home", useLoggedInUser: true },
|
||||
provider: { mode: "github-copilot" },
|
||||
})
|
||||
.mockReturnValueOnce({
|
||||
auth: {
|
||||
@@ -2007,13 +2050,14 @@ describe("createCopilotAgentHarness", () => {
|
||||
},
|
||||
key: { agentId: "test", authMode: "gitHubToken", copilotHome: "/copilot-home" },
|
||||
options: { copilotHome: "/copilot-home", gitHubToken: "ghp_test" },
|
||||
provider: { mode: "github-copilot" },
|
||||
});
|
||||
mocks.runCopilotAttempt.mockImplementation(async (_params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-token",
|
||||
pooledClient: {
|
||||
key: {} as any,
|
||||
client: { deleteSession: vi.fn(), resumeSession } as any,
|
||||
key: TEST_POOL_KEY,
|
||||
client: createMockCopilotClient({ deleteSession: vi.fn(), resumeSession }),
|
||||
},
|
||||
sessionConfig: TEST_SESSION_CONFIG,
|
||||
});
|
||||
@@ -2077,8 +2121,8 @@ describe("createCopilotAgentHarness", () => {
|
||||
}));
|
||||
const pool = makePoolMock();
|
||||
const acquire = vi.fn(async () => ({
|
||||
key: {} as any,
|
||||
client: { deleteSession: vi.fn(), resumeSession } as any,
|
||||
key: TEST_POOL_KEY,
|
||||
client: createMockCopilotClient({ deleteSession: vi.fn(), resumeSession }),
|
||||
}));
|
||||
pool.acquire = acquire;
|
||||
pool.release = vi.fn(async () => undefined);
|
||||
@@ -2106,6 +2150,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
},
|
||||
key: { agentId: "test", authMode: "byok", copilotHome: "/copilot-home" },
|
||||
options: { copilotHome: "/copilot-home" },
|
||||
provider: { mode: "byok" },
|
||||
};
|
||||
});
|
||||
const closeByokProxy = vi.fn(async () => undefined);
|
||||
@@ -2134,8 +2179,8 @@ describe("createCopilotAgentHarness", () => {
|
||||
},
|
||||
sdkSessionId: "sdk-sess-byok",
|
||||
pooledClient: {
|
||||
key: {} as any,
|
||||
client: { deleteSession: vi.fn(), resumeSession } as any,
|
||||
key: TEST_POOL_KEY,
|
||||
client: createMockCopilotClient({ deleteSession: vi.fn(), resumeSession }),
|
||||
},
|
||||
sessionConfig: TEST_SESSION_CONFIG,
|
||||
});
|
||||
@@ -2211,16 +2256,16 @@ describe("createCopilotAgentHarness", () => {
|
||||
const resumeSession = vi.fn();
|
||||
const pool = makePoolMock();
|
||||
const acquire = vi.fn(async () => ({
|
||||
key: {} as any,
|
||||
client: { deleteSession: vi.fn(), resumeSession } as any,
|
||||
key: TEST_POOL_KEY,
|
||||
client: createMockCopilotClient({ deleteSession: vi.fn(), resumeSession }),
|
||||
}));
|
||||
pool.acquire = acquire;
|
||||
mocks.runCopilotAttempt.mockImplementation(async (_params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-model",
|
||||
pooledClient: {
|
||||
key: {} as any,
|
||||
client: { deleteSession: vi.fn(), resumeSession } as any,
|
||||
key: TEST_POOL_KEY,
|
||||
client: createMockCopilotClient({ deleteSession: vi.fn(), resumeSession }),
|
||||
},
|
||||
sessionConfig: TEST_SESSION_CONFIG,
|
||||
});
|
||||
@@ -2247,15 +2292,15 @@ describe("createCopilotAgentHarness", () => {
|
||||
const resumeSession = vi.fn();
|
||||
const pool = makePoolMock();
|
||||
pool.acquire = vi.fn(async () => ({
|
||||
key: {} as any,
|
||||
client: { deleteSession: vi.fn(), resumeSession } as any,
|
||||
key: TEST_POOL_KEY,
|
||||
client: createMockCopilotClient({ deleteSession: vi.fn(), resumeSession }),
|
||||
}));
|
||||
mocks.runCopilotAttempt.mockImplementation(async (_params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-login",
|
||||
pooledClient: {
|
||||
key: {} as any,
|
||||
client: { deleteSession: vi.fn(), resumeSession } as any,
|
||||
key: TEST_POOL_KEY,
|
||||
client: createMockCopilotClient({ deleteSession: vi.fn(), resumeSession }),
|
||||
},
|
||||
sessionConfig: TEST_SESSION_CONFIG,
|
||||
});
|
||||
@@ -2275,6 +2320,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
},
|
||||
key: { agentId: "test", authMode: "gitHubToken", copilotHome: "/copilot-home" },
|
||||
options: { copilotHome: "/copilot-home", gitHubToken: "ghp_test" },
|
||||
provider: { mode: "github-copilot" },
|
||||
});
|
||||
const result = await harness.compact?.(
|
||||
makeCompactParams({
|
||||
@@ -2299,16 +2345,16 @@ describe("createCopilotAgentHarness", () => {
|
||||
});
|
||||
const pool = makePoolMock();
|
||||
pool.acquire = vi.fn(async () => ({
|
||||
key: {} as any,
|
||||
client: { deleteSession: vi.fn(), resumeSession } as any,
|
||||
key: TEST_POOL_KEY,
|
||||
client: createMockCopilotClient({ deleteSession: vi.fn(), resumeSession }),
|
||||
}));
|
||||
pool.release = vi.fn(async () => undefined);
|
||||
mocks.runCopilotAttempt.mockImplementation(async (_params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-stale",
|
||||
pooledClient: {
|
||||
key: {} as any,
|
||||
client: { deleteSession: vi.fn(), resumeSession } as any,
|
||||
key: TEST_POOL_KEY,
|
||||
client: createMockCopilotClient({ deleteSession: vi.fn(), resumeSession }),
|
||||
},
|
||||
sessionConfig: TEST_SESSION_CONFIG,
|
||||
});
|
||||
@@ -2334,16 +2380,16 @@ describe("createCopilotAgentHarness", () => {
|
||||
const resumeSession = vi.fn();
|
||||
const pool = makePoolMock();
|
||||
pool.acquire = vi.fn(async () => ({
|
||||
key: {} as any,
|
||||
client: { deleteSession: vi.fn(), resumeSession } as any,
|
||||
key: TEST_POOL_KEY,
|
||||
client: createMockCopilotClient({ deleteSession: vi.fn(), resumeSession }),
|
||||
}));
|
||||
pool.release = vi.fn(async () => undefined);
|
||||
mocks.runCopilotAttempt.mockImplementation(async (_params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-abort",
|
||||
pooledClient: {
|
||||
key: {} as any,
|
||||
client: { deleteSession: vi.fn(), resumeSession } as any,
|
||||
key: TEST_POOL_KEY,
|
||||
client: createMockCopilotClient({ deleteSession: vi.fn(), resumeSession }),
|
||||
},
|
||||
sessionConfig: TEST_SESSION_CONFIG,
|
||||
});
|
||||
@@ -2388,16 +2434,16 @@ describe("createCopilotAgentHarness", () => {
|
||||
}));
|
||||
const pool = makePoolMock();
|
||||
pool.acquire = vi.fn(async () => ({
|
||||
key: {} as any,
|
||||
client: { deleteSession: vi.fn(), resumeSession } as any,
|
||||
key: TEST_POOL_KEY,
|
||||
client: createMockCopilotClient({ deleteSession: vi.fn(), resumeSession }),
|
||||
}));
|
||||
pool.release = vi.fn(async () => undefined);
|
||||
mocks.runCopilotAttempt.mockImplementation(async (_params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-cancel",
|
||||
pooledClient: {
|
||||
key: {} as any,
|
||||
client: { deleteSession: vi.fn(), resumeSession } as any,
|
||||
key: TEST_POOL_KEY,
|
||||
client: createMockCopilotClient({ deleteSession: vi.fn(), resumeSession }),
|
||||
},
|
||||
sessionConfig: TEST_SESSION_CONFIG,
|
||||
});
|
||||
@@ -2438,13 +2484,14 @@ describe("createCopilotAgentHarness", () => {
|
||||
},
|
||||
key: { agentId: "test", authMode: "gitHubToken", copilotHome: "/copilot-home" },
|
||||
options: { copilotHome: "/copilot-home", gitHubToken: "ghp_test" },
|
||||
provider: { mode: "github-copilot" },
|
||||
});
|
||||
mocks.runCopilotAttempt.mockImplementationOnce(async (_params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-persisted-token",
|
||||
pooledClient: {
|
||||
key: {} as any,
|
||||
client: { deleteSession: vi.fn(), resumeSession: vi.fn() } as any,
|
||||
key: TEST_POOL_KEY,
|
||||
client: createMockCopilotClient({ deleteSession: vi.fn(), resumeSession: vi.fn() }),
|
||||
},
|
||||
sessionConfig: TEST_SESSION_CONFIG,
|
||||
});
|
||||
@@ -2464,8 +2511,8 @@ describe("createCopilotAgentHarness", () => {
|
||||
const resumeSession = vi.fn();
|
||||
const secondPool = makePoolMock();
|
||||
const secondAcquire = vi.fn(async () => ({
|
||||
key: {} as any,
|
||||
client: { deleteSession: vi.fn(), resumeSession } as any,
|
||||
key: TEST_POOL_KEY,
|
||||
client: createMockCopilotClient({ deleteSession: vi.fn(), resumeSession }),
|
||||
}));
|
||||
secondPool.acquire = secondAcquire;
|
||||
const secondHarness = createCopilotAgentHarness({
|
||||
@@ -2496,6 +2543,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
},
|
||||
key: { agentId: "test", authMode: "gitHubToken", copilotHome: "/copilot-home" },
|
||||
options: { copilotHome: "/copilot-home", gitHubToken: "ghp_other" },
|
||||
provider: { mode: "github-copilot" },
|
||||
});
|
||||
const rotatedPool = makePoolMock();
|
||||
const rotatedAcquire = vi.fn();
|
||||
@@ -2530,8 +2578,8 @@ describe("createCopilotAgentHarness", () => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-persisted",
|
||||
pooledClient: {
|
||||
key: {} as any,
|
||||
client: { deleteSession: vi.fn(), resumeSession: vi.fn() } as any,
|
||||
key: TEST_POOL_KEY,
|
||||
client: createMockCopilotClient({ deleteSession: vi.fn(), resumeSession: vi.fn() }),
|
||||
},
|
||||
sessionConfig: TEST_SESSION_CONFIG,
|
||||
});
|
||||
@@ -2542,8 +2590,8 @@ describe("createCopilotAgentHarness", () => {
|
||||
const resumeSession = vi.fn();
|
||||
const secondPool = makePoolMock();
|
||||
const secondAcquire = vi.fn(async () => ({
|
||||
key: {} as any,
|
||||
client: { deleteSession: vi.fn(), resumeSession } as any,
|
||||
key: TEST_POOL_KEY,
|
||||
client: createMockCopilotClient({ deleteSession: vi.fn(), resumeSession }),
|
||||
}));
|
||||
secondPool.acquire = secondAcquire;
|
||||
secondPool.release = vi.fn(async () => undefined);
|
||||
@@ -2579,16 +2627,16 @@ describe("createCopilotAgentHarness", () => {
|
||||
}));
|
||||
const pool = makePoolMock();
|
||||
pool.acquire = vi.fn(async () => ({
|
||||
key: {} as any,
|
||||
client: { deleteSession: vi.fn(), resumeSession } as any,
|
||||
key: TEST_POOL_KEY,
|
||||
client: createMockCopilotClient({ deleteSession: vi.fn(), resumeSession }),
|
||||
}));
|
||||
pool.release = vi.fn(async () => undefined);
|
||||
mocks.runCopilotAttempt.mockImplementation(async (_params, deps) => {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-noop",
|
||||
pooledClient: {
|
||||
key: {} as any,
|
||||
client: { deleteSession: vi.fn(), resumeSession } as any,
|
||||
key: TEST_POOL_KEY,
|
||||
client: createMockCopilotClient({ deleteSession: vi.fn(), resumeSession }),
|
||||
},
|
||||
sessionConfig: TEST_SESSION_CONFIG,
|
||||
});
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { ApplicationCommandType, type APIApplicationCommand } from "discord-api-types/v10";
|
||||
import {
|
||||
ApplicationCommandType,
|
||||
type APIApplicationCommand,
|
||||
type APIApplicationCommandOption,
|
||||
} from "discord-api-types/v10";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { DiscordCommandDeployer, testing } from "./command-deploy.js";
|
||||
import { BaseCommand } from "./commands.js";
|
||||
@@ -68,7 +72,7 @@ describe("commandsEqual", () => {
|
||||
name_localizations: null,
|
||||
description: "Skill name",
|
||||
description_localizations: null,
|
||||
} as any,
|
||||
} as APIApplicationCommandOption,
|
||||
],
|
||||
});
|
||||
const desired = desiredFromLocal({
|
||||
@@ -81,7 +85,9 @@ describe("commandsEqual", () => {
|
||||
const current = currentFromDiscord({
|
||||
name: "skill",
|
||||
description: "Run a skill.",
|
||||
options: [{ type: 3, name: "name", description: "Skill name" } as any],
|
||||
options: [
|
||||
{ type: 3, name: "name", description: "Skill name" } as APIApplicationCommandOption,
|
||||
],
|
||||
});
|
||||
const desired = desiredFromLocal({
|
||||
name: "skill",
|
||||
@@ -95,7 +101,9 @@ describe("commandsEqual", () => {
|
||||
const current = currentFromDiscord({
|
||||
name: "skill",
|
||||
description: "Run a skill.",
|
||||
options: [{ type: 3, name: "name", description: "Skill name" } as any],
|
||||
options: [
|
||||
{ type: 3, name: "name", description: "Skill name" } as APIApplicationCommandOption,
|
||||
],
|
||||
});
|
||||
const desired = desiredFromLocal({
|
||||
name: "skill",
|
||||
@@ -154,7 +162,7 @@ describe("commandsEqual", () => {
|
||||
name: "name",
|
||||
description: "Skill name",
|
||||
description_localizations: { "zh-CN": "技能名称。直接输入。" },
|
||||
} as any,
|
||||
} as APIApplicationCommandOption,
|
||||
],
|
||||
});
|
||||
const desired = desiredFromLocal({
|
||||
|
||||
@@ -300,7 +300,7 @@ describe("discord group policy", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(
|
||||
resolveDiscordGroupRequireMention({ cfg: discordCfg, groupSpace: "guild1", groupId: "123" }),
|
||||
@@ -376,7 +376,7 @@ describe("discord group policy", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(
|
||||
resolveDiscordGroupRequireMention({
|
||||
|
||||
@@ -304,10 +304,10 @@ describe("googlechat setup", () => {
|
||||
});
|
||||
|
||||
it("uses configured defaultAccount for omitted allowFrom prompt context", async () => {
|
||||
const prompter = {
|
||||
const prompter = createTestWizardPrompter({
|
||||
note: vi.fn(async () => {}),
|
||||
text: vi.fn(async () => "users/123456789"),
|
||||
};
|
||||
});
|
||||
|
||||
const next = await googlechatSetupWizard.dmPolicy?.promptAllowFrom?.({
|
||||
cfg: {
|
||||
@@ -328,7 +328,7 @@ describe("googlechat setup", () => {
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
prompter: prompter as any,
|
||||
prompter,
|
||||
});
|
||||
|
||||
expect(next?.channels?.googlechat?.dm?.allowFrom).toEqual(["users/root"]);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Googlechat tests cover targets plugin behavior.
|
||||
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../runtime-api.js";
|
||||
import type { ResolvedGoogleChatAccount } from "./accounts.js";
|
||||
import { downloadGoogleChatMedia, sendGoogleChatMessage, updateGoogleChatMessage } from "./api.js";
|
||||
import {
|
||||
@@ -271,7 +272,7 @@ describe("googlechat group policy", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(resolveGoogleChatGroupRequireMention({ cfg, groupId: "spaces/AAA" })).toBe(false);
|
||||
expect(resolveGoogleChatGroupRequireMention({ cfg, groupId: "spaces/BBB" })).toBe(true);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
// Imessage tests cover targets plugin behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
@@ -214,7 +215,7 @@ describe("imessage group policy", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(resolveIMessageGroupRequireMention({ cfg, groupId: "chat:family" })).toBe(false);
|
||||
expect(resolveIMessageGroupRequireMention({ cfg, groupId: "chat:other" })).toBe(true);
|
||||
|
||||
@@ -27,9 +27,14 @@ function createRuntime(): { runtime: PluginRuntime; mocks: LineRuntimeMocks } {
|
||||
accountId && accountId !== DEFAULT_ACCOUNT_ID
|
||||
? (lineConfig.accounts?.[accountId] ?? {})
|
||||
: lineConfig;
|
||||
const hasToken =
|
||||
Boolean((entry as any).channelAccessToken) || Boolean((entry as any).tokenFile);
|
||||
const hasSecret = Boolean((entry as any).channelSecret) || Boolean((entry as any).secretFile);
|
||||
const credentials = entry as {
|
||||
channelAccessToken?: unknown;
|
||||
channelSecret?: unknown;
|
||||
secretFile?: unknown;
|
||||
tokenFile?: unknown;
|
||||
};
|
||||
const hasToken = Boolean(credentials.channelAccessToken) || Boolean(credentials.tokenFile);
|
||||
const hasSecret = Boolean(credentials.channelSecret) || Boolean(credentials.secretFile);
|
||||
return { tokenSource: hasToken && hasSecret ? "config" : "none" };
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
// Line tests cover group keys plugin behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
@@ -56,7 +57,7 @@ describe("account-scoped LINE groups", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(resolveLineGroupsConfig(cfg, "work")).toEqual({
|
||||
"group:g1": { requireMention: false },
|
||||
@@ -88,7 +89,7 @@ describe("line group policy", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(resolveLineGroupRequireMention({ cfg, groupId: "r123" })).toBe(false);
|
||||
expect(resolveLineGroupRequireMention({ cfg, groupId: "room:r123" })).toBe(false);
|
||||
@@ -117,7 +118,7 @@ describe("line group policy", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(resolveLineGroupRequireMention({ cfg, groupId: "g123", accountId: "work" })).toBe(false);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Line tests cover webhook node plugin behavior.
|
||||
import crypto from "node:crypto";
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import type { NextFunction, Request, Response } from "express";
|
||||
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { createMockIncomingRequest } from "openclaw/plugin-sdk/test-env";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
@@ -68,16 +69,24 @@ function createRuntimeMock(): RuntimeEnvMock {
|
||||
}
|
||||
|
||||
function createMiddlewareRes() {
|
||||
const status = vi.fn<Response["status"]>();
|
||||
const json = vi.fn<Response["json"]>();
|
||||
const res = {
|
||||
status: vi.fn(),
|
||||
json: vi.fn(),
|
||||
status,
|
||||
json,
|
||||
headersSent: false,
|
||||
} as any;
|
||||
res.status.mockReturnValue(res);
|
||||
res.json.mockReturnValue(res);
|
||||
} as unknown as Response & { status: typeof status; json: typeof json };
|
||||
status.mockReturnValue(res);
|
||||
json.mockReturnValue(res);
|
||||
return res;
|
||||
}
|
||||
|
||||
function createMiddlewareRequest(
|
||||
request: Pick<Request, "body" | "headers"> & { rawBody?: string | Buffer },
|
||||
): Request {
|
||||
return request as unknown as Request;
|
||||
}
|
||||
|
||||
function createPostWebhookTestHarness(rawBody: string, secret = "secret") {
|
||||
const bot = { handleWebhook: vi.fn(async () => {}) };
|
||||
const runtime = createRuntimeMock();
|
||||
@@ -128,12 +137,12 @@ async function invokeWebhook(params: {
|
||||
}
|
||||
}
|
||||
|
||||
const req = {
|
||||
const req = createMiddlewareRequest({
|
||||
headers,
|
||||
body: params.body,
|
||||
} as any;
|
||||
});
|
||||
const res = createMiddlewareRes();
|
||||
await middleware(req, res, {} as any);
|
||||
await middleware(req, res, vi.fn() as NextFunction);
|
||||
return { res, onEvents: onEventsMock };
|
||||
}
|
||||
|
||||
@@ -242,14 +251,14 @@ async function expectSignedRawBodyWins(params: { rawBody: string | Buffer; signe
|
||||
});
|
||||
const rawBodyText =
|
||||
typeof params.rawBody === "string" ? params.rawBody : params.rawBody.toString("utf-8");
|
||||
const req = {
|
||||
const req = createMiddlewareRequest({
|
||||
headers: { "x-line-signature": sign(rawBodyText, SECRET) },
|
||||
rawBody: params.rawBody,
|
||||
body: reqBody,
|
||||
} as any;
|
||||
});
|
||||
const res = createMiddlewareRes();
|
||||
|
||||
await middleware(req, res, {} as any);
|
||||
await middleware(req, res, vi.fn() as NextFunction);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(onEvents).toHaveBeenCalledTimes(1);
|
||||
@@ -583,14 +592,14 @@ describe("createLineWebhookMiddleware", () => {
|
||||
onEvents,
|
||||
});
|
||||
|
||||
const req = {
|
||||
const req = createMiddlewareRequest({
|
||||
headers: { "x-line-signature": sign(rawBody, SECRET) },
|
||||
rawBody,
|
||||
body: { events: [{ type: "message" }] },
|
||||
} as any;
|
||||
});
|
||||
const res = createMiddlewareRes();
|
||||
|
||||
await middleware(req, res, {} as any);
|
||||
await middleware(req, res, vi.fn() as NextFunction);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.json).toHaveBeenCalledWith({ error: "Invalid webhook payload" });
|
||||
|
||||
@@ -16,8 +16,11 @@ afterAll(() => {
|
||||
|
||||
import { createLlmTaskTool } from "./llm-task-tool.js";
|
||||
|
||||
const runEmbeddedAgent = vi.fn(async () => ({
|
||||
meta: { startedAt: Date.now() },
|
||||
type LlmTaskApi = Parameters<typeof createLlmTaskTool>[0];
|
||||
type RunEmbeddedAgent = LlmTaskApi["runtime"]["agent"]["runEmbeddedAgent"];
|
||||
|
||||
const runEmbeddedAgent = vi.fn<RunEmbeddedAgent>(async () => ({
|
||||
meta: { durationMs: 0, startedAt: Date.now() },
|
||||
payloads: [{ text: "{}" }],
|
||||
}));
|
||||
|
||||
@@ -56,7 +59,7 @@ const normalizeThinkingLevel = vi.fn((raw?: string | null) => {
|
||||
return undefined;
|
||||
});
|
||||
|
||||
function fakeApi(overrides: any = {}) {
|
||||
function fakeApi(overrides: Record<string, unknown> = {}): LlmTaskApi {
|
||||
return {
|
||||
id: "llm-task",
|
||||
name: "llm-task",
|
||||
@@ -85,12 +88,12 @@ function fakeApi(overrides: any = {}) {
|
||||
logger: { debug() {}, info() {}, warn() {}, error() {} },
|
||||
registerTool() {},
|
||||
...overrides,
|
||||
};
|
||||
} as unknown as LlmTaskApi;
|
||||
}
|
||||
|
||||
function mockEmbeddedRunJson(payload: unknown) {
|
||||
(runEmbeddedAgent as any).mockResolvedValueOnce({
|
||||
meta: {},
|
||||
runEmbeddedAgent.mockResolvedValueOnce({
|
||||
meta: { durationMs: 0 },
|
||||
payloads: [{ text: JSON.stringify(payload) }],
|
||||
});
|
||||
}
|
||||
@@ -98,7 +101,7 @@ function mockEmbeddedRunJson(payload: unknown) {
|
||||
function resetRunnerMocks() {
|
||||
runEmbeddedAgent.mockReset();
|
||||
runEmbeddedAgent.mockImplementation(async () => ({
|
||||
meta: { startedAt: Date.now() },
|
||||
meta: { durationMs: 0, startedAt: Date.now() },
|
||||
payloads: [{ text: "{}" }],
|
||||
}));
|
||||
resolveThinkingPolicy.mockClear();
|
||||
@@ -108,7 +111,26 @@ function resetRunnerMocks() {
|
||||
async function executeEmbeddedRun(input: Record<string, unknown>) {
|
||||
const tool = createLlmTaskTool(fakeApi());
|
||||
await tool.execute("id", input);
|
||||
return (runEmbeddedAgent as any).mock.calls[0]?.[0];
|
||||
return firstEmbeddedRunCall();
|
||||
}
|
||||
|
||||
function firstEmbeddedRunCall() {
|
||||
const call = runEmbeddedAgent.mock.calls[0]?.[0];
|
||||
if (!call) {
|
||||
throw new Error("expected embedded agent run");
|
||||
}
|
||||
return call;
|
||||
}
|
||||
|
||||
function resultJson(result: unknown): unknown {
|
||||
if (!result || typeof result !== "object" || !("details" in result)) {
|
||||
throw new Error("expected tool result details");
|
||||
}
|
||||
const details = result.details;
|
||||
if (!details || typeof details !== "object" || !("json" in details)) {
|
||||
throw new Error("expected tool result JSON");
|
||||
}
|
||||
return details.json;
|
||||
}
|
||||
|
||||
describe("llm-task tool (json-only)", () => {
|
||||
@@ -117,28 +139,28 @@ describe("llm-task tool (json-only)", () => {
|
||||
});
|
||||
|
||||
it("returns parsed json", async () => {
|
||||
(runEmbeddedAgent as any).mockResolvedValueOnce({
|
||||
meta: {},
|
||||
runEmbeddedAgent.mockResolvedValueOnce({
|
||||
meta: { durationMs: 0 },
|
||||
payloads: [{ text: JSON.stringify({ foo: "bar" }) }],
|
||||
});
|
||||
const tool = createLlmTaskTool(fakeApi());
|
||||
const res = await tool.execute("id", { prompt: "return foo" });
|
||||
expect((res as any).details.json).toEqual({ foo: "bar" });
|
||||
expect(resultJson(res)).toEqual({ foo: "bar" });
|
||||
});
|
||||
|
||||
it("strips fenced json", async () => {
|
||||
(runEmbeddedAgent as any).mockResolvedValueOnce({
|
||||
meta: {},
|
||||
runEmbeddedAgent.mockResolvedValueOnce({
|
||||
meta: { durationMs: 0 },
|
||||
payloads: [{ text: '```json\n{"ok":true}\n```' }],
|
||||
});
|
||||
const tool = createLlmTaskTool(fakeApi());
|
||||
const res = await tool.execute("id", { prompt: "return ok" });
|
||||
expect((res as any).details.json).toEqual({ ok: true });
|
||||
expect(resultJson(res)).toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it("validates schema", async () => {
|
||||
(runEmbeddedAgent as any).mockResolvedValueOnce({
|
||||
meta: {},
|
||||
runEmbeddedAgent.mockResolvedValueOnce({
|
||||
meta: { durationMs: 0 },
|
||||
payloads: [{ text: JSON.stringify({ foo: "bar" }) }],
|
||||
});
|
||||
const tool = createLlmTaskTool(fakeApi());
|
||||
@@ -149,18 +171,18 @@ describe("llm-task tool (json-only)", () => {
|
||||
additionalProperties: false,
|
||||
};
|
||||
const res = await tool.execute("id", { prompt: "return foo", schema });
|
||||
expect((res as any).details.json).toEqual({ foo: "bar" });
|
||||
expect(resultJson(res)).toEqual({ foo: "bar" });
|
||||
});
|
||||
|
||||
it("validates caller schemas with repeated $id independently across calls", async () => {
|
||||
const tool = createLlmTaskTool(fakeApi());
|
||||
(runEmbeddedAgent as any)
|
||||
runEmbeddedAgent
|
||||
.mockResolvedValueOnce({
|
||||
meta: {},
|
||||
meta: { durationMs: 0 },
|
||||
payloads: [{ text: JSON.stringify({ foo: "bar" }) }],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
meta: {},
|
||||
meta: { durationMs: 0 },
|
||||
payloads: [{ text: JSON.stringify({ count: 1 }) }],
|
||||
});
|
||||
|
||||
@@ -198,8 +220,8 @@ describe("llm-task tool (json-only)", () => {
|
||||
});
|
||||
|
||||
it("throws on invalid json", async () => {
|
||||
(runEmbeddedAgent as any).mockResolvedValueOnce({
|
||||
meta: {},
|
||||
runEmbeddedAgent.mockResolvedValueOnce({
|
||||
meta: { durationMs: 0 },
|
||||
payloads: [{ text: "not-json" }],
|
||||
});
|
||||
const tool = createLlmTaskTool(fakeApi());
|
||||
@@ -207,8 +229,8 @@ describe("llm-task tool (json-only)", () => {
|
||||
});
|
||||
|
||||
it("throws on schema mismatch", async () => {
|
||||
(runEmbeddedAgent as any).mockResolvedValueOnce({
|
||||
meta: {},
|
||||
runEmbeddedAgent.mockResolvedValueOnce({
|
||||
meta: { durationMs: 0 },
|
||||
payloads: [{ text: JSON.stringify({ foo: 1 }) }],
|
||||
});
|
||||
const tool = createLlmTaskTool(fakeApi());
|
||||
@@ -258,7 +280,7 @@ describe("llm-task tool (json-only)", () => {
|
||||
|
||||
await tool.execute("id", { prompt: "x", model: "gemini-flash" });
|
||||
|
||||
const call = (runEmbeddedAgent as any).mock.calls[0]?.[0];
|
||||
const call = firstEmbeddedRunCall();
|
||||
expect(call.provider).toBe("google");
|
||||
expect(call.model).toBe("gemini-3-flash-preview");
|
||||
});
|
||||
@@ -301,7 +323,7 @@ describe("llm-task tool (json-only)", () => {
|
||||
model: "gpt-5.6-sol",
|
||||
agentRuntime: "codex",
|
||||
});
|
||||
const call = (runEmbeddedAgent as any).mock.calls[0]?.[0];
|
||||
const call = firstEmbeddedRunCall();
|
||||
expect(call.thinkLevel).toBe("ultra");
|
||||
expect(call.config).toBe(config);
|
||||
expect(call.agentHarnessRuntimeOverride).toBe("codex");
|
||||
@@ -334,7 +356,7 @@ describe("llm-task tool (json-only)", () => {
|
||||
model: "gpt-5.6-luna",
|
||||
agentRuntime: "openclaw",
|
||||
});
|
||||
const call = (runEmbeddedAgent as any).mock.calls[0]?.[0];
|
||||
const call = firstEmbeddedRunCall();
|
||||
expect(call.thinkLevel).toBe("ultra");
|
||||
expect(call.config).toBe(config);
|
||||
expect(call.agentHarnessRuntimeOverride).toBe("openclaw");
|
||||
|
||||
@@ -10,7 +10,7 @@ function fakeApi(overrides: Partial<OpenClawPluginApi> = {}): OpenClawPluginApi
|
||||
id: "lobster",
|
||||
name: "lobster",
|
||||
source: "test",
|
||||
runtime: { version: "test" } as any,
|
||||
runtime: { version: "test" } as OpenClawPluginApi["runtime"],
|
||||
resolvePath: (p) => p,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Memory Core plugin module implements memory tool manager mock behavior.
|
||||
import type { MemorySearchRuntimeDebug } from "openclaw/plugin-sdk/memory-core-host-runtime-files";
|
||||
import { vi } from "vitest";
|
||||
import type { getMemorySearchManager } from "./tools.runtime.js";
|
||||
|
||||
type SearchImpl = (opts?: {
|
||||
maxResults?: number;
|
||||
@@ -20,6 +21,7 @@ type MemoryReadResult = {
|
||||
nextFrom?: number;
|
||||
};
|
||||
type MemoryBackend = "builtin" | "qmd";
|
||||
type MemoryManagerDebug = Awaited<ReturnType<typeof getMemorySearchManager>>["debug"];
|
||||
|
||||
let backend: MemoryBackend = "builtin";
|
||||
let workspaceDir = "/workspace";
|
||||
@@ -29,6 +31,7 @@ let getManagerImpl:
|
||||
| ((params: { cfg?: unknown; agentId?: string; purpose?: string }) => Promise<{
|
||||
manager?: unknown;
|
||||
error?: string;
|
||||
debug?: MemoryManagerDebug;
|
||||
}>)
|
||||
| undefined;
|
||||
let readFileImpl: (params: MemoryReadParams) => Promise<MemoryReadResult> = async (params) => ({
|
||||
@@ -101,6 +104,7 @@ export function setMemorySearchManagerImpl(
|
||||
next: (params: { cfg?: unknown; agentId?: string; purpose?: string }) => Promise<{
|
||||
manager?: unknown;
|
||||
error?: string;
|
||||
debug?: MemoryManagerDebug;
|
||||
}>,
|
||||
): void {
|
||||
getManagerImpl = next;
|
||||
|
||||
@@ -60,6 +60,14 @@ const originalStartupConfigPath = process.env.OPENCLAW_CONFIG_PATH;
|
||||
let transcriptUpdateListener: ((update: MemorySessionTranscriptUpdate) => void) | undefined;
|
||||
|
||||
type SourceStateRow = { path: string; hash: string; mtime: number; size: number };
|
||||
type StartupCatchupHarnessInternals = {
|
||||
syncArchiveFiles(params: { needsFullReindex: boolean }): Promise<void>;
|
||||
updateSessionDelta(sessionFile: string): Promise<{
|
||||
pendingBytes: number;
|
||||
pendingLines: number;
|
||||
pendingMessages: number;
|
||||
}>;
|
||||
};
|
||||
|
||||
function setStartupStateDir(stateDir: string): void {
|
||||
Reflect.set(process.env, "OPENCLAW_STATE_DIR", stateDir);
|
||||
@@ -407,7 +415,9 @@ describe("session startup catch-up", () => {
|
||||
});
|
||||
|
||||
try {
|
||||
await (harness as any).syncArchiveFiles({ needsFullReindex: true });
|
||||
await (harness as unknown as StartupCatchupHarnessInternals).syncArchiveFiles({
|
||||
needsFullReindex: true,
|
||||
});
|
||||
expect(attempts).toBe(2);
|
||||
} finally {
|
||||
openSpy.mockRestore();
|
||||
@@ -528,7 +538,9 @@ describe("session startup catch-up", () => {
|
||||
});
|
||||
|
||||
try {
|
||||
const delta = await (harness as any).updateSessionDelta(session.filePath);
|
||||
const delta = await (harness as unknown as StartupCatchupHarnessInternals).updateSessionDelta(
|
||||
session.filePath,
|
||||
);
|
||||
expect(delta).toMatchObject({
|
||||
pendingBytes: session.size,
|
||||
pendingMessages: 1,
|
||||
|
||||
@@ -699,46 +699,43 @@ describe("memory_search unavailable payloads", () => {
|
||||
});
|
||||
|
||||
it("includes manager acquisition timing and cache-state debug payload", async () => {
|
||||
setMemorySearchManagerImpl(
|
||||
async () =>
|
||||
({
|
||||
manager: {
|
||||
search: vi.fn(async () => {
|
||||
return [
|
||||
{
|
||||
path: "MEMORY.md",
|
||||
startLine: 1,
|
||||
endLine: 2,
|
||||
score: 0.9,
|
||||
snippet: "ramen",
|
||||
source: "memory",
|
||||
},
|
||||
];
|
||||
}),
|
||||
readFile: vi.fn(),
|
||||
status: vi.fn(() => ({
|
||||
backend: "qmd",
|
||||
provider: "qmd",
|
||||
model: "qmd",
|
||||
requestedProvider: "qmd",
|
||||
files: 0,
|
||||
chunks: 0,
|
||||
dirty: false,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
dbPath: "/tmp/workspace/index.sqlite",
|
||||
sources: ["memory"],
|
||||
sourceCounts: [{ source: "memory", files: 0, chunks: 0 }],
|
||||
})),
|
||||
sync: vi.fn(async () => {}),
|
||||
probeEmbeddingAvailability: vi.fn(async () => ({ ok: true })),
|
||||
probeVectorAvailability: vi.fn(async () => true),
|
||||
},
|
||||
debug: {
|
||||
managerMs: 17,
|
||||
managerCacheState: "cached-full-hit",
|
||||
},
|
||||
}) as any,
|
||||
);
|
||||
setMemorySearchManagerImpl(async () => ({
|
||||
manager: {
|
||||
search: vi.fn(async () => {
|
||||
return [
|
||||
{
|
||||
path: "MEMORY.md",
|
||||
startLine: 1,
|
||||
endLine: 2,
|
||||
score: 0.9,
|
||||
snippet: "ramen",
|
||||
source: "memory",
|
||||
},
|
||||
];
|
||||
}),
|
||||
readFile: vi.fn(),
|
||||
status: vi.fn(() => ({
|
||||
backend: "qmd",
|
||||
provider: "qmd",
|
||||
model: "qmd",
|
||||
requestedProvider: "qmd",
|
||||
files: 0,
|
||||
chunks: 0,
|
||||
dirty: false,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
dbPath: "/tmp/workspace/index.sqlite",
|
||||
sources: ["memory"],
|
||||
sourceCounts: [{ source: "memory", files: 0, chunks: 0 }],
|
||||
})),
|
||||
sync: vi.fn(async () => {}),
|
||||
probeEmbeddingAvailability: vi.fn(async () => ({ ok: true })),
|
||||
probeVectorAvailability: vi.fn(async () => true),
|
||||
},
|
||||
debug: {
|
||||
managerMs: 17,
|
||||
managerCacheState: "cached-full-hit",
|
||||
},
|
||||
}));
|
||||
setMemorySearchImpl(async () => [
|
||||
{
|
||||
path: "MEMORY.md",
|
||||
|
||||
@@ -87,6 +87,10 @@ function createRuntimeLoader(
|
||||
|
||||
type MockCallSource = { mock: { calls: Array<Array<unknown>> } };
|
||||
|
||||
function registerTestPlugin(plugin: { register: (api: never) => void }, api: unknown): void {
|
||||
plugin.register(api as never);
|
||||
}
|
||||
|
||||
function firstMockArg(source: MockCallSource, label: string, argIndex = 0) {
|
||||
const [call] = source.mock.calls;
|
||||
if (!call) {
|
||||
@@ -312,7 +316,7 @@ describe("memory plugin e2e", () => {
|
||||
resolvePath: (filePath: string) => filePath,
|
||||
};
|
||||
|
||||
memoryPlugin.register(mockApi as any);
|
||||
registerTestPlugin(memoryPlugin, mockApi);
|
||||
const service = firstObjectArg(registerService as unknown as MockCallSource, "service");
|
||||
expect(service.id).toBe("memory-lancedb");
|
||||
expect(service.start).toBeTypeOf("function");
|
||||
@@ -355,7 +359,7 @@ describe("memory plugin e2e", () => {
|
||||
resolvePath: (filePath: string) => filePath,
|
||||
};
|
||||
|
||||
memoryPlugin.register(mockApi as any);
|
||||
registerTestPlugin(memoryPlugin, mockApi);
|
||||
|
||||
expectHookRegistered(on, "before_prompt_build");
|
||||
expectHookNotRegistered(on, "before_agent_start");
|
||||
@@ -396,7 +400,7 @@ describe("memory plugin e2e", () => {
|
||||
resolvePath: (filePath: string) => filePath,
|
||||
};
|
||||
|
||||
memoryPlugin.register(mockApi as any);
|
||||
registerTestPlugin(memoryPlugin, mockApi);
|
||||
const capability = firstObjectArg(
|
||||
registerMemoryCapabilityLocal as unknown as MockCallSource,
|
||||
"memory capability",
|
||||
@@ -491,7 +495,7 @@ describe("memory plugin e2e", () => {
|
||||
resolvePath: (filePath: string) => filePath,
|
||||
};
|
||||
|
||||
memoryPlugin.register(mockApi as any);
|
||||
registerTestPlugin(memoryPlugin, mockApi);
|
||||
|
||||
expect(registerMemoryCapabilityForPlugin).toHaveBeenCalledOnce();
|
||||
expect(
|
||||
@@ -607,7 +611,7 @@ describe("memory plugin e2e", () => {
|
||||
resolvePath: (filePath: string) => filePath,
|
||||
};
|
||||
|
||||
dynamicMemoryPlugin.register(mockApi as any);
|
||||
registerTestPlugin(dynamicMemoryPlugin, mockApi);
|
||||
const recallTool = registerTool.mock.calls
|
||||
.map(([tool]) => tool)
|
||||
.find((tool) => tool.name === "memory_recall");
|
||||
@@ -696,7 +700,7 @@ describe("memory plugin e2e", () => {
|
||||
resolvePath: (filePath: string) => filePath,
|
||||
};
|
||||
|
||||
dynamicMemoryPlugin.register(mockApi as any);
|
||||
registerTestPlugin(dynamicMemoryPlugin, mockApi);
|
||||
const recallTool = registeredTools.find((t) => t.opts?.name === "memory_recall")?.tool;
|
||||
if (!recallTool) {
|
||||
throw new Error("memory_recall tool was not registered");
|
||||
@@ -793,7 +797,7 @@ describe("memory plugin e2e", () => {
|
||||
resolvePath: (filePath: string) => filePath,
|
||||
};
|
||||
|
||||
dynamicMemoryPlugin.register(mockApi as any);
|
||||
registerTestPlugin(dynamicMemoryPlugin, mockApi);
|
||||
const recallTool = registeredTools.find((t) => t.opts?.name === "memory_recall")?.tool;
|
||||
if (!recallTool) {
|
||||
throw new Error("memory_recall tool was not registered");
|
||||
@@ -882,7 +886,7 @@ describe("memory plugin e2e", () => {
|
||||
resolvePath: (filePath: string) => filePath,
|
||||
};
|
||||
|
||||
dynamicMemoryPlugin.register(mockApi as any);
|
||||
registerTestPlugin(dynamicMemoryPlugin, mockApi);
|
||||
const recallTool = registeredTools.find((t) => t.opts?.name === "memory_recall")?.tool;
|
||||
if (!recallTool) {
|
||||
throw new Error("memory_recall tool was not registered");
|
||||
@@ -973,7 +977,7 @@ describe("memory plugin e2e", () => {
|
||||
};
|
||||
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
||||
try {
|
||||
dynamicMemoryPlugin.register(mockApi as any);
|
||||
registerTestPlugin(dynamicMemoryPlugin, mockApi);
|
||||
const registrar = firstMockArg(registerCli as unknown as MockCallSource, "cli registrar");
|
||||
const program = new Command();
|
||||
(registrar as (params: { program: Command }) => void)({ program });
|
||||
@@ -1018,7 +1022,7 @@ describe("memory plugin e2e", () => {
|
||||
resolvePath: (filePath: string) => filePath,
|
||||
};
|
||||
|
||||
memoryPlugin.register(mockApi as any);
|
||||
registerTestPlugin(memoryPlugin, mockApi);
|
||||
|
||||
const beforePromptBuild = on.mock.calls.find(
|
||||
([hookName]) => hookName === "before_prompt_build",
|
||||
@@ -1060,7 +1064,7 @@ describe("memory plugin e2e", () => {
|
||||
resolvePath: (filePath: string) => filePath,
|
||||
};
|
||||
|
||||
memoryPlugin.register(mockApi as any);
|
||||
registerTestPlugin(memoryPlugin, mockApi);
|
||||
|
||||
expectHookRegistered(on, "before_prompt_build");
|
||||
const agentEnd = on.mock.calls.find(([hookName]) => hookName === "agent_end")?.[1];
|
||||
@@ -1143,7 +1147,7 @@ describe("memory plugin e2e", () => {
|
||||
resolvePath: (p: string) => p,
|
||||
};
|
||||
|
||||
dynamicMemoryPlugin.register(mockApi as any);
|
||||
registerTestPlugin(dynamicMemoryPlugin, mockApi);
|
||||
|
||||
const beforePromptBuild = on.mock.calls.find(
|
||||
([hookName]) => hookName === "before_prompt_build",
|
||||
@@ -1248,7 +1252,7 @@ describe("memory plugin e2e", () => {
|
||||
resolvePath: (p: string) => p,
|
||||
};
|
||||
|
||||
dynamicMemoryPlugin.register(mockApi as any);
|
||||
registerTestPlugin(dynamicMemoryPlugin, mockApi);
|
||||
|
||||
const beforePromptBuild = on.mock.calls.find(
|
||||
([hookName]) => hookName === "before_prompt_build",
|
||||
@@ -1414,7 +1418,7 @@ describe("memory plugin e2e", () => {
|
||||
resolvePath: (p: string) => p,
|
||||
};
|
||||
|
||||
dynamicMemoryPlugin.register(mockApi as any);
|
||||
registerTestPlugin(dynamicMemoryPlugin, mockApi);
|
||||
|
||||
configFile = {
|
||||
plugins: {
|
||||
@@ -1543,7 +1547,7 @@ describe("memory plugin e2e", () => {
|
||||
resolvePath: (p: string) => p,
|
||||
};
|
||||
|
||||
dynamicMemoryPlugin.register(mockApi as any);
|
||||
registerTestPlugin(dynamicMemoryPlugin, mockApi);
|
||||
|
||||
configFile = {
|
||||
plugins: {
|
||||
@@ -1668,7 +1672,7 @@ describe("memory plugin e2e", () => {
|
||||
resolvePath: (p: string) => p,
|
||||
};
|
||||
|
||||
dynamicMemoryPlugin.register(mockApi as any);
|
||||
registerTestPlugin(dynamicMemoryPlugin, mockApi);
|
||||
|
||||
configFile = {
|
||||
plugins: {
|
||||
@@ -1765,7 +1769,7 @@ describe("memory plugin e2e", () => {
|
||||
resolvePath: (p: string) => p,
|
||||
};
|
||||
|
||||
dynamicMemoryPlugin.register(mockApi as any);
|
||||
registerTestPlugin(dynamicMemoryPlugin, mockApi);
|
||||
|
||||
const agentEnd = on.mock.calls.find(([hookName]) => hookName === "agent_end")?.[1];
|
||||
expect(agentEnd).toBeTypeOf("function");
|
||||
@@ -1893,7 +1897,7 @@ describe("memory plugin e2e", () => {
|
||||
resolvePath: (p: string) => p,
|
||||
};
|
||||
|
||||
dynamicMemoryPlugin.register(mockApi as any);
|
||||
registerTestPlugin(dynamicMemoryPlugin, mockApi);
|
||||
|
||||
configFile = {
|
||||
plugins: {
|
||||
@@ -2027,7 +2031,7 @@ describe("memory plugin e2e", () => {
|
||||
resolvePath: (p: string) => p,
|
||||
};
|
||||
|
||||
dynamicMemoryPlugin.register(mockApi as any);
|
||||
registerTestPlugin(dynamicMemoryPlugin, mockApi);
|
||||
|
||||
configFile = {
|
||||
plugins: {
|
||||
@@ -2154,7 +2158,7 @@ describe("memory plugin e2e", () => {
|
||||
resolvePath: (p: string) => p,
|
||||
};
|
||||
|
||||
dynamicMemoryPlugin.register(mockApi as any);
|
||||
registerTestPlugin(dynamicMemoryPlugin, mockApi);
|
||||
|
||||
configFile = {
|
||||
plugins: {
|
||||
@@ -2257,7 +2261,7 @@ describe("memory plugin e2e", () => {
|
||||
resolvePath: (p: string) => p,
|
||||
};
|
||||
|
||||
dynamicMemoryPlugin.register(mockApi as any);
|
||||
registerTestPlugin(dynamicMemoryPlugin, mockApi);
|
||||
|
||||
const agentEnd = on.mock.calls.find(([hookName]) => hookName === "agent_end")?.[1];
|
||||
const sessionEnd = on.mock.calls.find(([hookName]) => hookName === "session_end")?.[1];
|
||||
@@ -2510,7 +2514,7 @@ describe("memory plugin e2e", () => {
|
||||
resolvePath: (p: string) => p,
|
||||
};
|
||||
|
||||
memoryPluginItem.register(mockApi as any);
|
||||
registerTestPlugin(memoryPluginItem, mockApi);
|
||||
const recallTool = registeredTools.find((t) => t.opts?.name === "memory_recall")?.tool;
|
||||
if (!recallTool) {
|
||||
throw new Error("memory_recall tool was not registered");
|
||||
@@ -2611,7 +2615,7 @@ describe("memory plugin e2e", () => {
|
||||
resolvePath: (p: string) => p,
|
||||
};
|
||||
|
||||
dynamicMemoryPlugin.register(mockApi as any);
|
||||
registerTestPlugin(dynamicMemoryPlugin, mockApi);
|
||||
const recallTool = registeredTools.find((t) => t.opts?.name === "memory_recall")?.tool;
|
||||
if (!recallTool) {
|
||||
throw new Error("memory_recall tool was not registered");
|
||||
@@ -2893,7 +2897,7 @@ describe("memory plugin e2e", () => {
|
||||
resolvePath: (filePath: string) => filePath,
|
||||
};
|
||||
|
||||
dynamicMemoryPlugin.register(mockApi as any);
|
||||
registerTestPlugin(dynamicMemoryPlugin, mockApi);
|
||||
const storeTool = registeredTools.find((t) => t.opts?.name === "memory_store")?.tool;
|
||||
if (!storeTool) {
|
||||
throw new Error("memory_store tool was not registered");
|
||||
@@ -3028,7 +3032,7 @@ describe("memory plugin e2e", () => {
|
||||
resolvePath: (p: string) => p,
|
||||
};
|
||||
|
||||
memoryPluginLocal.register(mockApi as any);
|
||||
registerTestPlugin(memoryPluginLocal, mockApi);
|
||||
const forgetTool = registeredTools.find((t) => t.opts?.name === "memory_forget")?.tool;
|
||||
if (!forgetTool) {
|
||||
throw new Error("expected memory_forget tool registration");
|
||||
|
||||
@@ -62,7 +62,7 @@ describeLive("memory plugin live tests", () => {
|
||||
};
|
||||
|
||||
// Register plugin
|
||||
memoryPlugin.register(mockApi as any);
|
||||
memoryPlugin.register(mockApi as unknown as Parameters<typeof memoryPlugin.register>[0]);
|
||||
|
||||
// Check registration
|
||||
expect(registeredTools.length).toBe(3);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { MSTeamsConfig } from "../runtime-api.js";
|
||||
import { readAccessToken } from "./token-response.js";
|
||||
import {
|
||||
hasConfiguredMSTeamsCredentials,
|
||||
@@ -73,12 +74,16 @@ describe("token – secret credentials", () => {
|
||||
afterEach(restoreEnv);
|
||||
|
||||
it("returns true when appId + appPassword + tenantId are provided in config", () => {
|
||||
const cfg = { appId: "app-id", appPassword: "app-pw", tenantId: "tenant-id" } as any;
|
||||
const cfg = {
|
||||
appId: "app-id",
|
||||
appPassword: "app-pw",
|
||||
tenantId: "tenant-id",
|
||||
} satisfies MSTeamsConfig;
|
||||
expect(hasConfiguredMSTeamsCredentials(cfg)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when appPassword is missing", () => {
|
||||
const cfg = { appId: "app-id", tenantId: "tenant-id" } as any;
|
||||
const cfg = { appId: "app-id", tenantId: "tenant-id" } satisfies MSTeamsConfig;
|
||||
expect(hasConfiguredMSTeamsCredentials(cfg)).toBe(false);
|
||||
});
|
||||
|
||||
@@ -87,7 +92,11 @@ describe("token – secret credentials", () => {
|
||||
});
|
||||
|
||||
it("resolves secret credentials from config", () => {
|
||||
const cfg = { appId: "app-id", appPassword: "app-pw", tenantId: "tenant-id" } as any;
|
||||
const cfg = {
|
||||
appId: "app-id",
|
||||
appPassword: "app-pw",
|
||||
tenantId: "tenant-id",
|
||||
} satisfies MSTeamsConfig;
|
||||
const result = resolveMSTeamsCredentials(cfg);
|
||||
expect(result).toEqual({
|
||||
type: "secret",
|
||||
@@ -111,7 +120,7 @@ describe("token – secret credentials", () => {
|
||||
});
|
||||
|
||||
it("returns undefined when appPassword is missing", () => {
|
||||
const cfg = { appId: "app-id", tenantId: "tenant-id" } as any;
|
||||
const cfg = { appId: "app-id", tenantId: "tenant-id" } satisfies MSTeamsConfig;
|
||||
expect(resolveMSTeamsCredentials(cfg)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -126,12 +135,16 @@ describe("token – federated credentials (certificate)", () => {
|
||||
tenantId: "tenant-id",
|
||||
authType: "federated",
|
||||
certificatePath: "/cert.pem",
|
||||
} as any;
|
||||
} satisfies MSTeamsConfig;
|
||||
expect(hasConfiguredMSTeamsCredentials(cfg)).toBe(true);
|
||||
});
|
||||
|
||||
it("hasConfigured returns false when neither cert nor MI is provided", () => {
|
||||
const cfg = { appId: "app-id", tenantId: "tenant-id", authType: "federated" } as any;
|
||||
const cfg = {
|
||||
appId: "app-id",
|
||||
tenantId: "tenant-id",
|
||||
authType: "federated",
|
||||
} satisfies MSTeamsConfig;
|
||||
expect(hasConfiguredMSTeamsCredentials(cfg)).toBe(false);
|
||||
});
|
||||
|
||||
@@ -142,7 +155,7 @@ describe("token – federated credentials (certificate)", () => {
|
||||
authType: "federated",
|
||||
certificatePath: "/cert.pem",
|
||||
certificateThumbprint: "AABBCCDD",
|
||||
} as any;
|
||||
} satisfies MSTeamsConfig;
|
||||
const result = resolveMSTeamsCredentials(cfg);
|
||||
expect(result).toEqual({
|
||||
type: "federated",
|
||||
@@ -185,7 +198,7 @@ describe("token – federated credentials (managed identity)", () => {
|
||||
authType: "federated",
|
||||
useManagedIdentity: true,
|
||||
managedIdentityClientId: "mi-client-id",
|
||||
} as any;
|
||||
} satisfies MSTeamsConfig;
|
||||
const result = resolveMSTeamsCredentials(cfg);
|
||||
expect(result).toEqual({
|
||||
type: "federated",
|
||||
@@ -204,7 +217,7 @@ describe("token – federated credentials (managed identity)", () => {
|
||||
tenantId: "tenant-id",
|
||||
authType: "federated",
|
||||
useManagedIdentity: true,
|
||||
} as any;
|
||||
} satisfies MSTeamsConfig;
|
||||
const result = resolveMSTeamsCredentials(cfg);
|
||||
expect(result).toEqual({
|
||||
type: "federated",
|
||||
@@ -233,7 +246,7 @@ describe("token – federated credentials (managed identity)", () => {
|
||||
authType: "federated",
|
||||
certificatePath: "/cert.pem",
|
||||
useManagedIdentity: false,
|
||||
} as any;
|
||||
} satisfies MSTeamsConfig;
|
||||
const result = resolveMSTeamsCredentials(cfg);
|
||||
expect(result).toEqual({
|
||||
type: "federated",
|
||||
@@ -252,7 +265,11 @@ describe("token – backward compatibility", () => {
|
||||
afterEach(restoreEnv);
|
||||
|
||||
it("defaults to secret when authType is absent", () => {
|
||||
const cfg = { appId: "app-id", appPassword: "pw", tenantId: "tenant-id" } as any;
|
||||
const cfg = {
|
||||
appId: "app-id",
|
||||
appPassword: "pw",
|
||||
tenantId: "tenant-id",
|
||||
} satisfies MSTeamsConfig;
|
||||
const result = resolveMSTeamsCredentials(cfg);
|
||||
expect(result).toEqual({
|
||||
type: "secret",
|
||||
@@ -268,7 +285,7 @@ describe("token – backward compatibility", () => {
|
||||
appPassword: "pw",
|
||||
tenantId: "tenant-id",
|
||||
authType: "secret",
|
||||
} as any;
|
||||
} satisfies MSTeamsConfig;
|
||||
const result = resolveMSTeamsCredentials(cfg);
|
||||
expect(result).toEqual({
|
||||
type: "secret",
|
||||
|
||||
@@ -9,6 +9,19 @@ import { createOpencodeGoStalledStreamWrapper } from "./stream-termination.js";
|
||||
|
||||
type AnyEvent = AssistantMessageEvent;
|
||||
type StreamLike = AssistantMessageEventStreamContract;
|
||||
type ProviderStreamFn = Parameters<typeof createOpencodeGoStalledStreamWrapper>[0];
|
||||
type ProviderModel = Parameters<ProviderStreamFn>[0];
|
||||
type ProviderContext = Parameters<ProviderStreamFn>[1];
|
||||
type ProviderCallOptions = Parameters<ProviderStreamFn>[2];
|
||||
type ErrorEvent = Extract<AnyEvent, { type: "error" }>;
|
||||
|
||||
function asProviderEvent(event: unknown): AnyEvent {
|
||||
return event as AnyEvent;
|
||||
}
|
||||
|
||||
function asProviderModel(model: unknown): ProviderModel {
|
||||
return model as ProviderModel;
|
||||
}
|
||||
|
||||
interface FakeStreamController {
|
||||
emit(event: AnyEvent): void;
|
||||
@@ -125,13 +138,17 @@ describe("createOpencodeGoStalledStreamWrapper", () => {
|
||||
return baseStream;
|
||||
});
|
||||
|
||||
const wrapper = createOpencodeGoStalledStreamWrapper(underlying as any, {
|
||||
const wrapper = createOpencodeGoStalledStreamWrapper(underlying as ProviderStreamFn, {
|
||||
provider: "opencode-go",
|
||||
idleTimeoutMs: 5_000,
|
||||
});
|
||||
|
||||
const downstream = await Promise.resolve(
|
||||
wrapper({ provider: "opencode-go", id: "deepseek-v4-flash" } as any, {} as any, {} as any),
|
||||
wrapper(
|
||||
{ provider: "opencode-go", id: "deepseek-v4-flash" } as ProviderModel,
|
||||
{} as ProviderContext,
|
||||
{} as ProviderCallOptions,
|
||||
),
|
||||
);
|
||||
expect(downstream).toBeDefined();
|
||||
if (!downstream) {
|
||||
@@ -152,13 +169,15 @@ describe("createOpencodeGoStalledStreamWrapper", () => {
|
||||
content: [{ type: "text", text: "hi" }],
|
||||
stopReason: undefined,
|
||||
};
|
||||
controller.emit({ type: "start", partial } as any);
|
||||
controller.emit({
|
||||
type: "text_delta",
|
||||
contentIndex: 0,
|
||||
delta: "hi",
|
||||
partial,
|
||||
} as any);
|
||||
controller.emit(asProviderEvent({ type: "start", partial }));
|
||||
controller.emit(
|
||||
asProviderEvent({
|
||||
type: "text_delta",
|
||||
contentIndex: 0,
|
||||
delta: "hi",
|
||||
partial,
|
||||
}),
|
||||
);
|
||||
|
||||
// Advance wall clock beyond idleTimeoutMs without any new progress.
|
||||
await vi.advanceTimersByTimeAsync(6_000);
|
||||
@@ -169,10 +188,10 @@ describe("createOpencodeGoStalledStreamWrapper", () => {
|
||||
|
||||
// And it pushed a terminal error event to the downstream consumer.
|
||||
const terminal = received.find(
|
||||
(event) => event.type === "error" && (event as any).reason === "error",
|
||||
(event): event is ErrorEvent => event.type === "error" && event.reason === "error",
|
||||
);
|
||||
expect(terminal).toBeDefined();
|
||||
expect((terminal as any)?.error).toMatchObject({
|
||||
expect(terminal?.error).toMatchObject({
|
||||
stopReason: "error",
|
||||
errorMessage: "opencode-go stream timed out after provider-owned SSE boundary stalled",
|
||||
});
|
||||
@@ -195,14 +214,18 @@ describe("createOpencodeGoStalledStreamWrapper", () => {
|
||||
return baseStream;
|
||||
});
|
||||
|
||||
const wrapper = createOpencodeGoStalledStreamWrapper(underlying as any, {
|
||||
const wrapper = createOpencodeGoStalledStreamWrapper(underlying as ProviderStreamFn, {
|
||||
provider: "opencode-go",
|
||||
idleTimeoutMs: 5_000,
|
||||
firstEventTimeoutMs: 10_000,
|
||||
});
|
||||
|
||||
const downstream = await Promise.resolve(
|
||||
wrapper({ provider: "opencode-go", id: "deepseek-v4-flash" } as any, {} as any, {} as any),
|
||||
wrapper(
|
||||
{ provider: "opencode-go", id: "deepseek-v4-flash" } as ProviderModel,
|
||||
{} as ProviderContext,
|
||||
{} as ProviderCallOptions,
|
||||
),
|
||||
);
|
||||
expect(downstream).toBeDefined();
|
||||
if (!downstream) {
|
||||
@@ -236,14 +259,18 @@ describe("createOpencodeGoStalledStreamWrapper", () => {
|
||||
return baseStream;
|
||||
});
|
||||
|
||||
const wrapper = createOpencodeGoStalledStreamWrapper(underlying as any, {
|
||||
const wrapper = createOpencodeGoStalledStreamWrapper(underlying as ProviderStreamFn, {
|
||||
provider: "opencode-go",
|
||||
idleTimeoutMs: 5_000,
|
||||
firstEventTimeoutMs: 10_000,
|
||||
});
|
||||
|
||||
const downstream = await Promise.resolve(
|
||||
wrapper({ provider: "opencode-go", id: "deepseek-v4-flash" } as any, {} as any, {} as any),
|
||||
wrapper(
|
||||
{ provider: "opencode-go", id: "deepseek-v4-flash" } as ProviderModel,
|
||||
{} as ProviderContext,
|
||||
{} as ProviderCallOptions,
|
||||
),
|
||||
);
|
||||
expect(downstream).toBeDefined();
|
||||
if (!downstream) {
|
||||
@@ -262,20 +289,22 @@ describe("createOpencodeGoStalledStreamWrapper", () => {
|
||||
content: [],
|
||||
stopReason: undefined,
|
||||
};
|
||||
controller.emit({ type: "start", partial } as any);
|
||||
controller.emit(asProviderEvent({ type: "start", partial }));
|
||||
|
||||
await vi.advanceTimersByTimeAsync(6_000);
|
||||
expect(abortCalled).toBe(false);
|
||||
|
||||
controller.emit({
|
||||
type: "text_delta",
|
||||
contentIndex: 0,
|
||||
delta: "hello",
|
||||
partial: {
|
||||
...partial,
|
||||
content: [{ type: "text", text: "hello" }],
|
||||
},
|
||||
} as any);
|
||||
controller.emit(
|
||||
asProviderEvent({
|
||||
type: "text_delta",
|
||||
contentIndex: 0,
|
||||
delta: "hello",
|
||||
partial: {
|
||||
...partial,
|
||||
content: [{ type: "text", text: "hello" }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
controller.emit({
|
||||
type: "done",
|
||||
reason: "stop",
|
||||
@@ -284,7 +313,7 @@ describe("createOpencodeGoStalledStreamWrapper", () => {
|
||||
content: [{ type: "text", text: "hello" }],
|
||||
stopReason: "stop",
|
||||
},
|
||||
} as any);
|
||||
} as AnyEvent);
|
||||
await consumer;
|
||||
|
||||
expect(abortCalled).toBe(false);
|
||||
@@ -305,14 +334,18 @@ describe("createOpencodeGoStalledStreamWrapper", () => {
|
||||
return baseStream;
|
||||
});
|
||||
|
||||
const wrapper = createOpencodeGoStalledStreamWrapper(underlying as any, {
|
||||
const wrapper = createOpencodeGoStalledStreamWrapper(underlying as ProviderStreamFn, {
|
||||
provider: "opencode-go",
|
||||
idleTimeoutMs: 5_000,
|
||||
firstEventTimeoutMs: 10_000,
|
||||
});
|
||||
|
||||
const downstream = await Promise.resolve(
|
||||
wrapper({ provider: "opencode-go", id: "deepseek-v4-flash" } as any, {} as any, {} as any),
|
||||
wrapper(
|
||||
{ provider: "opencode-go", id: "deepseek-v4-flash" } as ProviderModel,
|
||||
{} as ProviderContext,
|
||||
{} as ProviderCallOptions,
|
||||
),
|
||||
);
|
||||
expect(downstream).toBeDefined();
|
||||
if (!downstream) {
|
||||
@@ -331,8 +364,8 @@ describe("createOpencodeGoStalledStreamWrapper", () => {
|
||||
content: [{ type: "text", text: "" }],
|
||||
stopReason: undefined,
|
||||
};
|
||||
controller.emit({ type: "start", partial } as any);
|
||||
controller.emit({ type: "text_start", contentIndex: 0, partial } as any);
|
||||
controller.emit(asProviderEvent({ type: "start", partial }));
|
||||
controller.emit(asProviderEvent({ type: "text_start", contentIndex: 0, partial }));
|
||||
|
||||
await vi.advanceTimersByTimeAsync(6_000);
|
||||
expect(abortCalled).toBe(false);
|
||||
@@ -347,8 +380,8 @@ describe("createOpencodeGoStalledStreamWrapper", () => {
|
||||
contentIndex: 0,
|
||||
delta: "hello",
|
||||
partial: message,
|
||||
} as any);
|
||||
controller.emit({ type: "done", reason: "stop", message } as any);
|
||||
} as AnyEvent);
|
||||
controller.emit({ type: "done", reason: "stop", message } as AnyEvent);
|
||||
await consumer;
|
||||
|
||||
expect(abortCalled).toBe(false);
|
||||
@@ -369,7 +402,7 @@ describe("createOpencodeGoStalledStreamWrapper", () => {
|
||||
return baseStream;
|
||||
});
|
||||
|
||||
const wrapper = createOpencodeGoStalledStreamWrapper(underlying as any, {
|
||||
const wrapper = createOpencodeGoStalledStreamWrapper(underlying as ProviderStreamFn, {
|
||||
provider: "opencode-go",
|
||||
idleTimeoutMs: 5_000,
|
||||
firstEventTimeoutMs: 5_000,
|
||||
@@ -377,9 +410,13 @@ describe("createOpencodeGoStalledStreamWrapper", () => {
|
||||
|
||||
const downstream = await Promise.resolve(
|
||||
wrapper(
|
||||
{ provider: "opencode-go", id: "deepseek-v4-flash", requestTimeoutMs: 10_000 } as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
asProviderModel({
|
||||
provider: "opencode-go",
|
||||
id: "deepseek-v4-flash",
|
||||
requestTimeoutMs: 10_000,
|
||||
}),
|
||||
{} as ProviderContext,
|
||||
{} as ProviderCallOptions,
|
||||
),
|
||||
);
|
||||
expect(downstream).toBeDefined();
|
||||
@@ -398,7 +435,7 @@ describe("createOpencodeGoStalledStreamWrapper", () => {
|
||||
content: [{ type: "text", text: "slow" }],
|
||||
stopReason: undefined,
|
||||
};
|
||||
controller.emit({ type: "start", partial } as any);
|
||||
controller.emit(asProviderEvent({ type: "start", partial }));
|
||||
|
||||
await vi.advanceTimersByTimeAsync(6_000);
|
||||
expect(abortCalled).toBe(false);
|
||||
@@ -413,7 +450,7 @@ describe("createOpencodeGoStalledStreamWrapper", () => {
|
||||
|
||||
const underlying = vi.fn((_model, _context, _options) => baseStream);
|
||||
|
||||
const wrapper = createOpencodeGoStalledStreamWrapper(underlying as any, {
|
||||
const wrapper = createOpencodeGoStalledStreamWrapper(underlying as ProviderStreamFn, {
|
||||
provider: "opencode-go",
|
||||
idleTimeoutMs: 120_000,
|
||||
firstEventTimeoutMs: 300_000,
|
||||
@@ -421,9 +458,9 @@ describe("createOpencodeGoStalledStreamWrapper", () => {
|
||||
|
||||
const downstream = await Promise.resolve(
|
||||
wrapper(
|
||||
{ provider: "opencode-go", id: "deepseek-v4-flash" } as any,
|
||||
{} as any,
|
||||
{ firstEventTimeoutMs: 30_000 } as any,
|
||||
{ provider: "opencode-go", id: "deepseek-v4-flash" } as ProviderModel,
|
||||
{} as ProviderContext,
|
||||
{ firstEventTimeoutMs: 30_000 } as ProviderCallOptions,
|
||||
),
|
||||
);
|
||||
expect(downstream).toBeDefined();
|
||||
@@ -459,7 +496,7 @@ describe("createOpencodeGoStalledStreamWrapper", () => {
|
||||
return baseStream;
|
||||
});
|
||||
|
||||
const wrapper = createOpencodeGoStalledStreamWrapper(underlying as any, {
|
||||
const wrapper = createOpencodeGoStalledStreamWrapper(underlying as ProviderStreamFn, {
|
||||
provider: "opencode-go",
|
||||
idleTimeoutMs: 5_000,
|
||||
firstEventTimeoutMs: 10_000,
|
||||
@@ -467,9 +504,13 @@ describe("createOpencodeGoStalledStreamWrapper", () => {
|
||||
|
||||
const downstream = await Promise.resolve(
|
||||
wrapper(
|
||||
{ provider: "opencode-go", id: "deepseek-v4-flash", requestTimeoutMs: 2_000 } as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
asProviderModel({
|
||||
provider: "opencode-go",
|
||||
id: "deepseek-v4-flash",
|
||||
requestTimeoutMs: 2_000,
|
||||
}),
|
||||
{} as ProviderContext,
|
||||
{} as ProviderCallOptions,
|
||||
),
|
||||
);
|
||||
expect(downstream).toBeDefined();
|
||||
@@ -503,13 +544,17 @@ describe("createOpencodeGoStalledStreamWrapper", () => {
|
||||
return baseStream;
|
||||
});
|
||||
|
||||
const wrapper = createOpencodeGoStalledStreamWrapper(underlying as any, {
|
||||
const wrapper = createOpencodeGoStalledStreamWrapper(underlying as ProviderStreamFn, {
|
||||
provider: "opencode-go",
|
||||
idleTimeoutMs: 5_000,
|
||||
});
|
||||
|
||||
const downstream = await Promise.resolve(
|
||||
wrapper({ provider: "opencode-go", id: "deepseek-v4-flash" } as any, {} as any, {} as any),
|
||||
wrapper(
|
||||
{ provider: "opencode-go", id: "deepseek-v4-flash" } as ProviderModel,
|
||||
{} as ProviderContext,
|
||||
{} as ProviderCallOptions,
|
||||
),
|
||||
);
|
||||
expect(downstream).toBeDefined();
|
||||
if (!downstream) {
|
||||
@@ -528,9 +573,7 @@ describe("createOpencodeGoStalledStreamWrapper", () => {
|
||||
expect(capturedSignals).toHaveLength(1);
|
||||
expect(abortCalled).toBe(true);
|
||||
expect(getReturnCalls()).toBe(1);
|
||||
expect(
|
||||
received.some((event) => event.type === "error" && (event as any).reason === "error"),
|
||||
).toBe(true);
|
||||
expect(received.some((event) => event.type === "error" && event.reason === "error")).toBe(true);
|
||||
|
||||
await consumer;
|
||||
});
|
||||
@@ -549,13 +592,17 @@ describe("createOpencodeGoStalledStreamWrapper", () => {
|
||||
});
|
||||
});
|
||||
|
||||
const wrapper = createOpencodeGoStalledStreamWrapper(underlying as any, {
|
||||
const wrapper = createOpencodeGoStalledStreamWrapper(underlying as ProviderStreamFn, {
|
||||
provider: "opencode-go",
|
||||
idleTimeoutMs: 5_000,
|
||||
});
|
||||
|
||||
const downstream = await Promise.resolve(
|
||||
wrapper({ provider: "opencode-go", id: "deepseek-v4-flash" } as any, {} as any, {} as any),
|
||||
wrapper(
|
||||
{ provider: "opencode-go", id: "deepseek-v4-flash" } as ProviderModel,
|
||||
{} as ProviderContext,
|
||||
{} as ProviderCallOptions,
|
||||
),
|
||||
);
|
||||
expect(downstream).toBeDefined();
|
||||
if (!downstream) {
|
||||
@@ -572,9 +619,7 @@ describe("createOpencodeGoStalledStreamWrapper", () => {
|
||||
await vi.advanceTimersByTimeAsync(6_000);
|
||||
|
||||
expect(abortCalled).toBe(true);
|
||||
expect(
|
||||
received.some((event) => event.type === "error" && (event as any).reason === "error"),
|
||||
).toBe(true);
|
||||
expect(received.some((event) => event.type === "error" && event.reason === "error")).toBe(true);
|
||||
await consumer;
|
||||
});
|
||||
|
||||
@@ -593,16 +638,16 @@ describe("createOpencodeGoStalledStreamWrapper", () => {
|
||||
return baseStream;
|
||||
});
|
||||
|
||||
const wrapper = createOpencodeGoStalledStreamWrapper(underlying as any, {
|
||||
const wrapper = createOpencodeGoStalledStreamWrapper(underlying as ProviderStreamFn, {
|
||||
provider: "opencode-go",
|
||||
idleTimeoutMs: 5_000,
|
||||
});
|
||||
|
||||
const downstream = await Promise.resolve(
|
||||
wrapper(
|
||||
{ provider: "opencode-go", id: "deepseek-v4-flash" } as any,
|
||||
{} as any,
|
||||
{ signal: new AbortController().signal } as any,
|
||||
{ provider: "opencode-go", id: "deepseek-v4-flash" } as ProviderModel,
|
||||
{} as ProviderContext,
|
||||
{ signal: new AbortController().signal } as ProviderCallOptions,
|
||||
),
|
||||
);
|
||||
expect(downstream).toBeDefined();
|
||||
@@ -633,16 +678,19 @@ describe("createOpencodeGoStalledStreamWrapper", () => {
|
||||
const { stream: baseStream, controller } = createFakeBaseStream();
|
||||
|
||||
try {
|
||||
const wrapper = createOpencodeGoStalledStreamWrapper(vi.fn(() => baseStream) as any, {
|
||||
provider: "opencode-go",
|
||||
idleTimeoutMs: 5_000,
|
||||
});
|
||||
const wrapper = createOpencodeGoStalledStreamWrapper(
|
||||
vi.fn(() => baseStream) as ProviderStreamFn,
|
||||
{
|
||||
provider: "opencode-go",
|
||||
idleTimeoutMs: 5_000,
|
||||
},
|
||||
);
|
||||
|
||||
const downstream = await Promise.resolve(
|
||||
wrapper(
|
||||
{ provider: "opencode-go", id: "deepseek-v4-flash" } as any,
|
||||
{} as any,
|
||||
{ signal: sourceController.signal } as any,
|
||||
{ provider: "opencode-go", id: "deepseek-v4-flash" } as ProviderModel,
|
||||
{} as ProviderContext,
|
||||
{ signal: sourceController.signal } as ProviderCallOptions,
|
||||
),
|
||||
);
|
||||
expect(downstream).toBeDefined();
|
||||
@@ -662,8 +710,8 @@ describe("createOpencodeGoStalledStreamWrapper", () => {
|
||||
content: [{ type: "text", text: "done" }],
|
||||
stopReason: "stop",
|
||||
};
|
||||
controller.emit({ type: "start", partial } as any);
|
||||
controller.emit({ type: "done", reason: "stop", message: partial } as any);
|
||||
controller.emit({ type: "start", partial } as AnyEvent);
|
||||
controller.emit({ type: "done", reason: "stop", message: partial } as AnyEvent);
|
||||
await consumer;
|
||||
|
||||
expect(received.some((event) => event.type === "done")).toBe(true);
|
||||
@@ -695,13 +743,17 @@ describe("createOpencodeGoStalledStreamWrapper", () => {
|
||||
return baseStream;
|
||||
});
|
||||
|
||||
const wrapper = createOpencodeGoStalledStreamWrapper(underlying as any, {
|
||||
const wrapper = createOpencodeGoStalledStreamWrapper(underlying as ProviderStreamFn, {
|
||||
provider: "opencode-go",
|
||||
idleTimeoutMs: 5_000,
|
||||
});
|
||||
|
||||
const downstream = await Promise.resolve(
|
||||
wrapper({ provider: "opencode-go", id: "deepseek-v4-flash" } as any, {} as any, {} as any),
|
||||
wrapper(
|
||||
{ provider: "opencode-go", id: "deepseek-v4-flash" } as ProviderModel,
|
||||
{} as ProviderContext,
|
||||
{} as ProviderCallOptions,
|
||||
),
|
||||
);
|
||||
expect(downstream).toBeDefined();
|
||||
if (!downstream) {
|
||||
@@ -720,13 +772,13 @@ describe("createOpencodeGoStalledStreamWrapper", () => {
|
||||
content: [{ type: "text", text: "hello" }],
|
||||
stopReason: "stop",
|
||||
};
|
||||
controller.emit({ type: "start", partial } as any);
|
||||
controller.emit({ type: "start", partial } as AnyEvent);
|
||||
controller.emit({
|
||||
type: "text_delta",
|
||||
contentIndex: 0,
|
||||
delta: "hello",
|
||||
partial,
|
||||
} as any);
|
||||
} as AnyEvent);
|
||||
|
||||
// Simulate a delayed final chunk after a short (sub-timeout) quiet gap.
|
||||
await vi.advanceTimersByTimeAsync(2_000);
|
||||
@@ -736,7 +788,7 @@ describe("createOpencodeGoStalledStreamWrapper", () => {
|
||||
type: "done",
|
||||
reason: "stop",
|
||||
message: partial,
|
||||
} as any);
|
||||
} as AnyEvent);
|
||||
|
||||
// Advance well past the idle timeout — wrapper should NOT have fired.
|
||||
await vi.advanceTimersByTimeAsync(10_000);
|
||||
@@ -770,13 +822,17 @@ describe("createOpencodeGoStalledStreamWrapper", () => {
|
||||
});
|
||||
|
||||
const idleTimeoutMs = 5_000;
|
||||
const wrapper = createOpencodeGoStalledStreamWrapper(underlying as any, {
|
||||
const wrapper = createOpencodeGoStalledStreamWrapper(underlying as ProviderStreamFn, {
|
||||
provider: "opencode-go",
|
||||
idleTimeoutMs,
|
||||
});
|
||||
|
||||
const downstream = await Promise.resolve(
|
||||
wrapper({ provider: "opencode-go", id: "glm-4.6" } as any, {} as any, {} as any),
|
||||
wrapper(
|
||||
{ provider: "opencode-go", id: "glm-4.6" } as ProviderModel,
|
||||
{} as ProviderContext,
|
||||
{} as ProviderCallOptions,
|
||||
),
|
||||
);
|
||||
expect(downstream).toBeDefined();
|
||||
if (!downstream) {
|
||||
@@ -793,31 +849,33 @@ describe("createOpencodeGoStalledStreamWrapper", () => {
|
||||
const partial = { role: "assistant", content: [{ type: "text", text: "x" }] };
|
||||
|
||||
// Provider starts producing a tool-call turn. The last *delta* arms the idle timer.
|
||||
controller.emit({ type: "start", partial } as any);
|
||||
controller.emit({ type: "start", partial } as AnyEvent);
|
||||
controller.emit({
|
||||
type: "toolcall_delta",
|
||||
contentIndex: 0,
|
||||
delta: "{",
|
||||
partial,
|
||||
} as any);
|
||||
} as AnyEvent);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
// The model finalizes the tool call and deliberates on the next one,
|
||||
// emitting real block-boundary events that prove the SSE socket is alive.
|
||||
// Each gap is < idleTimeoutMs, so a liveness-aware watchdog must stay armed.
|
||||
await vi.advanceTimersByTimeAsync(3_000);
|
||||
controller.emit({
|
||||
type: "toolcall_end",
|
||||
contentIndex: 0,
|
||||
toolCall: { name: "f", arguments: "{}" },
|
||||
partial,
|
||||
} as any);
|
||||
controller.emit(
|
||||
asProviderEvent({
|
||||
type: "toolcall_end",
|
||||
contentIndex: 0,
|
||||
toolCall: { name: "f", arguments: "{}" },
|
||||
partial,
|
||||
}),
|
||||
);
|
||||
await vi.advanceTimersByTimeAsync(3_000);
|
||||
controller.emit({
|
||||
type: "toolcall_start",
|
||||
contentIndex: 1,
|
||||
partial,
|
||||
} as any);
|
||||
} as AnyEvent);
|
||||
|
||||
// Advance to 5s after the last delta, but only 2s after the last
|
||||
// boundary event. The idle timer should have been re-armed by the
|
||||
@@ -833,14 +891,14 @@ describe("createOpencodeGoStalledStreamWrapper", () => {
|
||||
content: [{ type: "text", text: "final answer" }],
|
||||
stopReason: "stop",
|
||||
},
|
||||
} as any);
|
||||
} as AnyEvent);
|
||||
controller.end();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
await consumer;
|
||||
|
||||
const hasDone = received.some((e) => e.type === "done");
|
||||
const hasStalledError = received.some(
|
||||
(e) => e.type === "error" && (e as any).error?.stopReason === "error",
|
||||
(e) => e.type === "error" && e.error?.stopReason === "error",
|
||||
);
|
||||
|
||||
expect(abortCalled).toBe(false);
|
||||
|
||||
@@ -11,30 +11,28 @@ import {
|
||||
import * as containerClientModule from "./client-container.js";
|
||||
import * as nativeClientModule from "./client.js";
|
||||
|
||||
const mockNativeCheck = vi.fn();
|
||||
const mockNativeRpcRequest = vi.fn();
|
||||
const mockNativeStreamEvents = vi.fn();
|
||||
const mockContainerCheck = vi.fn();
|
||||
const mockContainerRpcRequest = vi.fn();
|
||||
const mockContainerFetchAttachment = vi.fn();
|
||||
const mockStreamContainerEvents = vi.fn();
|
||||
const mockNativeCheck = vi.fn<typeof nativeClientModule.signalCheck>();
|
||||
const mockNativeRpcRequest = vi.fn<typeof nativeClientModule.signalRpcRequest>();
|
||||
const mockNativeStreamEvents = vi.fn<typeof nativeClientModule.streamSignalEvents>();
|
||||
const mockContainerCheck = vi.fn<typeof containerClientModule.containerCheck>();
|
||||
const mockContainerRpcRequest = vi.fn<typeof containerClientModule.containerRpcRequest>();
|
||||
const mockContainerFetchAttachment = vi.fn<typeof containerClientModule.containerFetchAttachment>();
|
||||
const mockStreamContainerEvents = vi.fn<typeof containerClientModule.streamContainerEvents>();
|
||||
let currentApiMode: SignalApiMode = "auto";
|
||||
|
||||
beforeEach(() => {
|
||||
vi.spyOn(nativeClientModule, "signalCheck").mockImplementation(mockNativeCheck as any);
|
||||
vi.spyOn(nativeClientModule, "signalRpcRequest").mockImplementation(mockNativeRpcRequest as any);
|
||||
vi.spyOn(nativeClientModule, "streamSignalEvents").mockImplementation(
|
||||
mockNativeStreamEvents as any,
|
||||
);
|
||||
vi.spyOn(containerClientModule, "containerCheck").mockImplementation(mockContainerCheck as any);
|
||||
vi.spyOn(nativeClientModule, "signalCheck").mockImplementation(mockNativeCheck);
|
||||
vi.spyOn(nativeClientModule, "signalRpcRequest").mockImplementation(mockNativeRpcRequest);
|
||||
vi.spyOn(nativeClientModule, "streamSignalEvents").mockImplementation(mockNativeStreamEvents);
|
||||
vi.spyOn(containerClientModule, "containerCheck").mockImplementation(mockContainerCheck);
|
||||
vi.spyOn(containerClientModule, "containerRpcRequest").mockImplementation(
|
||||
mockContainerRpcRequest as any,
|
||||
mockContainerRpcRequest,
|
||||
);
|
||||
vi.spyOn(containerClientModule, "containerFetchAttachment").mockImplementation(
|
||||
mockContainerFetchAttachment as any,
|
||||
mockContainerFetchAttachment,
|
||||
);
|
||||
vi.spyOn(containerClientModule, "streamContainerEvents").mockImplementation(
|
||||
mockStreamContainerEvents as any,
|
||||
mockStreamContainerEvents,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -198,7 +198,7 @@ describe("signal createSignalEventHandler inbound context", () => {
|
||||
it("sets ReplyToId from the inbound Signal timestamp", async () => {
|
||||
const handler = createSignalEventHandler(
|
||||
createBaseSignalEventHandlerDeps({
|
||||
cfg: { messages: { inbound: { debounceMs: 0 } } } as any,
|
||||
cfg: { messages: { inbound: { debounceMs: 0 } } } as OpenClawConfig,
|
||||
historyLimit: 0,
|
||||
}),
|
||||
);
|
||||
@@ -246,7 +246,7 @@ describe("signal createSignalEventHandler inbound context", () => {
|
||||
])("falls back to $name timestamp for native reply metadata", async ({ envelope }) => {
|
||||
const handler = createSignalEventHandler(
|
||||
createBaseSignalEventHandlerDeps({
|
||||
cfg: { messages: { inbound: { debounceMs: 0 } } } as any,
|
||||
cfg: { messages: { inbound: { debounceMs: 0 } } } as OpenClawConfig,
|
||||
historyLimit: 0,
|
||||
}),
|
||||
);
|
||||
@@ -269,7 +269,7 @@ describe("signal createSignalEventHandler inbound context", () => {
|
||||
it("uses editMessage.targetSentTimestamp as the native reply target", async () => {
|
||||
const handler = createSignalEventHandler(
|
||||
createBaseSignalEventHandlerDeps({
|
||||
cfg: { messages: { inbound: { debounceMs: 0 } } } as any,
|
||||
cfg: { messages: { inbound: { debounceMs: 0 } } } as OpenClawConfig,
|
||||
historyLimit: 0,
|
||||
}),
|
||||
);
|
||||
@@ -319,7 +319,7 @@ describe("signal createSignalEventHandler inbound context", () => {
|
||||
cfg: {
|
||||
messages: { inbound: { debounceMs: 10 } },
|
||||
channels: { signal: { replyToMode: "batched" } },
|
||||
} as any,
|
||||
} as OpenClawConfig,
|
||||
deliverReplies: deliverRepliesMock,
|
||||
historyLimit: 0,
|
||||
}),
|
||||
@@ -375,7 +375,7 @@ describe("signal createSignalEventHandler inbound context", () => {
|
||||
session: { dmScope: "per-channel-peer" },
|
||||
messages: { inbound: { debounceMs: 0 } },
|
||||
channels: { signal: { dmPolicy: "open", allowFrom: ["*"] } },
|
||||
} as any,
|
||||
} as OpenClawConfig,
|
||||
historyLimit: 0,
|
||||
}),
|
||||
);
|
||||
@@ -416,7 +416,7 @@ describe("signal createSignalEventHandler inbound context", () => {
|
||||
it("keeps direct chat text in BodyForAgent while Body remains the legacy envelope", async () => {
|
||||
const handler = createSignalEventHandler(
|
||||
createBaseSignalEventHandlerDeps({
|
||||
cfg: { messages: { inbound: { debounceMs: 0 } } } as any,
|
||||
cfg: { messages: { inbound: { debounceMs: 0 } } } as OpenClawConfig,
|
||||
historyLimit: 0,
|
||||
}),
|
||||
);
|
||||
@@ -475,7 +475,7 @@ describe("signal createSignalEventHandler inbound context", () => {
|
||||
},
|
||||
},
|
||||
channels: { signal: { dmPolicy: "open", allowFrom: ["*"] } },
|
||||
} as any,
|
||||
} as OpenClawConfig,
|
||||
historyLimit: 0,
|
||||
}),
|
||||
);
|
||||
@@ -551,7 +551,7 @@ describe("signal createSignalEventHandler inbound context", () => {
|
||||
},
|
||||
},
|
||||
channels: { signal: { dmPolicy: "open", allowFrom: ["*"] } },
|
||||
} as any,
|
||||
} as OpenClawConfig,
|
||||
historyLimit: 0,
|
||||
}),
|
||||
);
|
||||
@@ -626,7 +626,7 @@ describe("signal createSignalEventHandler inbound context", () => {
|
||||
},
|
||||
},
|
||||
channels: { signal: { dmPolicy: "open", allowFrom: ["*"] } },
|
||||
} as any,
|
||||
} as OpenClawConfig,
|
||||
historyLimit: 0,
|
||||
}),
|
||||
);
|
||||
@@ -692,7 +692,7 @@ describe("signal createSignalEventHandler inbound context", () => {
|
||||
},
|
||||
},
|
||||
channels: { signal: { dmPolicy: "open", allowFrom: ["*"] } },
|
||||
} as any,
|
||||
} as OpenClawConfig,
|
||||
historyLimit: 0,
|
||||
}),
|
||||
);
|
||||
@@ -758,7 +758,7 @@ describe("signal createSignalEventHandler inbound context", () => {
|
||||
},
|
||||
},
|
||||
channels: { signal: { dmPolicy: "open", allowFrom: ["*"] } },
|
||||
} as any,
|
||||
} as OpenClawConfig,
|
||||
historyLimit: 0,
|
||||
}),
|
||||
);
|
||||
@@ -814,7 +814,7 @@ describe("signal createSignalEventHandler inbound context", () => {
|
||||
},
|
||||
},
|
||||
channels: { signal: { dmPolicy: "open", allowFrom: ["*"] } },
|
||||
} as any,
|
||||
} as OpenClawConfig,
|
||||
sendReadReceipts: true,
|
||||
historyLimit: 0,
|
||||
}),
|
||||
@@ -866,7 +866,7 @@ describe("signal createSignalEventHandler inbound context", () => {
|
||||
statusReactions: { enabled: true },
|
||||
},
|
||||
channels: { signal: { dmPolicy: "open", allowFrom: ["*"] } },
|
||||
} as any,
|
||||
} as OpenClawConfig,
|
||||
historyLimit: 0,
|
||||
}),
|
||||
);
|
||||
@@ -900,7 +900,7 @@ describe("signal createSignalEventHandler inbound context", () => {
|
||||
statusReactions: { enabled: true },
|
||||
},
|
||||
channels: { signal: { dmPolicy: "open", allowFrom: ["*"] } },
|
||||
} as any,
|
||||
} as OpenClawConfig,
|
||||
historyLimit: 0,
|
||||
}),
|
||||
);
|
||||
@@ -929,7 +929,7 @@ describe("signal createSignalEventHandler inbound context", () => {
|
||||
cfg: {
|
||||
messages: { inbound: { debounceMs: 0 } },
|
||||
channels: { signal: { dmPolicy: "open", allowFrom: ["*"] } },
|
||||
} as any,
|
||||
} as OpenClawConfig,
|
||||
historyLimit: 0,
|
||||
}),
|
||||
);
|
||||
@@ -969,7 +969,7 @@ describe("signal createSignalEventHandler inbound context", () => {
|
||||
reactionLevel: "off",
|
||||
},
|
||||
},
|
||||
} as any,
|
||||
} as OpenClawConfig,
|
||||
historyLimit: 0,
|
||||
}),
|
||||
);
|
||||
@@ -1026,7 +1026,7 @@ describe("signal createSignalEventHandler inbound context", () => {
|
||||
reactionLevel: "ack",
|
||||
},
|
||||
},
|
||||
} as any,
|
||||
} as OpenClawConfig,
|
||||
historyLimit: 0,
|
||||
}),
|
||||
);
|
||||
@@ -1080,7 +1080,7 @@ describe("signal createSignalEventHandler inbound context", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any,
|
||||
} as OpenClawConfig,
|
||||
accountId: "work",
|
||||
historyLimit: 0,
|
||||
}),
|
||||
@@ -1114,7 +1114,7 @@ describe("signal createSignalEventHandler inbound context", () => {
|
||||
statusReactions: { enabled: true },
|
||||
},
|
||||
channels: { signal: { dmPolicy: "open", allowFrom: ["*"] } },
|
||||
} as any,
|
||||
} as OpenClawConfig,
|
||||
historyLimit: 0,
|
||||
}),
|
||||
);
|
||||
@@ -1162,7 +1162,7 @@ describe("signal createSignalEventHandler inbound context", () => {
|
||||
},
|
||||
},
|
||||
channels: { signal: { dmPolicy: "open", allowFrom: ["*"] } },
|
||||
} as any,
|
||||
} as OpenClawConfig,
|
||||
historyLimit: 0,
|
||||
}),
|
||||
);
|
||||
@@ -1219,7 +1219,7 @@ describe("signal createSignalEventHandler inbound context", () => {
|
||||
},
|
||||
},
|
||||
channels: { signal: { dmPolicy: "open", allowFrom: ["*"] } },
|
||||
} as any,
|
||||
} as OpenClawConfig,
|
||||
historyLimit: 0,
|
||||
}),
|
||||
);
|
||||
@@ -1272,7 +1272,7 @@ describe("signal createSignalEventHandler inbound context", () => {
|
||||
groups: { "*": { requireMention: false } },
|
||||
},
|
||||
},
|
||||
} as any,
|
||||
} as OpenClawConfig,
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: ["g1"],
|
||||
historyLimit: 0,
|
||||
@@ -1328,7 +1328,7 @@ describe("signal createSignalEventHandler inbound context", () => {
|
||||
groups: { "*": { requireMention: true } },
|
||||
},
|
||||
},
|
||||
} as any,
|
||||
} as OpenClawConfig,
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: ["g1"],
|
||||
historyLimit: 0,
|
||||
@@ -1380,7 +1380,7 @@ describe("signal createSignalEventHandler inbound context", () => {
|
||||
},
|
||||
},
|
||||
channels: { signal: { dmPolicy: "open", allowFrom: ["*"] } },
|
||||
} as any,
|
||||
} as OpenClawConfig,
|
||||
historyLimit: 0,
|
||||
}),
|
||||
);
|
||||
@@ -1433,7 +1433,7 @@ describe("signal createSignalEventHandler inbound context", () => {
|
||||
},
|
||||
},
|
||||
channels: { signal: { dmPolicy: "open", allowFrom: ["*"] } },
|
||||
} as any,
|
||||
} as OpenClawConfig,
|
||||
historyLimit: 0,
|
||||
}),
|
||||
);
|
||||
@@ -1479,7 +1479,7 @@ describe("signal createSignalEventHandler inbound context", () => {
|
||||
},
|
||||
},
|
||||
channels: { signal: { dmPolicy: "open", allowFrom: ["*"] } },
|
||||
} as any,
|
||||
} as OpenClawConfig,
|
||||
historyLimit: 0,
|
||||
}),
|
||||
);
|
||||
@@ -1523,7 +1523,7 @@ describe("signal createSignalEventHandler inbound context", () => {
|
||||
]);
|
||||
const handler = createSignalEventHandler(
|
||||
createBaseSignalEventHandlerDeps({
|
||||
cfg: { messages: { inbound: { debounceMs: 0 } } } as any,
|
||||
cfg: { messages: { inbound: { debounceMs: 0 } } } as OpenClawConfig,
|
||||
groupHistories,
|
||||
historyLimit: 5,
|
||||
}),
|
||||
@@ -1823,7 +1823,7 @@ describe("signal createSignalEventHandler inbound context", () => {
|
||||
};
|
||||
const handler = createSignalEventHandler(
|
||||
createBaseSignalEventHandlerDeps({
|
||||
cfg: cfg as any,
|
||||
cfg: cfg as OpenClawConfig,
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: [],
|
||||
reactionMode: "all",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
// Signal tests cover retry behavior for reply session initialization conflicts.
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { SignalEventHandlerDeps } from "./event-handler.types.js";
|
||||
|
||||
const [
|
||||
{ createBaseSignalEventHandlerDeps, createSignalReceiveEvent },
|
||||
@@ -240,7 +242,7 @@ describe("signal reply session init conflict retry", () => {
|
||||
error: (msg: string) => {
|
||||
errorLogs.push(msg);
|
||||
},
|
||||
} as any,
|
||||
} as SignalEventHandlerDeps["runtime"],
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -313,7 +315,7 @@ describe("signal reply session init conflict retry", () => {
|
||||
createBaseSignalEventHandlerDeps({
|
||||
cfg: {
|
||||
messages: { inbound: { debounceMs: 10 } },
|
||||
} as any,
|
||||
} as OpenClawConfig,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ export function createBaseSignalEventHandlerDeps(
|
||||
overrides: Partial<SignalEventHandlerDeps> = {},
|
||||
): SignalEventHandlerDeps {
|
||||
return {
|
||||
runtime: { log: () => {}, error: () => {} } as any,
|
||||
runtime: { log: () => {}, error: () => {} } as SignalEventHandlerDeps["runtime"],
|
||||
cfg: {},
|
||||
baseUrl: "http://localhost",
|
||||
accountId: "default",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
// Slack tests cover group policy plugin behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveSlackGroupRequireMention, resolveSlackGroupToolPolicy } from "./group-policy.js";
|
||||
@@ -22,7 +23,7 @@ const cfg = {
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
} as OpenClawConfig;
|
||||
|
||||
describe("slack group policy", () => {
|
||||
it("uses matched channel requireMention and wildcard fallback", () => {
|
||||
|
||||
@@ -114,7 +114,7 @@ describe("slack prepareSlackMessage inbound contract", () => {
|
||||
channels: { slack: { enabled: true } },
|
||||
} as OpenClawConfig,
|
||||
});
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" }) as any;
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" });
|
||||
return slackCtx;
|
||||
}
|
||||
|
||||
@@ -292,7 +292,7 @@ describe("slack prepareSlackMessage inbound contract", () => {
|
||||
ctx.accountId = "soltea";
|
||||
ctx.allowFrom = ["*"];
|
||||
ctx.dmPolicy = "open";
|
||||
ctx.resolveUserName = async () => ({ name: "External User" }) as any;
|
||||
ctx.resolveUserName = async () => ({ name: "External User" });
|
||||
|
||||
const prepared = await prepareSlackMessage({
|
||||
ctx,
|
||||
@@ -796,7 +796,7 @@ describe("slack prepareSlackMessage inbound contract", () => {
|
||||
session: { dmScope: "main" },
|
||||
} as OpenClawConfig,
|
||||
});
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" }) as any;
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" });
|
||||
// Simulate API returning correct type for DM channel
|
||||
slackCtx.resolveChannelName = async () => ({ name: undefined, type: "im" as const });
|
||||
return slackCtx;
|
||||
@@ -817,7 +817,7 @@ describe("slack prepareSlackMessage inbound contract", () => {
|
||||
options?: { includeFromCheck?: boolean },
|
||||
) {
|
||||
assertPrepared(prepared);
|
||||
expectInboundContextContract(prepared.ctxPayload as any);
|
||||
expectInboundContextContract(prepared.ctxPayload);
|
||||
expect(prepared.isDirectMessage).toBe(true);
|
||||
expect(prepared.route.sessionKey).toBe("agent:main:main");
|
||||
expect(prepared.ctxPayload.ChatType).toBe("direct");
|
||||
@@ -851,7 +851,7 @@ describe("slack prepareSlackMessage inbound contract", () => {
|
||||
? {}
|
||||
: { defaultRequireMention: params.defaultRequireMention }),
|
||||
});
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" }) as any;
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" });
|
||||
if (params?.asChannel) {
|
||||
slackCtx.resolveChannelName = async () => ({ name: "general", type: "channel" });
|
||||
}
|
||||
@@ -870,7 +870,7 @@ describe("slack prepareSlackMessage inbound contract", () => {
|
||||
const prepared = await prepareWithDefaultCtx(message);
|
||||
|
||||
assertPrepared(prepared);
|
||||
expectInboundContextContract(prepared.ctxPayload as any);
|
||||
expectInboundContextContract(prepared.ctxPayload);
|
||||
expect(prepared.ctxPayload.GroupSpace).toBe("T1");
|
||||
});
|
||||
|
||||
@@ -885,7 +885,7 @@ describe("slack prepareSlackMessage inbound contract", () => {
|
||||
channels: { slack: { enabled: true } },
|
||||
} as OpenClawConfig,
|
||||
});
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" }) as any;
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" });
|
||||
|
||||
const prepared = await prepareMessageWith(slackCtx, defaultAccount, {
|
||||
channel: "D123",
|
||||
@@ -932,7 +932,7 @@ describe("slack prepareSlackMessage inbound contract", () => {
|
||||
} as OpenClawConfig,
|
||||
replyToMode: "all",
|
||||
});
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" }) as any;
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" });
|
||||
slackCtx.resolveChannelName = async () => ({ name: "general", type: "channel" });
|
||||
|
||||
const prepared = await prepareMessageWith(slackCtx, defaultAccount, {
|
||||
@@ -968,7 +968,7 @@ describe("slack prepareSlackMessage inbound contract", () => {
|
||||
reactions: { add: addReaction },
|
||||
} as unknown as App["client"],
|
||||
});
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" }) as any;
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" });
|
||||
slackCtx.resolveChannelName = async () => ({ name: "general", type: "channel" });
|
||||
|
||||
const prepared = await prepareMessageWith(slackCtx, defaultAccount, {
|
||||
@@ -1010,7 +1010,7 @@ describe("slack prepareSlackMessage inbound contract", () => {
|
||||
} as OpenClawConfig,
|
||||
defaultRequireMention: false,
|
||||
});
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" }) as any;
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" });
|
||||
slackCtx.resolveChannelName = async () => ({ name: "general", type: "channel" });
|
||||
slackCtx.ackReactionScope = "group-all";
|
||||
|
||||
@@ -1048,10 +1048,10 @@ describe("slack prepareSlackMessage inbound contract", () => {
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
appClient: { reactions: { add: reactionAdd } } as any,
|
||||
appClient: { reactions: { add: reactionAdd } } as unknown as App["client"],
|
||||
defaultRequireMention: false,
|
||||
});
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" }) as any;
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" });
|
||||
slackCtx.resolveChannelName = async () => ({ name: "general", type: "channel" });
|
||||
slackCtx.ackReactionScope = "all";
|
||||
|
||||
@@ -1093,7 +1093,7 @@ describe("slack prepareSlackMessage inbound contract", () => {
|
||||
} as OpenClawConfig,
|
||||
defaultRequireMention: false,
|
||||
});
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" }) as any;
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" });
|
||||
slackCtx.resolveChannelName = async () => ({ name: "general", type: "channel" });
|
||||
|
||||
const prepared = await prepareMessageWith(slackCtx, defaultAccount, {
|
||||
@@ -1229,7 +1229,7 @@ Second paragraph should still reach the agent after Slack's preview cutoff.`;
|
||||
} as OpenClawConfig,
|
||||
defaultRequireMention: false,
|
||||
});
|
||||
slackCtx.resolveUserName = async () => ({ name: "Bot" }) as any;
|
||||
slackCtx.resolveUserName = async () => ({ name: "Bot" });
|
||||
|
||||
const account = createSlackAccount({ allowBots: true });
|
||||
const message = createSlackMessage({
|
||||
@@ -1559,7 +1559,7 @@ Second paragraph should still reach the agent after Slack's preview cutoff.`;
|
||||
} as OpenClawConfig,
|
||||
defaultRequireMention: false,
|
||||
});
|
||||
slackCtx.resolveUserName = async () => ({ name: "Bot" }) as any;
|
||||
slackCtx.resolveUserName = async () => ({ name: "Bot" });
|
||||
|
||||
const prepared = await prepareMessageWith(
|
||||
slackCtx,
|
||||
@@ -1799,7 +1799,7 @@ Second paragraph should still reach the agent after Slack's preview cutoff.`;
|
||||
C123: { systemPrompt: "Config prompt" },
|
||||
},
|
||||
});
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" }) as any;
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" });
|
||||
const channelInfo = {
|
||||
name: "general",
|
||||
type: "channel" as const,
|
||||
@@ -2793,7 +2793,7 @@ Second paragraph should still reach the agent after Slack's preview cutoff.`;
|
||||
} as OpenClawConfig,
|
||||
replyToMode: "all",
|
||||
});
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" }) as any;
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" });
|
||||
|
||||
const message = createSlackMessage({ ts: "500.000" });
|
||||
const prepared = await prepareMessageWith(
|
||||
@@ -2816,7 +2816,7 @@ Second paragraph should still reach the agent after Slack's preview cutoff.`;
|
||||
} as OpenClawConfig,
|
||||
replyToMode: "all",
|
||||
});
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" }) as any;
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" });
|
||||
|
||||
const prepared = await prepareMessageWith(
|
||||
slackCtx,
|
||||
@@ -2856,7 +2856,7 @@ Second paragraph should still reach the agent after Slack's preview cutoff.`;
|
||||
} as OpenClawConfig,
|
||||
replyToMode: "all",
|
||||
});
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" }) as any;
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" });
|
||||
|
||||
const prepared = await prepareMessageWith(
|
||||
slackCtx,
|
||||
@@ -2919,7 +2919,7 @@ Second paragraph should still reach the agent after Slack's preview cutoff.`;
|
||||
appClient: { conversations: { replies } } as unknown as App["client"],
|
||||
replyToMode: "all",
|
||||
});
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" }) as any;
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" });
|
||||
|
||||
const prepared = await prepareMessageWith(
|
||||
slackCtx,
|
||||
@@ -2949,7 +2949,7 @@ Second paragraph should still reach the agent after Slack's preview cutoff.`;
|
||||
} as OpenClawConfig,
|
||||
replyToMode: "all",
|
||||
});
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" }) as any;
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" });
|
||||
|
||||
const prepared = await prepareMessageWith(
|
||||
slackCtx,
|
||||
@@ -4365,7 +4365,7 @@ describe("prepareSlackMessage sender prefix", () => {
|
||||
channels: {},
|
||||
slashCommand: { command: "/openclaw", enabled: true },
|
||||
});
|
||||
ctx.resolveUserName = async (id: string) => ({ name: id === "U1" ? "Alice" : "Bek" }) as any;
|
||||
ctx.resolveUserName = async (id: string) => ({ name: id === "U1" ? "Alice" : "Bek" });
|
||||
|
||||
const result = await prepareSenderPrefixMessage(ctx, "<@BOT> hello", "1700000000.0001");
|
||||
|
||||
@@ -4382,8 +4382,9 @@ describe("prepareSlackMessage sender prefix", () => {
|
||||
channels: {},
|
||||
slashCommand: { command: "/openclaw", enabled: true },
|
||||
});
|
||||
ctx.resolveUserName = async (id: string) =>
|
||||
({ name: id === "U1" ? "Alice" : undefined }) as any;
|
||||
ctx.resolveUserName = async (id: string) => ({
|
||||
name: id === "U1" ? "Alice" : undefined,
|
||||
});
|
||||
|
||||
const result = await prepareSenderPrefixMessage(ctx, "<@BOT> hello", "1700000000.0001");
|
||||
|
||||
@@ -4521,7 +4522,7 @@ describe("slack thread.requireExplicitMention", () => {
|
||||
} as OpenClawConfig,
|
||||
threadRequireExplicitMention: requireExplicitMention,
|
||||
});
|
||||
ctx.resolveUserName = async () => ({ name: "Alice" }) as any;
|
||||
ctx.resolveUserName = async () => ({ name: "Alice" });
|
||||
return ctx;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,12 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { SlackMessageEvent } from "../types.js";
|
||||
import { createSlackThreadTsResolver } from "./thread-resolution.js";
|
||||
|
||||
type SlackThreadClient = Parameters<typeof createSlackThreadTsResolver>[0]["client"];
|
||||
|
||||
function createThreadClient(history: ReturnType<typeof vi.fn>): SlackThreadClient {
|
||||
return { conversations: { history } } as unknown as SlackThreadClient;
|
||||
}
|
||||
|
||||
describe("createSlackThreadTsResolver", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
@@ -22,7 +28,7 @@ describe("createSlackThreadTsResolver", () => {
|
||||
messages: [{ ts: "1", thread_ts: "9" }],
|
||||
});
|
||||
const resolver = createSlackThreadTsResolver({
|
||||
client: { conversations: { history: historyMock } } as any,
|
||||
client: createThreadClient(historyMock),
|
||||
cacheTtlMs: 60_000,
|
||||
maxSize: 5,
|
||||
});
|
||||
@@ -42,7 +48,7 @@ describe("createSlackThreadTsResolver", () => {
|
||||
messages: [{ ts: "1" }],
|
||||
});
|
||||
const resolver = createSlackThreadTsResolver({
|
||||
client: { conversations: { history: historyMock } } as any,
|
||||
client: createThreadClient(historyMock),
|
||||
cacheTtlMs: 60_000,
|
||||
maxSize: 5,
|
||||
});
|
||||
|
||||
@@ -19,6 +19,12 @@ import {
|
||||
shouldUseTelegramDmThreadSession,
|
||||
} from "./helpers.js";
|
||||
|
||||
type TelegramMessage = Parameters<typeof normalizeForwardedContext>[0];
|
||||
|
||||
function asMalformedTelegramMessage(message: unknown): TelegramMessage {
|
||||
return message as TelegramMessage;
|
||||
}
|
||||
|
||||
describe("resolveTelegramForumThreadId", () => {
|
||||
it.each([
|
||||
{ isForum: false, messageThreadId: 42 },
|
||||
@@ -332,7 +338,7 @@ describe("normalizeForwardedContext", () => {
|
||||
sender_user: { first_name: "Ada", last_name: "Lovelace", username: "ada", id: 42 },
|
||||
date: 123,
|
||||
},
|
||||
} as any);
|
||||
} as TelegramMessage);
|
||||
expect(ctx?.from).toBe("Ada Lovelace (@ada)");
|
||||
expect(ctx?.fromType).toBe("user");
|
||||
expect(ctx?.fromId).toBe("42");
|
||||
@@ -344,7 +350,7 @@ describe("normalizeForwardedContext", () => {
|
||||
it("handles hidden forward_origin names", () => {
|
||||
const ctx = normalizeForwardedContext({
|
||||
forward_origin: { type: "hidden_user", sender_user_name: "Hidden Name", date: 456 },
|
||||
} as any);
|
||||
} as TelegramMessage);
|
||||
expect(ctx?.from).toBe("Hidden Name");
|
||||
expect(ctx?.fromType).toBe("hidden_user");
|
||||
expect(ctx?.fromTitle).toBe("Hidden Name");
|
||||
@@ -365,7 +371,7 @@ describe("normalizeForwardedContext", () => {
|
||||
author_signature: "Editor",
|
||||
message_id: 42,
|
||||
},
|
||||
} as any);
|
||||
} as TelegramMessage);
|
||||
expect(ctx?.from).toBe("Tech News (Editor)");
|
||||
expect(ctx?.fromType).toBe("channel");
|
||||
expect(ctx?.fromId).toBe("-1001234");
|
||||
@@ -389,7 +395,7 @@ describe("normalizeForwardedContext", () => {
|
||||
date: 600,
|
||||
author_signature: "Admin",
|
||||
},
|
||||
} as any);
|
||||
} as TelegramMessage);
|
||||
expect(ctx?.from).toBe("Discussion Group (Admin)");
|
||||
expect(ctx?.fromType).toBe("chat");
|
||||
expect(ctx?.fromId).toBe("-1005678");
|
||||
@@ -408,7 +414,7 @@ describe("normalizeForwardedContext", () => {
|
||||
author_signature: "New Sig",
|
||||
message_id: 1,
|
||||
},
|
||||
} as any);
|
||||
} as TelegramMessage);
|
||||
expect(ctx?.fromSignature).toBe("New Sig");
|
||||
expect(ctx?.from).toBe("My Channel (New Sig)");
|
||||
});
|
||||
@@ -422,7 +428,7 @@ describe("normalizeForwardedContext", () => {
|
||||
author_signature: " ",
|
||||
message_id: 1,
|
||||
},
|
||||
} as any);
|
||||
} as TelegramMessage);
|
||||
expect(ctx?.fromSignature).toBeUndefined();
|
||||
expect(ctx?.from).toBe("Updates");
|
||||
});
|
||||
@@ -435,7 +441,7 @@ describe("normalizeForwardedContext", () => {
|
||||
date: 900,
|
||||
message_id: 1,
|
||||
},
|
||||
} as any);
|
||||
} as TelegramMessage);
|
||||
expect(ctx?.from).toBe("News");
|
||||
expect(ctx?.fromSignature).toBeUndefined();
|
||||
expect(ctx?.fromChatType).toBe("channel");
|
||||
@@ -444,27 +450,31 @@ describe("normalizeForwardedContext", () => {
|
||||
|
||||
describe("describeReplyTarget", () => {
|
||||
it("returns null when no reply_to_message", () => {
|
||||
const result = describeReplyTarget({
|
||||
message_id: 1,
|
||||
date: 1000,
|
||||
chat: { id: 1, type: "private" },
|
||||
} as any);
|
||||
const result = describeReplyTarget(
|
||||
asMalformedTelegramMessage({
|
||||
message_id: 1,
|
||||
date: 1000,
|
||||
chat: { id: 1, type: "private" },
|
||||
}),
|
||||
);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("extracts basic reply info", () => {
|
||||
const result = describeReplyTarget({
|
||||
message_id: 2,
|
||||
date: 1000,
|
||||
chat: { id: 1, type: "private" },
|
||||
reply_to_message: {
|
||||
message_id: 1,
|
||||
date: 900,
|
||||
const result = describeReplyTarget(
|
||||
asMalformedTelegramMessage({
|
||||
message_id: 2,
|
||||
date: 1000,
|
||||
chat: { id: 1, type: "private" },
|
||||
text: "Original message",
|
||||
from: { id: 42, first_name: "Alice", is_bot: false },
|
||||
},
|
||||
} as any);
|
||||
reply_to_message: {
|
||||
message_id: 1,
|
||||
date: 900,
|
||||
chat: { id: 1, type: "private" },
|
||||
text: "Original message",
|
||||
from: { id: 42, first_name: "Alice", is_bot: false },
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(result?.body).toBe("Original message");
|
||||
expect(result?.sender).toBe("Alice");
|
||||
expect(result?.id).toBe("1");
|
||||
@@ -473,36 +483,40 @@ describe("describeReplyTarget", () => {
|
||||
});
|
||||
|
||||
it("handles non-string reply text gracefully (issue #27201)", () => {
|
||||
const result = describeReplyTarget({
|
||||
message_id: 2,
|
||||
date: 1000,
|
||||
chat: { id: 1, type: "private" },
|
||||
reply_to_message: {
|
||||
message_id: 1,
|
||||
date: 900,
|
||||
chat: { id: 1, type: "private" },
|
||||
// Simulate edge case where text is an unexpected non-string value
|
||||
text: { some: "object" },
|
||||
from: { id: 42, first_name: "Alice", is_bot: false },
|
||||
},
|
||||
} as any);
|
||||
const result = describeReplyTarget(
|
||||
asMalformedTelegramMessage({
|
||||
message_id: 2,
|
||||
date: 1000,
|
||||
chat: { id: 1, type: "private", first_name: "Test" },
|
||||
reply_to_message: {
|
||||
message_id: 1,
|
||||
date: 900,
|
||||
chat: { id: 1, type: "private", first_name: "Test" },
|
||||
// Simulate edge case where text is an unexpected non-string value
|
||||
text: { some: "object" },
|
||||
from: { id: 42, first_name: "Alice", is_bot: false },
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("falls back to caption when reply text is malformed", () => {
|
||||
const result = describeReplyTarget({
|
||||
message_id: 2,
|
||||
date: 1000,
|
||||
chat: { id: 1, type: "private" },
|
||||
reply_to_message: {
|
||||
message_id: 1,
|
||||
date: 900,
|
||||
chat: { id: 1, type: "private" },
|
||||
text: { some: "object" },
|
||||
caption: "Caption body",
|
||||
from: { id: 42, first_name: "Alice", is_bot: false },
|
||||
},
|
||||
} as any);
|
||||
const result = describeReplyTarget(
|
||||
asMalformedTelegramMessage({
|
||||
message_id: 2,
|
||||
date: 1000,
|
||||
chat: { id: 1, type: "private", first_name: "Test" },
|
||||
reply_to_message: {
|
||||
message_id: 1,
|
||||
date: 900,
|
||||
chat: { id: 1, type: "private", first_name: "Test" },
|
||||
text: { some: "object" },
|
||||
caption: "Caption body",
|
||||
from: { id: 42, first_name: "Alice", is_bot: false },
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(result?.body).toBe("Caption body");
|
||||
expect(result?.kind).toBe("reply");
|
||||
});
|
||||
@@ -511,15 +525,15 @@ describe("describeReplyTarget", () => {
|
||||
const result = describeReplyTarget({
|
||||
message_id: 2,
|
||||
date: 1000,
|
||||
chat: { id: 1, type: "private" },
|
||||
chat: { id: 1, type: "private", first_name: "Test" },
|
||||
reply_to_message: {
|
||||
message_id: 1,
|
||||
date: 900,
|
||||
chat: { id: 1, type: "private" },
|
||||
chat: { id: 1, type: "private", first_name: "Test" },
|
||||
rich_message: { blocks: [{ type: "paragraph" }] },
|
||||
from: { id: 42, first_name: "Alice", is_bot: false },
|
||||
},
|
||||
} as any);
|
||||
} as TelegramMessage);
|
||||
|
||||
expect(result?.body).toBe("[unsupported Telegram rich_message received]");
|
||||
expect(result?.quoteSourceText).toBeUndefined();
|
||||
@@ -529,7 +543,7 @@ describe("describeReplyTarget", () => {
|
||||
const result = describeReplyTarget({
|
||||
message_id: 2,
|
||||
date: 1000,
|
||||
chat: { id: 1, type: "private" },
|
||||
chat: { id: 1, type: "private", first_name: "Test" },
|
||||
reply_to_message: {
|
||||
message_id: 1,
|
||||
date: 900,
|
||||
@@ -606,7 +620,7 @@ describe("describeReplyTarget", () => {
|
||||
caption: "PK\x00\x03\x04binary",
|
||||
from: { id: 42, first_name: "Alice", is_bot: false },
|
||||
},
|
||||
} as any);
|
||||
} as TelegramMessage);
|
||||
expect(result?.id).toBe("1");
|
||||
expect(result?.sender).toBe("Alice");
|
||||
expect(result?.body).toBeUndefined();
|
||||
@@ -627,28 +641,30 @@ describe("describeReplyTarget", () => {
|
||||
text: "Original message",
|
||||
from: { id: 42, first_name: "Alice", is_bot: false },
|
||||
},
|
||||
} as any);
|
||||
} as TelegramMessage);
|
||||
expect(result?.body).toBe("Original message");
|
||||
expect(result?.kind).toBe("reply");
|
||||
});
|
||||
|
||||
it("falls back to external reply text when external quote text is binary", () => {
|
||||
const result = describeReplyTarget({
|
||||
message_id: 5,
|
||||
date: 1300,
|
||||
chat: { id: 1, type: "private" },
|
||||
text: "Comment on forwarded message",
|
||||
external_reply: {
|
||||
message_id: 4,
|
||||
date: 1200,
|
||||
const result = describeReplyTarget(
|
||||
asMalformedTelegramMessage({
|
||||
message_id: 5,
|
||||
date: 1300,
|
||||
chat: { id: 1, type: "private" },
|
||||
text: "Forwarded from elsewhere",
|
||||
quote: {
|
||||
text: "PK\x00\x03\x04binary quote",
|
||||
text: "Comment on forwarded message",
|
||||
external_reply: {
|
||||
message_id: 4,
|
||||
date: 1200,
|
||||
chat: { id: 1, type: "private" },
|
||||
text: "Forwarded from elsewhere",
|
||||
quote: {
|
||||
text: "PK\x00\x03\x04binary quote",
|
||||
},
|
||||
from: { id: 123, first_name: "Eve", is_bot: false },
|
||||
},
|
||||
from: { id: 123, first_name: "Eve", is_bot: false },
|
||||
},
|
||||
} as any);
|
||||
}),
|
||||
);
|
||||
expect(result?.body).toBe("Forwarded from elsewhere");
|
||||
expect(result?.kind).toBe("reply");
|
||||
});
|
||||
@@ -679,7 +695,7 @@ describe("describeReplyTarget", () => {
|
||||
date: 500,
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
} as TelegramMessage);
|
||||
expect(result?.body).toBe("This is the forwarded content");
|
||||
expect(result?.id).toBe("2");
|
||||
expect(result?.forwardedFrom?.from).toBe("Bob Smith (@bobsmith)");
|
||||
@@ -707,31 +723,33 @@ describe("describeReplyTarget", () => {
|
||||
author_signature: "Editor",
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
} as TelegramMessage);
|
||||
expect(result?.forwardedFrom?.from).toBe("Tech News (Editor)");
|
||||
expect(result?.forwardedFrom?.fromType).toBe("channel");
|
||||
expect(result?.forwardedFrom?.fromMessageId).toBe(456);
|
||||
});
|
||||
|
||||
it("marks top-level quote metadata on external replies as external targets", () => {
|
||||
const result = describeReplyTarget({
|
||||
message_id: 5,
|
||||
date: 1300,
|
||||
chat: { id: 1, type: "private" },
|
||||
text: "Comment on forwarded message",
|
||||
quote: {
|
||||
text: "quoted slice",
|
||||
position: 4,
|
||||
entities: [{ type: "italic", offset: 0, length: 6 }],
|
||||
},
|
||||
external_reply: {
|
||||
message_id: 4,
|
||||
date: 1200,
|
||||
const result = describeReplyTarget(
|
||||
asMalformedTelegramMessage({
|
||||
message_id: 5,
|
||||
date: 1300,
|
||||
chat: { id: 1, type: "private" },
|
||||
text: "Forwarded from elsewhere",
|
||||
from: { id: 123, first_name: "Eve", is_bot: false },
|
||||
},
|
||||
} as any);
|
||||
text: "Comment on forwarded message",
|
||||
quote: {
|
||||
text: "quoted slice",
|
||||
position: 4,
|
||||
entities: [{ type: "italic", offset: 0, length: 6 }],
|
||||
},
|
||||
external_reply: {
|
||||
message_id: 4,
|
||||
date: 1200,
|
||||
chat: { id: 1, type: "private" },
|
||||
text: "Forwarded from elsewhere",
|
||||
from: { id: 123, first_name: "Eve", is_bot: false },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result?.id).toBe("4");
|
||||
expect(result?.kind).toBe("quote");
|
||||
@@ -742,29 +760,31 @@ describe("describeReplyTarget", () => {
|
||||
});
|
||||
|
||||
it("extracts forwarded context from external_reply", () => {
|
||||
const result = describeReplyTarget({
|
||||
message_id: 5,
|
||||
date: 1300,
|
||||
chat: { id: 1, type: "private" },
|
||||
text: "Comment on forwarded message",
|
||||
external_reply: {
|
||||
message_id: 4,
|
||||
date: 1200,
|
||||
const result = describeReplyTarget(
|
||||
asMalformedTelegramMessage({
|
||||
message_id: 5,
|
||||
date: 1300,
|
||||
chat: { id: 1, type: "private" },
|
||||
text: "Forwarded from elsewhere",
|
||||
forward_origin: {
|
||||
type: "user",
|
||||
sender_user: {
|
||||
id: 123,
|
||||
first_name: "Eve",
|
||||
last_name: "Stone",
|
||||
username: "eve",
|
||||
is_bot: false,
|
||||
text: "Comment on forwarded message",
|
||||
external_reply: {
|
||||
message_id: 4,
|
||||
date: 1200,
|
||||
chat: { id: 1, type: "private" },
|
||||
text: "Forwarded from elsewhere",
|
||||
forward_origin: {
|
||||
type: "user",
|
||||
sender_user: {
|
||||
id: 123,
|
||||
first_name: "Eve",
|
||||
last_name: "Stone",
|
||||
username: "eve",
|
||||
is_bot: false,
|
||||
},
|
||||
date: 700,
|
||||
},
|
||||
date: 700,
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
}),
|
||||
);
|
||||
expect(result?.id).toBe("4");
|
||||
expect(result?.forwardedFrom?.from).toBe("Eve Stone (@eve)");
|
||||
expect(result?.forwardedFrom?.fromType).toBe("user");
|
||||
@@ -822,19 +842,21 @@ describe("getTelegramTextParts — binary caption filtering (#66647)", () => {
|
||||
chat: { id: 1, type: "private" },
|
||||
date: 1,
|
||||
message_id: 1,
|
||||
} as any);
|
||||
} as TelegramMessage);
|
||||
expect(result.text).toBe("");
|
||||
expect(result.entities).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("preserves normal caption text", () => {
|
||||
const result = getTelegramTextParts({
|
||||
caption: "Here is my document",
|
||||
caption_entities: [],
|
||||
chat: { id: 1, type: "private" },
|
||||
date: 1,
|
||||
message_id: 1,
|
||||
} as any);
|
||||
const result = getTelegramTextParts(
|
||||
asMalformedTelegramMessage({
|
||||
caption: "Here is my document",
|
||||
caption_entities: [],
|
||||
chat: { id: 1, type: "private" },
|
||||
date: 1,
|
||||
message_id: 1,
|
||||
}),
|
||||
);
|
||||
expect(result.text).toBe("Here is my document");
|
||||
});
|
||||
|
||||
@@ -845,7 +867,7 @@ describe("getTelegramTextParts — binary caption filtering (#66647)", () => {
|
||||
chat: { id: 1, type: "private" },
|
||||
date: 1,
|
||||
message_id: 1,
|
||||
} as any);
|
||||
} as TelegramMessage);
|
||||
expect(result.text).toBe("");
|
||||
expect(result.entities).toStrictEqual([]);
|
||||
});
|
||||
@@ -860,7 +882,7 @@ describe("hasBotMention", () => {
|
||||
chat: { id: 1, type: "private" },
|
||||
date: 1,
|
||||
message_id: 1,
|
||||
} as any),
|
||||
} as TelegramMessage),
|
||||
).toEqual({
|
||||
text: "@gaian hello",
|
||||
entities: [{ type: "mention", offset: 0, length: 6 }],
|
||||
@@ -873,7 +895,7 @@ describe("hasBotMention", () => {
|
||||
{
|
||||
text: "@gaian what is the group id?",
|
||||
chat: { id: 1, type: "supergroup" },
|
||||
} as any,
|
||||
} as TelegramMessage,
|
||||
"gaian",
|
||||
),
|
||||
).toBe(true);
|
||||
@@ -885,7 +907,7 @@ describe("hasBotMention", () => {
|
||||
{
|
||||
text: "@GaianChat_Bot what is the group id?",
|
||||
chat: { id: 1, type: "supergroup" },
|
||||
} as any,
|
||||
} as TelegramMessage,
|
||||
"gaian",
|
||||
),
|
||||
).toBe(false);
|
||||
@@ -898,7 +920,7 @@ describe("hasBotMention", () => {
|
||||
text: "@GaianChat_Bot hi @gaian",
|
||||
entities: [{ type: "mention", offset: 18, length: 6 }],
|
||||
chat: { id: 1, type: "supergroup" },
|
||||
} as any,
|
||||
} as TelegramMessage,
|
||||
"gaian",
|
||||
),
|
||||
).toBe(true);
|
||||
@@ -913,7 +935,7 @@ describe("hasBotMention", () => {
|
||||
text,
|
||||
entities: [{ type: "bot_command", offset: 0, length: "/deploy@gaian".length }],
|
||||
chat: { id: 1, type: "supergroup" },
|
||||
} as any,
|
||||
} as TelegramMessage,
|
||||
"gaian",
|
||||
),
|
||||
).toBe(true);
|
||||
@@ -928,7 +950,7 @@ describe("hasBotMention", () => {
|
||||
text,
|
||||
entities: [{ type: "bot_command", offset: 0, length: "/deploy@other_bot".length }],
|
||||
chat: { id: 1, type: "supergroup" },
|
||||
} as any,
|
||||
} as TelegramMessage,
|
||||
"gaian",
|
||||
),
|
||||
).toBe(false);
|
||||
@@ -940,7 +962,7 @@ describe("hasBotMention", () => {
|
||||
{
|
||||
text: "@gaian, what's up?",
|
||||
chat: { id: 1, type: "supergroup" },
|
||||
} as any,
|
||||
} as TelegramMessage,
|
||||
"gaian",
|
||||
),
|
||||
).toBe(true);
|
||||
@@ -952,7 +974,7 @@ describe("hasBotMention", () => {
|
||||
{
|
||||
text: "@gaian how are you",
|
||||
chat: { id: 1, type: "supergroup" },
|
||||
} as any,
|
||||
} as TelegramMessage,
|
||||
"gaian",
|
||||
),
|
||||
).toBe(true);
|
||||
@@ -964,7 +986,7 @@ describe("hasBotMention", () => {
|
||||
{
|
||||
text: "@gaianchat_bot hello",
|
||||
chat: { id: 1, type: "supergroup" },
|
||||
} as any,
|
||||
} as TelegramMessage,
|
||||
"gaian",
|
||||
),
|
||||
).toBe(false);
|
||||
@@ -976,7 +998,7 @@ describe("hasBotMention", () => {
|
||||
{
|
||||
text: "@gaianbot do something",
|
||||
chat: { id: 1, type: "supergroup" },
|
||||
} as any,
|
||||
} as TelegramMessage,
|
||||
"gaian",
|
||||
),
|
||||
).toBe(false);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
// Telegram tests cover targets plugin behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
@@ -200,7 +201,7 @@ describe("telegram group policy", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
} as OpenClawConfig;
|
||||
expect(
|
||||
resolveTelegramGroupRequireMention({ cfg: telegramCfg, groupId: "-1001:topic:77" }),
|
||||
).toBe(false);
|
||||
@@ -242,7 +243,7 @@ describe("telegram group policy", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(
|
||||
resolveTelegramGroupRequireMention({
|
||||
|
||||
@@ -33,6 +33,15 @@ const authSuccessHandlers: Array<() => void> = [];
|
||||
const authFailureHandlers: Array<(text: string, retryCount: number) => void> = [];
|
||||
const disconnectHandlers: Array<(manual: boolean, reason?: Error) => void> = [];
|
||||
|
||||
type TwitchClientManagerState = {
|
||||
clients: Map<string, unknown>;
|
||||
messageHandlers: Map<string, (message: TwitchChatMessage) => void>;
|
||||
};
|
||||
|
||||
function managerState(manager: TwitchClientManager): TwitchClientManagerState {
|
||||
return manager as unknown as TwitchClientManagerState;
|
||||
}
|
||||
|
||||
// Mock functions that track handlers and return unbind objects
|
||||
const mockOnMessage = vi.fn((handler: any) => {
|
||||
messageHandlers.push(handler);
|
||||
@@ -339,7 +348,7 @@ describe("TwitchClientManager", () => {
|
||||
// The broken auth provider must not be cached as a usable client;
|
||||
// otherwise later sends fail with an opaque error instead of failing fast.
|
||||
const key = manager.getAccountKey(refreshingAccount);
|
||||
expect((manager as any).clients.has(key)).toBe(false);
|
||||
expect(managerState(manager).clients.has(key)).toBe(false);
|
||||
});
|
||||
|
||||
it("retries client creation after an earlier addUserForToken failure (83853)", async () => {
|
||||
@@ -423,7 +432,7 @@ describe("TwitchClientManager", () => {
|
||||
|
||||
// Check the stored handler is handler2
|
||||
const key = manager.getAccountKey(testAccount);
|
||||
expect((manager as any).messageHandlers.get(key)).toBe(handler2);
|
||||
expect(managerState(manager).messageHandlers.get(key)).toBe(handler2);
|
||||
});
|
||||
|
||||
it("cleanup of an earlier handler does not remove a newer registered handler (#83888)", () => {
|
||||
@@ -437,7 +446,7 @@ describe("TwitchClientManager", () => {
|
||||
// Running the first handler's cleanup must not drop handler2.
|
||||
cleanup1();
|
||||
|
||||
expect((manager as any).messageHandlers.get(key)).toBe(handler2);
|
||||
expect(managerState(manager).messageHandlers.get(key)).toBe(handler2);
|
||||
});
|
||||
|
||||
it("cleanup of an earlier registration does not remove a newer registration using the same handler", () => {
|
||||
@@ -448,7 +457,7 @@ describe("TwitchClientManager", () => {
|
||||
manager.onMessage(testAccount, handler);
|
||||
cleanup1();
|
||||
|
||||
expect((manager as any).messageHandlers.get(key)).toBe(handler);
|
||||
expect(managerState(manager).messageHandlers.get(key)).toBe(handler);
|
||||
});
|
||||
|
||||
it("cleanup of the current handler removes it", () => {
|
||||
@@ -458,7 +467,7 @@ describe("TwitchClientManager", () => {
|
||||
const cleanup = manager.onMessage(testAccount, handler);
|
||||
cleanup();
|
||||
|
||||
expect((manager as any).messageHandlers.has(key)).toBe(false);
|
||||
expect(managerState(manager).messageHandlers.has(key)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -479,8 +488,8 @@ describe("TwitchClientManager", () => {
|
||||
await manager.disconnect(testAccount);
|
||||
|
||||
const key = manager.getAccountKey(testAccount);
|
||||
expect((manager as any).clients.has(key)).toBe(false);
|
||||
expect((manager as any).messageHandlers.has(key)).toBe(false);
|
||||
expect(managerState(manager).clients.has(key)).toBe(false);
|
||||
expect(managerState(manager).messageHandlers.has(key)).toBe(false);
|
||||
});
|
||||
|
||||
it("clears pending client message handlers when disconnect cancels connection", async () => {
|
||||
@@ -493,7 +502,7 @@ describe("TwitchClientManager", () => {
|
||||
await manager.disconnect(testAccount);
|
||||
|
||||
const key = manager.getAccountKey(testAccount);
|
||||
expect((manager as any).messageHandlers.has(key)).toBe(false);
|
||||
expect(managerState(manager).messageHandlers.has(key)).toBe(false);
|
||||
authSuccessHandlers[0]?.();
|
||||
await expect(connection).rejects.toThrow("Twitch connection cancelled");
|
||||
|
||||
@@ -528,7 +537,7 @@ describe("TwitchClientManager", () => {
|
||||
expect(mockQuit).toHaveBeenCalledTimes(1);
|
||||
|
||||
const key2 = manager.getAccountKey(testAccount2);
|
||||
expect((manager as any).clients.has(key2)).toBe(true);
|
||||
expect(managerState(manager).clients.has(key2)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -540,8 +549,8 @@ describe("TwitchClientManager", () => {
|
||||
await manager.disconnectAll();
|
||||
|
||||
expect(mockQuit).toHaveBeenCalledTimes(2);
|
||||
expect((manager as any).clients.size).toBe(0);
|
||||
expect((manager as any).messageHandlers.size).toBe(0);
|
||||
expect(managerState(manager).clients.size).toBe(0);
|
||||
expect(managerState(manager).messageHandlers.size).toBe(0);
|
||||
});
|
||||
|
||||
it("should handle empty client list gracefully", async () => {
|
||||
@@ -617,7 +626,7 @@ describe("TwitchClientManager", () => {
|
||||
|
||||
it("should create client if not already connected", async () => {
|
||||
// Clear the existing client
|
||||
(manager as any).clients.clear();
|
||||
managerState(manager).clients.clear();
|
||||
|
||||
// Reset connect call count for this specific test
|
||||
const connectCallCountBefore = mockConnect.mock.calls.length;
|
||||
|
||||
@@ -88,11 +88,13 @@ const callbackMessageWithoutAdmissionFacts = {
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-expect-error Callback messages must provide admission or deprecated admission fields.
|
||||
const invalidCallbackMessage: WebInboundCallbackMessage = callbackMessageWithoutAdmissionFacts;
|
||||
void invalidCallbackMessage;
|
||||
|
||||
describe("WhatsApp inbound flat aliases", () => {
|
||||
it("rejects callback messages without admission facts", () => {
|
||||
expectTypeOf(
|
||||
callbackMessageWithoutAdmissionFacts,
|
||||
).not.toMatchTypeOf<WebInboundCallbackMessage>();
|
||||
});
|
||||
|
||||
it("keeps deprecated admission fields typed on monitor callbacks", () => {
|
||||
expectTypeOf<MonitorWebInboxMessage>().toMatchTypeOf<{
|
||||
admission: NonNullable<WebInboundCallbackMessage["admission"]>;
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { buildXaiImageGenerationProvider } from "./image-generation-provider.js";
|
||||
|
||||
type GenerateImageParams = Parameters<
|
||||
ReturnType<typeof buildXaiImageGenerationProvider>["generateImage"]
|
||||
>[0];
|
||||
|
||||
const {
|
||||
resolveApiKeyForProviderMock,
|
||||
isProviderApiKeyConfiguredMock,
|
||||
@@ -170,11 +174,12 @@ describe("xai image generation provider", () => {
|
||||
providers: {
|
||||
xai: {
|
||||
baseUrl: "https://custom.x.ai/v1",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
} as GenerateImageParams);
|
||||
|
||||
const authParams = (
|
||||
resolveApiKeyForProviderMock.mock.calls as unknown as Array<[unknown]>
|
||||
@@ -232,7 +237,7 @@ describe("xai image generation provider", () => {
|
||||
},
|
||||
],
|
||||
cfg: {},
|
||||
} as any);
|
||||
} as GenerateImageParams);
|
||||
|
||||
const request = requirePostJsonCall();
|
||||
expect(request.url).toContain("/images/edits");
|
||||
@@ -259,7 +264,7 @@ describe("xai image generation provider", () => {
|
||||
model: "grok-imagine-image",
|
||||
prompt: "ua check",
|
||||
cfg: {},
|
||||
} as any);
|
||||
} as GenerateImageParams);
|
||||
|
||||
const request = requirePostJsonCall();
|
||||
expect(request.headers?.get("user-agent")).toBe("openclaw/2026.3.22");
|
||||
@@ -292,7 +297,7 @@ describe("xai image generation provider", () => {
|
||||
{ buffer: Buffer.from("third"), mimeType: "image/webp" },
|
||||
],
|
||||
cfg: {},
|
||||
} as any);
|
||||
} as GenerateImageParams);
|
||||
|
||||
const request = requirePostJsonCall();
|
||||
expect(request.url).toContain("/images/edits");
|
||||
@@ -319,7 +324,7 @@ describe("xai image generation provider", () => {
|
||||
mimeType: "image/png",
|
||||
})),
|
||||
cfg: {},
|
||||
} as any),
|
||||
} as GenerateImageParams),
|
||||
).rejects.toThrow("xAI image editing supports up to 3 reference images");
|
||||
expect(postJsonRequestMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -1887,6 +1887,7 @@
|
||||
"test:docker:upgrade-survivor": "bash scripts/e2e/upgrade-survivor-docker.sh",
|
||||
"test:env-mutations:report": "node --import tsx scripts/test-env-mutation-report.ts",
|
||||
"test:skip-inventory:report": "node --import tsx scripts/test-skip-inventory.ts",
|
||||
"test:type-suppression-inventory:report": "node --import tsx scripts/type-suppression-inventory.ts",
|
||||
"test:e2e": "pnpm test:e2e:gateway && pnpm test:ui:e2e",
|
||||
"test:e2e:gateway": "node scripts/run-vitest.mjs run --config test/vitest/vitest.e2e.config.ts",
|
||||
"test:e2e:openshell": "node scripts/run-with-env.mjs OPENCLAW_E2E_OPENSHELL=1 -- node scripts/run-vitest.mjs run --config test/vitest/vitest.e2e.config.ts extensions/openshell/src/backend.e2e.test.ts",
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
#!/usr/bin/env node
|
||||
// Type Suppression Inventory reports unchecked any casts and expected TypeScript errors.
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import ts from "typescript";
|
||||
import { collectFilesSync, isCodeFile, toPosixPath } from "./check-file-utils.js";
|
||||
|
||||
type TypeSuppressionKind = "as-any" | "expect-error" | "type-assertion-any";
|
||||
|
||||
export type TypeSuppressionFinding = {
|
||||
excerpt: string;
|
||||
file: string;
|
||||
kind: TypeSuppressionKind;
|
||||
line: number;
|
||||
};
|
||||
|
||||
export type TypeSuppressionReport = {
|
||||
findings: TypeSuppressionFinding[];
|
||||
scannedFileCount: number;
|
||||
schemaVersion: 1;
|
||||
summary: {
|
||||
findingCount: number;
|
||||
kindCounts: Record<TypeSuppressionKind, number>;
|
||||
scannedFileCount: number;
|
||||
touchedFileCount: number;
|
||||
};
|
||||
};
|
||||
|
||||
const DEFAULT_SCAN_ROOTS = ["src", "test", "extensions", "packages", "ui", "scripts"];
|
||||
const DEFAULT_SKIPPED_DIR_NAMES = new Set([
|
||||
".artifacts",
|
||||
".generated",
|
||||
"coverage",
|
||||
"dist",
|
||||
"fixtures",
|
||||
"node_modules",
|
||||
"vendor",
|
||||
]);
|
||||
|
||||
function listGitFiles(repoRoot: string, roots: readonly string[]): string[] | null {
|
||||
try {
|
||||
const stdout = execFileSync("git", ["-C", repoRoot, "ls-files", "--", ...roots], {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
});
|
||||
return stdout.split(/\r?\n/u).filter(Boolean);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function listCandidateFiles(repoRoot: string, roots: readonly string[]): string[] {
|
||||
const gitFiles = listGitFiles(repoRoot, roots);
|
||||
const relativeFiles =
|
||||
gitFiles ??
|
||||
roots.flatMap((root) => {
|
||||
const absoluteRoot = path.join(repoRoot, root);
|
||||
if (!fs.existsSync(absoluteRoot)) {
|
||||
return [];
|
||||
}
|
||||
return collectFilesSync(absoluteRoot, {
|
||||
includeFile: isCodeFile,
|
||||
skipDirNames: DEFAULT_SKIPPED_DIR_NAMES,
|
||||
}).map((filePath) => toPosixPath(path.relative(repoRoot, filePath)));
|
||||
});
|
||||
return relativeFiles
|
||||
.filter((file) => {
|
||||
const pathSegments = toPosixPath(file).split("/");
|
||||
return (
|
||||
/\.[cm]?tsx?$/u.test(file) &&
|
||||
!file.endsWith(".d.ts") &&
|
||||
!pathSegments.some((segment) => DEFAULT_SKIPPED_DIR_NAMES.has(segment))
|
||||
);
|
||||
})
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
function sourceKindForFile(file: string): ts.ScriptKind {
|
||||
return file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
||||
}
|
||||
|
||||
function addAnyCastFindings(
|
||||
sourceFile: ts.SourceFile,
|
||||
file: string,
|
||||
findings: TypeSuppressionFinding[],
|
||||
): void {
|
||||
const visit = (node: ts.Node): void => {
|
||||
const kind =
|
||||
ts.isAsExpression(node) && node.type.kind === ts.SyntaxKind.AnyKeyword
|
||||
? "as-any"
|
||||
: ts.isTypeAssertionExpression(node) && node.type.kind === ts.SyntaxKind.AnyKeyword
|
||||
? "type-assertion-any"
|
||||
: null;
|
||||
if (kind) {
|
||||
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
||||
findings.push({
|
||||
excerpt: node.getText(sourceFile).replace(/\s+/gu, " ").trim(),
|
||||
file,
|
||||
kind,
|
||||
line: line + 1,
|
||||
});
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(sourceFile);
|
||||
}
|
||||
|
||||
function addExpectErrorFindings(
|
||||
sourceFile: ts.SourceFile,
|
||||
file: string,
|
||||
findings: TypeSuppressionFinding[],
|
||||
): void {
|
||||
const source = sourceFile.getFullText();
|
||||
const scanner = ts.createScanner(
|
||||
ts.ScriptTarget.Latest,
|
||||
false,
|
||||
file.endsWith(".tsx") ? ts.LanguageVariant.JSX : ts.LanguageVariant.Standard,
|
||||
source,
|
||||
);
|
||||
for (let token = scanner.scan(); token !== ts.SyntaxKind.EndOfFileToken; token = scanner.scan()) {
|
||||
if (
|
||||
token !== ts.SyntaxKind.SingleLineCommentTrivia &&
|
||||
token !== ts.SyntaxKind.MultiLineCommentTrivia
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const comment = scanner.getTokenText();
|
||||
const markerPattern = /@ts-expect-error[^\r\n]*/gu;
|
||||
for (const match of comment.matchAll(markerPattern)) {
|
||||
const position = scanner.getTokenPos() + (match.index ?? 0);
|
||||
const line = sourceFile.getLineAndCharacterOfPosition(position).line;
|
||||
findings.push({
|
||||
excerpt: match[0].trim(),
|
||||
file,
|
||||
kind: "expect-error",
|
||||
line: line + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function collectTypeSuppressionReport(params: {
|
||||
files?: readonly string[];
|
||||
repoRoot: string;
|
||||
roots?: readonly string[];
|
||||
}): TypeSuppressionReport {
|
||||
const files = [
|
||||
...(params.files ?? listCandidateFiles(params.repoRoot, params.roots ?? DEFAULT_SCAN_ROOTS)),
|
||||
]
|
||||
.map(toPosixPath)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
const findings: TypeSuppressionFinding[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
const absolutePath = path.join(params.repoRoot, file);
|
||||
if (!fs.existsSync(absolutePath)) {
|
||||
continue;
|
||||
}
|
||||
const source = fs.readFileSync(absolutePath, "utf8");
|
||||
const sourceFile = ts.createSourceFile(
|
||||
file,
|
||||
source,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
sourceKindForFile(file),
|
||||
);
|
||||
addAnyCastFindings(sourceFile, file, findings);
|
||||
addExpectErrorFindings(sourceFile, file, findings);
|
||||
}
|
||||
|
||||
findings.sort(
|
||||
(left, right) =>
|
||||
left.file.localeCompare(right.file) ||
|
||||
left.line - right.line ||
|
||||
left.kind.localeCompare(right.kind),
|
||||
);
|
||||
const kindCounts: Record<TypeSuppressionKind, number> = {
|
||||
"as-any": 0,
|
||||
"expect-error": 0,
|
||||
"type-assertion-any": 0,
|
||||
};
|
||||
for (const finding of findings) {
|
||||
kindCounts[finding.kind] = kindCounts[finding.kind] + 1;
|
||||
}
|
||||
|
||||
return {
|
||||
findings,
|
||||
scannedFileCount: files.length,
|
||||
schemaVersion: 1,
|
||||
summary: {
|
||||
findingCount: findings.length,
|
||||
kindCounts,
|
||||
scannedFileCount: files.length,
|
||||
touchedFileCount: new Set(findings.map((finding) => finding.file)).size,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
process.stdout.write(`${JSON.stringify(collectTypeSuppressionReport({ repoRoot }), null, 2)}\n`);
|
||||
}
|
||||
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
main();
|
||||
}
|
||||
@@ -19,8 +19,12 @@ import {
|
||||
} from "../infra/diagnostic-events.js";
|
||||
import { MAX_PLUGIN_APPROVAL_TIMEOUT_MS } from "../infra/plugin-approvals.js";
|
||||
import { resetDiagnosticSessionStateForTest } from "../logging/diagnostic-session-state.js";
|
||||
import { PluginApprovalResolutions } from "../plugins/hook-before-tool-call-result.js";
|
||||
import {
|
||||
PluginApprovalResolutions,
|
||||
type PluginApprovalResolution,
|
||||
} from "../plugins/hook-before-tool-call-result.js";
|
||||
import { getGlobalHookRunner } from "../plugins/hook-runner-global.js";
|
||||
import { createHookRunner, type HookRunner } from "../plugins/hooks.js";
|
||||
import { createEmptyPluginRegistry } from "../plugins/registry-empty.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { setPluginToolMeta } from "../plugins/tools.js";
|
||||
@@ -53,10 +57,10 @@ vi.mock("./tools/gateway.js", () => ({
|
||||
const mockGetGlobalHookRunner = vi.mocked(getGlobalHookRunner);
|
||||
const hookRunnerGlobalStateKey = Symbol.for("openclaw.plugins.hook-runner-global-state");
|
||||
|
||||
function setGlobalHookRunnerForTest(hookRunner: unknown): void {
|
||||
function setGlobalHookRunnerForTest(hookRunner: HookRunner | null): void {
|
||||
const hookRunnerGlobalState = globalThis as Record<
|
||||
symbol,
|
||||
{ hookRunner: unknown; registry?: unknown } | undefined
|
||||
{ hookRunner: HookRunner | null; registry?: unknown } | undefined
|
||||
>;
|
||||
if (!hookRunnerGlobalState[hookRunnerGlobalStateKey]) {
|
||||
hookRunnerGlobalState[hookRunnerGlobalStateKey] = {
|
||||
@@ -67,27 +71,39 @@ function setGlobalHookRunnerForTest(hookRunner: unknown): void {
|
||||
hookRunnerGlobalState[hookRunnerGlobalStateKey].hookRunner = hookRunner;
|
||||
}
|
||||
|
||||
function getGlobalHookRunnerForTest(): unknown {
|
||||
function getGlobalHookRunnerForTest(): HookRunner | null {
|
||||
const hookRunnerGlobalState = globalThis as Record<
|
||||
symbol,
|
||||
{ hookRunner: unknown; registry?: unknown } | undefined
|
||||
{ hookRunner: HookRunner | null; registry?: unknown } | undefined
|
||||
>;
|
||||
return hookRunnerGlobalState[hookRunnerGlobalStateKey]?.hookRunner ?? null;
|
||||
}
|
||||
|
||||
type TestHookRunner = HookRunner & {
|
||||
hasHooks: ReturnType<typeof vi.fn<HookRunner["hasHooks"]>>;
|
||||
runBeforeToolCall: ReturnType<typeof vi.fn<HookRunner["runBeforeToolCall"]>>;
|
||||
};
|
||||
|
||||
function createTestHookRunner(): TestHookRunner {
|
||||
return {
|
||||
...createHookRunner(createEmptyPluginRegistry()),
|
||||
hasHooks: vi.fn<HookRunner["hasHooks"]>(),
|
||||
runBeforeToolCall: vi.fn<HookRunner["runBeforeToolCall"]>(),
|
||||
};
|
||||
}
|
||||
|
||||
function asAgentTool(tool: { name: string; execute: ReturnType<typeof vi.fn> }): AnyAgentTool {
|
||||
return tool as unknown as AnyAgentTool;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
setGlobalHookRunnerForTest(null);
|
||||
mockGetGlobalHookRunner.mockReset();
|
||||
mockGetGlobalHookRunner.mockImplementation(
|
||||
() => getGlobalHookRunnerForTest() as ReturnType<typeof getGlobalHookRunner>,
|
||||
);
|
||||
mockGetGlobalHookRunner.mockImplementation(() => getGlobalHookRunnerForTest());
|
||||
});
|
||||
|
||||
describe("before_tool_call loop detection behavior", () => {
|
||||
let hookRunner: {
|
||||
hasHooks: ReturnType<typeof vi.fn>;
|
||||
runBeforeToolCall: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
let hookRunner: TestHookRunner;
|
||||
const enabledLoopDetectionContext = {
|
||||
agentId: "main",
|
||||
sessionKey: "main",
|
||||
@@ -103,11 +119,8 @@ describe("before_tool_call loop detection behavior", () => {
|
||||
beforeEach(() => {
|
||||
resetDiagnosticSessionStateForTest();
|
||||
resetDiagnosticEventsForTest();
|
||||
hookRunner = {
|
||||
hasHooks: vi.fn(),
|
||||
runBeforeToolCall: vi.fn(),
|
||||
};
|
||||
mockGetGlobalHookRunner.mockReturnValue(hookRunner as any);
|
||||
hookRunner = createTestHookRunner();
|
||||
mockGetGlobalHookRunner.mockReturnValue(hookRunner);
|
||||
hookRunner.hasHooks.mockReturnValue(false);
|
||||
});
|
||||
|
||||
@@ -374,7 +387,7 @@ describe("before_tool_call loop detection behavior", () => {
|
||||
content: [{ type: "text", text: "(no new output)\n\nProcess still running." }],
|
||||
details: { status: "running", aggregated: "steady" },
|
||||
});
|
||||
const tool = wrapToolWithBeforeToolCallHook({ name: "process", execute } as any, {
|
||||
const tool = wrapToolWithBeforeToolCallHook(asAgentTool({ name: "process", execute }), {
|
||||
...disabledLoopDetectionContext,
|
||||
});
|
||||
const params = { action: "poll", sessionId: "sess-off" };
|
||||
@@ -424,11 +437,11 @@ describe("before_tool_call loop detection behavior", () => {
|
||||
details: { ok: true },
|
||||
});
|
||||
const params = { path: "/tmp/file" };
|
||||
const firstRunTool = wrapToolWithBeforeToolCallHook({ name: "read", execute } as any, {
|
||||
const firstRunTool = wrapToolWithBeforeToolCallHook(asAgentTool({ name: "read", execute }), {
|
||||
...enabledLoopDetectionContext,
|
||||
runId: "heartbeat-1",
|
||||
});
|
||||
const secondRunTool = wrapToolWithBeforeToolCallHook({ name: "read", execute } as any, {
|
||||
const secondRunTool = wrapToolWithBeforeToolCallHook(asAgentTool({ name: "read", execute }), {
|
||||
...enabledLoopDetectionContext,
|
||||
runId: "heartbeat-2",
|
||||
});
|
||||
@@ -553,7 +566,7 @@ describe("before_tool_call loop detection behavior", () => {
|
||||
const execute = vi.fn().mockResolvedValue({
|
||||
content: [{ type: "text", text: "ok" }],
|
||||
});
|
||||
const tool = wrapToolWithBeforeToolCallHook({ name: "bash", execute } as any, {
|
||||
const tool = wrapToolWithBeforeToolCallHook(asAgentTool({ name: "bash", execute }), {
|
||||
agentId: "main",
|
||||
sessionKey: "session-key",
|
||||
sessionId: "session-id",
|
||||
@@ -828,7 +841,7 @@ describe("before_tool_call loop detection behavior", () => {
|
||||
content: [{ type: "text", text: "tool failed" }],
|
||||
details,
|
||||
});
|
||||
const tool = wrapToolWithBeforeToolCallHook({ name: "exec", execute } as any, {
|
||||
const tool = wrapToolWithBeforeToolCallHook(asAgentTool({ name: "exec", execute }), {
|
||||
agentId: "main",
|
||||
sessionKey: "session-key",
|
||||
runId: "run-1",
|
||||
@@ -877,7 +890,7 @@ describe("before_tool_call loop detection behavior", () => {
|
||||
const skillBaseDir = path.join(workspaceDir, ".agents", "skills", "demo-skill");
|
||||
const skillFilePath = path.join(skillBaseDir, "SKILL.md");
|
||||
const execute = vi.fn().mockResolvedValue({ content: [{ type: "text", text: "skill" }] });
|
||||
const tool = wrapToolWithBeforeToolCallHook({ name: "read", execute } as any, {
|
||||
const tool = wrapToolWithBeforeToolCallHook(asAgentTool({ name: "read", execute }), {
|
||||
agentId: "main",
|
||||
sessionKey: "session-key",
|
||||
sessionId: "session-id",
|
||||
@@ -936,7 +949,7 @@ describe("before_tool_call loop detection behavior", () => {
|
||||
const skillBaseDir = path.join(os.homedir(), ".openclaw", "skills", "home-skill");
|
||||
const skillFilePath = path.join(skillBaseDir, "SKILL.md");
|
||||
const execute = vi.fn().mockResolvedValue({ content: [{ type: "text", text: "skill" }] });
|
||||
const tool = wrapToolWithBeforeToolCallHook({ name: "read", execute } as any, {
|
||||
const tool = wrapToolWithBeforeToolCallHook(asAgentTool({ name: "read", execute }), {
|
||||
agentId: "main",
|
||||
sessionKey: "session-key",
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
@@ -981,7 +994,7 @@ describe("before_tool_call loop detection behavior", () => {
|
||||
it("emits skill usage diagnostics for node skill locators", async () => {
|
||||
const locator = "node://node-1/skills/remote-skill/SKILL.md";
|
||||
const execute = vi.fn().mockResolvedValue({ content: [{ type: "text", text: "skill" }] });
|
||||
const tool = wrapToolWithBeforeToolCallHook({ name: "read", execute } as any, {
|
||||
const tool = wrapToolWithBeforeToolCallHook(asAgentTool({ name: "read", execute }), {
|
||||
agentId: "main",
|
||||
sessionKey: "session-key",
|
||||
skillsSnapshot: {
|
||||
@@ -1018,7 +1031,7 @@ describe("before_tool_call loop detection behavior", () => {
|
||||
const readPath = "/workspace/.openclaw/sandbox-skills/skills/demo/SKILL.md";
|
||||
const skillFile = "/agent-workspace/skills/demo/SKILL.md";
|
||||
const execute = vi.fn().mockResolvedValue({ content: [{ type: "text", text: "skill" }] });
|
||||
const tool = wrapToolWithBeforeToolCallHook({ name: "read", execute } as any, {
|
||||
const tool = wrapToolWithBeforeToolCallHook(asAgentTool({ name: "read", execute }), {
|
||||
agentId: "main",
|
||||
sessionKey: "session-key",
|
||||
workspaceDir,
|
||||
@@ -1058,7 +1071,7 @@ describe("before_tool_call loop detection behavior", () => {
|
||||
const workspaceDir = path.join("/tmp", "openclaw-skill-unused-param");
|
||||
const skillBaseDir = path.join(workspaceDir, ".agents", "skills", "demo-skill");
|
||||
const execute = vi.fn().mockResolvedValue({ content: [{ type: "text", text: "readme" }] });
|
||||
const tool = wrapToolWithBeforeToolCallHook({ name: "read", execute } as any, {
|
||||
const tool = wrapToolWithBeforeToolCallHook(asAgentTool({ name: "read", execute }), {
|
||||
agentId: "main",
|
||||
sessionKey: "session-key",
|
||||
workspaceDir,
|
||||
@@ -1101,7 +1114,7 @@ describe("before_tool_call loop detection behavior", () => {
|
||||
const skillBaseDir = path.join("/tmp", "openclaw-skill-command", "skills", "matrix-profile");
|
||||
const skillFilePath = path.join(skillBaseDir, "SKILL.md");
|
||||
const execute = vi.fn().mockResolvedValue({ content: [{ type: "text", text: "sent" }] });
|
||||
const tool = wrapToolWithBeforeToolCallHook({ name: "message", execute } as any, {
|
||||
const tool = wrapToolWithBeforeToolCallHook(asAgentTool({ name: "message", execute }), {
|
||||
agentId: "main",
|
||||
sessionKey: "session-key",
|
||||
sessionId: "session-id",
|
||||
@@ -1147,7 +1160,7 @@ describe("before_tool_call loop detection behavior", () => {
|
||||
const execute = vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error("failed with key sk-1234567890abcdef1234567890abcdef"));
|
||||
const tool = wrapToolWithBeforeToolCallHook({ name: "read", execute } as any, {
|
||||
const tool = wrapToolWithBeforeToolCallHook(asAgentTool({ name: "read", execute }), {
|
||||
agentId: "main",
|
||||
sessionKey: "session-key",
|
||||
loopDetection: { enabled: false },
|
||||
@@ -1180,7 +1193,7 @@ describe("before_tool_call loop detection behavior", () => {
|
||||
abortController.abort();
|
||||
throw new Error("tool stopped with run");
|
||||
});
|
||||
const tool = wrapToolWithBeforeToolCallHook({ name: "read", execute } as any, {
|
||||
const tool = wrapToolWithBeforeToolCallHook(asAgentTool({ name: "read", execute }), {
|
||||
agentId: "main",
|
||||
sessionKey: "session-key",
|
||||
loopDetection: { enabled: false },
|
||||
@@ -1212,7 +1225,7 @@ describe("before_tool_call loop detection behavior", () => {
|
||||
abortController.abort(Object.assign(new Error("timed out"), { name: "TimeoutError" }));
|
||||
throw new Error("tool stopped with timeout");
|
||||
});
|
||||
const tool = wrapToolWithBeforeToolCallHook({ name: "read", execute } as any, {
|
||||
const tool = wrapToolWithBeforeToolCallHook(asAgentTool({ name: "read", execute }), {
|
||||
agentId: "main",
|
||||
sessionKey: "session-key",
|
||||
loopDetection: { enabled: false },
|
||||
@@ -1238,7 +1251,7 @@ describe("before_tool_call loop detection behavior", () => {
|
||||
.mockRejectedValue(
|
||||
Object.assign(new Error("tool deadline elapsed"), { name: "TimeoutError" }),
|
||||
);
|
||||
const tool = wrapToolWithBeforeToolCallHook({ name: "read", execute } as any, {
|
||||
const tool = wrapToolWithBeforeToolCallHook(asAgentTool({ name: "read", execute }), {
|
||||
agentId: "main",
|
||||
sessionKey: "session-key",
|
||||
loopDetection: { enabled: false },
|
||||
@@ -1266,7 +1279,7 @@ describe("before_tool_call loop detection behavior", () => {
|
||||
blockReason: "blocked by policy",
|
||||
});
|
||||
const execute = vi.fn().mockResolvedValue({ content: [{ type: "text", text: "nope" }] });
|
||||
const tool = wrapToolWithBeforeToolCallHook({ name: "read", execute } as any, {
|
||||
const tool = wrapToolWithBeforeToolCallHook(asAgentTool({ name: "read", execute }), {
|
||||
agentId: "main",
|
||||
sessionKey: "session-key",
|
||||
loopDetection: { enabled: false },
|
||||
@@ -1303,7 +1316,7 @@ describe("before_tool_call loop detection behavior", () => {
|
||||
blockReason: "blocked by policy",
|
||||
});
|
||||
const execute = vi.fn().mockResolvedValue({ content: [{ type: "text", text: "nope" }] });
|
||||
const tool = wrapToolWithBeforeToolCallHook({ name: "read", execute } as any, {
|
||||
const tool = wrapToolWithBeforeToolCallHook(asAgentTool({ name: "read", execute }), {
|
||||
agentId: "main",
|
||||
sessionKey: "session-key",
|
||||
loopDetection: { enabled: false },
|
||||
@@ -1362,7 +1375,7 @@ describe("before_tool_call loop detection behavior", () => {
|
||||
},
|
||||
);
|
||||
const execute = vi.fn().mockRejectedValue(hostileError);
|
||||
const tool = wrapToolWithBeforeToolCallHook({ name: "read", execute } as any, {
|
||||
const tool = wrapToolWithBeforeToolCallHook(asAgentTool({ name: "read", execute }), {
|
||||
agentId: "main",
|
||||
sessionKey: "session-key",
|
||||
loopDetection: { enabled: false },
|
||||
@@ -1394,7 +1407,7 @@ describe("before_tool_call loop detection behavior", () => {
|
||||
status: 429,
|
||||
});
|
||||
const execute = vi.fn().mockRejectedValue(error);
|
||||
const tool = wrapToolWithBeforeToolCallHook({ name: "read", execute } as any, {
|
||||
const tool = wrapToolWithBeforeToolCallHook(asAgentTool({ name: "read", execute }), {
|
||||
agentId: "main",
|
||||
sessionKey: "session-key",
|
||||
loopDetection: { enabled: false },
|
||||
@@ -1416,7 +1429,7 @@ describe("before_tool_call loop detection behavior", () => {
|
||||
|
||||
it("summarizes hostile object params without enumerating keys", async () => {
|
||||
const execute = vi.fn().mockResolvedValue({ content: [{ type: "text", text: "ok" }] });
|
||||
const tool = wrapToolWithBeforeToolCallHook({ name: "bash", execute } as any, {
|
||||
const tool = wrapToolWithBeforeToolCallHook(asAgentTool({ name: "bash", execute }), {
|
||||
agentId: "main",
|
||||
sessionKey: "session-key",
|
||||
loopDetection: { enabled: false },
|
||||
@@ -1445,10 +1458,7 @@ describe("before_tool_call loop detection behavior", () => {
|
||||
});
|
||||
|
||||
describe("before_tool_call requireApproval handling", () => {
|
||||
let hookRunner: {
|
||||
hasHooks: ReturnType<typeof vi.fn>;
|
||||
runBeforeToolCall: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
let hookRunner: TestHookRunner;
|
||||
const mockCallGateway = vi.mocked(callGatewayTool);
|
||||
|
||||
function requireRecord(value: unknown, label: string): Record<string, unknown> {
|
||||
@@ -1508,11 +1518,9 @@ describe("before_tool_call requireApproval handling", () => {
|
||||
beforeEach(() => {
|
||||
resetDiagnosticSessionStateForTest();
|
||||
resetDiagnosticEventsForTest();
|
||||
hookRunner = {
|
||||
hasHooks: vi.fn((hookName: string) => hookName === "before_tool_call"),
|
||||
runBeforeToolCall: vi.fn(),
|
||||
};
|
||||
mockGetGlobalHookRunner.mockReturnValue(hookRunner as any);
|
||||
hookRunner = createTestHookRunner();
|
||||
hookRunner.hasHooks.mockImplementation((hookName) => hookName === "before_tool_call");
|
||||
mockGetGlobalHookRunner.mockReturnValue(hookRunner);
|
||||
// Keep the global singleton aligned as a fallback in case another setup path
|
||||
// preloads hook-runner-global before this test's module reset/mocks take effect.
|
||||
setGlobalHookRunnerForTest(hookRunner);
|
||||
@@ -1522,7 +1530,7 @@ describe("before_tool_call requireApproval handling", () => {
|
||||
|
||||
async function runAbortDuringApprovalWait(options?: {
|
||||
abortReason?: unknown;
|
||||
onResolution?: ReturnType<typeof vi.fn>;
|
||||
onResolution?: (decision: PluginApprovalResolution) => void | Promise<void>;
|
||||
}) {
|
||||
hookRunner.runBeforeToolCall.mockResolvedValue({
|
||||
requireApproval: {
|
||||
@@ -2663,7 +2671,7 @@ describe("before_tool_call tool content private-data capture", () => {
|
||||
|
||||
it("attaches tool input/output to private data when opted in", async () => {
|
||||
const execute = vi.fn().mockResolvedValue({ content: [{ type: "text", text: "file body" }] });
|
||||
const tool = wrapToolWithBeforeToolCallHook({ name: "read", execute } as any, {
|
||||
const tool = wrapToolWithBeforeToolCallHook(asAgentTool({ name: "read", execute }), {
|
||||
agentId: "main",
|
||||
sessionKey: "session-key",
|
||||
runId: "run-1",
|
||||
@@ -2688,7 +2696,7 @@ describe("before_tool_call tool content private-data capture", () => {
|
||||
|
||||
it("omits tool content from private data when capture is not configured", async () => {
|
||||
const execute = vi.fn().mockResolvedValue({ content: [{ type: "text", text: "ok" }] });
|
||||
const tool = wrapToolWithBeforeToolCallHook({ name: "read", execute } as any, {
|
||||
const tool = wrapToolWithBeforeToolCallHook(asAgentTool({ name: "read", execute }), {
|
||||
agentId: "main",
|
||||
sessionKey: "session-key",
|
||||
runId: "run-1",
|
||||
@@ -2708,7 +2716,7 @@ describe("before_tool_call tool content private-data capture", () => {
|
||||
it("captures only opted-in fields and clones away from live params", async () => {
|
||||
const liveParams = { path: "/etc/secret" };
|
||||
const execute = vi.fn().mockResolvedValue({ content: [{ type: "text", text: "out" }] });
|
||||
const tool = wrapToolWithBeforeToolCallHook({ name: "read", execute } as any, {
|
||||
const tool = wrapToolWithBeforeToolCallHook(asAgentTool({ name: "read", execute }), {
|
||||
agentId: "main",
|
||||
sessionKey: "session-key",
|
||||
runId: "run-1",
|
||||
@@ -2730,7 +2738,7 @@ describe("before_tool_call tool content private-data capture", () => {
|
||||
|
||||
it("attaches tool input but not output on execution errors", async () => {
|
||||
const execute = vi.fn().mockRejectedValue(new Error("boom"));
|
||||
const tool = wrapToolWithBeforeToolCallHook({ name: "read", execute } as any, {
|
||||
const tool = wrapToolWithBeforeToolCallHook(asAgentTool({ name: "read", execute }), {
|
||||
agentId: "main",
|
||||
sessionKey: "session-key",
|
||||
runId: "run-1",
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
wrapToolWithBeforeToolCallHook,
|
||||
} from "./agent-tools.before-tool-call.js";
|
||||
import { normalizeToolParameters } from "./agent-tools.schema.js";
|
||||
import type { AnyAgentTool } from "./agent-tools.types.js";
|
||||
import { markCodeModeControlTool } from "./code-mode-control-tools.js";
|
||||
import { CODE_MODE_EXEC_TOOL_NAME, createCodeModeTools } from "./code-mode.js";
|
||||
import { splitSdkTools } from "./embedded-agent-runner.js";
|
||||
@@ -39,6 +40,15 @@ import { setToolTerminalPresentation } from "./tool-terminal-presentation.js";
|
||||
|
||||
type BeforeToolCallHandlerMock = ReturnType<typeof vi.fn>;
|
||||
|
||||
function asAgentTool(tool: {
|
||||
description?: string;
|
||||
execute: ReturnType<typeof vi.fn>;
|
||||
name: string;
|
||||
parameters?: object;
|
||||
}): AnyAgentTool {
|
||||
return tool as unknown as AnyAgentTool;
|
||||
}
|
||||
|
||||
type BeforeToolCallHookInstall = {
|
||||
pluginId: string;
|
||||
priority?: number;
|
||||
@@ -103,7 +113,7 @@ describe("before_tool_call hook integration", () => {
|
||||
it("executes tool normally when no hook is registered", async () => {
|
||||
beforeToolCallHook = installBeforeToolCallHook({ enabled: false });
|
||||
const execute = vi.fn().mockResolvedValue({ content: [], details: { ok: true } });
|
||||
const tool = wrapToolWithBeforeToolCallHook({ name: "Read", execute } as any, {
|
||||
const tool = wrapToolWithBeforeToolCallHook(asAgentTool({ name: "Read", execute }), {
|
||||
agentId: "main",
|
||||
sessionKey: "main",
|
||||
});
|
||||
@@ -123,10 +133,10 @@ describe("before_tool_call hook integration", () => {
|
||||
it("records structured replay trust only for concrete core-owned tools", async () => {
|
||||
beforeToolCallHook = installBeforeToolCallHook({ enabled: false });
|
||||
const execute = vi.fn().mockResolvedValue({ content: [], details: { ok: true } });
|
||||
const coreTool = wrapToolWithBeforeToolCallHook({ name: "search", execute } as any, {
|
||||
const coreTool = wrapToolWithBeforeToolCallHook(asAgentTool({ name: "search", execute }), {
|
||||
runId: "run-core",
|
||||
});
|
||||
const pluginSource = { name: "search", execute } as any;
|
||||
const pluginSource = asAgentTool({ name: "search", execute });
|
||||
setPluginToolMeta(pluginSource, { pluginId: "example", optional: false });
|
||||
const pluginTool = wrapToolWithBeforeToolCallHook(pluginSource, {
|
||||
runId: "run-plugin",
|
||||
@@ -173,7 +183,7 @@ describe("before_tool_call hook integration", () => {
|
||||
runBeforeToolCallImpl: async () => ({ params: { mode: "safe" } }),
|
||||
});
|
||||
const execute = vi.fn().mockResolvedValue({ content: [], details: { ok: true } });
|
||||
const tool = wrapToolWithBeforeToolCallHook({ name: "exec", execute } as any);
|
||||
const tool = wrapToolWithBeforeToolCallHook(asAgentTool({ name: "exec", execute }));
|
||||
const extensionContext = {} as Parameters<typeof tool.execute>[3];
|
||||
|
||||
await tool.execute("call-2", { cmd: "ls" }, undefined, extensionContext);
|
||||
@@ -194,7 +204,7 @@ describe("before_tool_call hook integration", () => {
|
||||
}),
|
||||
});
|
||||
const execute = vi.fn().mockResolvedValue({ content: [], details: { ok: true } });
|
||||
const tool = wrapToolWithBeforeToolCallHook({ name: "exec", execute } as any);
|
||||
const tool = wrapToolWithBeforeToolCallHook(asAgentTool({ name: "exec", execute }));
|
||||
const extensionContext = {} as Parameters<typeof tool.execute>[3];
|
||||
|
||||
await expect(
|
||||
@@ -219,7 +229,7 @@ describe("before_tool_call hook integration", () => {
|
||||
]);
|
||||
|
||||
const execute = vi.fn().mockResolvedValue({ content: [], details: { ok: true } });
|
||||
const tool = wrapToolWithBeforeToolCallHook({ name: "exec", execute } as any);
|
||||
const tool = wrapToolWithBeforeToolCallHook(asAgentTool({ name: "exec", execute }));
|
||||
const extensionContext = {} as Parameters<typeof tool.execute>[3];
|
||||
|
||||
await expect(
|
||||
@@ -245,7 +255,7 @@ describe("before_tool_call hook integration", () => {
|
||||
},
|
||||
});
|
||||
const execute = vi.fn().mockResolvedValue({ content: [], details: { ok: true } });
|
||||
const tool = wrapToolWithBeforeToolCallHook({ name: "read", execute } as any);
|
||||
const tool = wrapToolWithBeforeToolCallHook(asAgentTool({ name: "read", execute }));
|
||||
const extensionContext = {} as Parameters<typeof tool.execute>[3];
|
||||
|
||||
await expect(
|
||||
@@ -259,7 +269,7 @@ describe("before_tool_call hook integration", () => {
|
||||
runBeforeToolCallImpl: async () => undefined,
|
||||
});
|
||||
const execute = vi.fn().mockResolvedValue({ content: [], details: { ok: true } });
|
||||
const tool = wrapToolWithBeforeToolCallHook({ name: "ReAd", execute } as any, {
|
||||
const tool = wrapToolWithBeforeToolCallHook(asAgentTool({ name: "ReAd", execute }), {
|
||||
agentId: "main",
|
||||
sessionKey: "main",
|
||||
sessionId: "ephemeral-main",
|
||||
@@ -296,10 +306,10 @@ describe("before_tool_call hook integration", () => {
|
||||
.mockResolvedValueOnce({ params: { marker: "B" } }),
|
||||
});
|
||||
const execute = vi.fn().mockResolvedValue({ content: [], details: { ok: true } });
|
||||
const toolA = wrapToolWithBeforeToolCallHook({ name: "Read", execute } as any, {
|
||||
const toolA = wrapToolWithBeforeToolCallHook(asAgentTool({ name: "Read", execute }), {
|
||||
runId: "run-a",
|
||||
});
|
||||
const toolB = wrapToolWithBeforeToolCallHook({ name: "Read", execute } as any, {
|
||||
const toolB = wrapToolWithBeforeToolCallHook(asAgentTool({ name: "Read", execute }), {
|
||||
runId: "run-b",
|
||||
});
|
||||
const extensionContextA = {} as Parameters<typeof toolA.execute>[3];
|
||||
@@ -334,7 +344,12 @@ describe("before_tool_call hook deduplication (#15502)", () => {
|
||||
|
||||
it("fires hook exactly once when tool goes through wrap + toToolDefinitions", async () => {
|
||||
const execute = vi.fn().mockResolvedValue({ content: [], details: { ok: true } });
|
||||
const baseTool = { name: "web_fetch", execute, description: "fetch", parameters: {} } as any;
|
||||
const baseTool = asAgentTool({
|
||||
name: "web_fetch",
|
||||
execute,
|
||||
description: "fetch",
|
||||
parameters: {},
|
||||
});
|
||||
|
||||
const wrapped = wrapToolWithBeforeToolCallHook(baseTool, {
|
||||
agentId: "main",
|
||||
@@ -553,7 +568,14 @@ describe("before_tool_call hook deduplication (#15502)", () => {
|
||||
});
|
||||
const plainExecute = vi.fn().mockResolvedValue({ content: [], details: { ok: true } });
|
||||
const [plainExecDef] = toToolDefinitions(
|
||||
[{ name: "exec", execute: plainExecute, description: "Plain exec", parameters: {} } as any],
|
||||
[
|
||||
asAgentTool({
|
||||
name: "exec",
|
||||
execute: plainExecute,
|
||||
description: "Plain exec",
|
||||
parameters: {},
|
||||
}),
|
||||
],
|
||||
{
|
||||
agentId: "main",
|
||||
sessionKey: "agent:main:main",
|
||||
@@ -705,12 +727,14 @@ describe("before_tool_call hook deduplication (#15502)", () => {
|
||||
runBeforeToolCallImpl: async () => ({ params: { command: "return 2;" } }),
|
||||
});
|
||||
const execute = vi.fn().mockResolvedValue({ content: [], details: { ok: true } });
|
||||
const tool = markCodeModeControlTool({
|
||||
name: CODE_MODE_EXEC_TOOL_NAME,
|
||||
execute,
|
||||
description: "exec",
|
||||
parameters: {},
|
||||
} as any);
|
||||
const tool = markCodeModeControlTool(
|
||||
asAgentTool({
|
||||
name: CODE_MODE_EXEC_TOOL_NAME,
|
||||
execute,
|
||||
description: "exec",
|
||||
parameters: {},
|
||||
}),
|
||||
);
|
||||
const [def] = toToolDefinitions([tool], {
|
||||
agentId: "main",
|
||||
sessionKey: "agent:main:main",
|
||||
@@ -813,12 +837,14 @@ describe("before_tool_call hook deduplication (#15502)", () => {
|
||||
initializeGlobalHookRunner(registry);
|
||||
try {
|
||||
const execute = vi.fn().mockResolvedValue({ content: [], details: { ok: true } });
|
||||
const tool = markCodeModeControlTool({
|
||||
name: CODE_MODE_EXEC_TOOL_NAME,
|
||||
execute,
|
||||
description: "exec",
|
||||
parameters: {},
|
||||
} as any);
|
||||
const tool = markCodeModeControlTool(
|
||||
asAgentTool({
|
||||
name: CODE_MODE_EXEC_TOOL_NAME,
|
||||
execute,
|
||||
description: "exec",
|
||||
parameters: {},
|
||||
}),
|
||||
);
|
||||
const [def] = toToolDefinitions([tool], {
|
||||
agentId: "main",
|
||||
sessionKey: "agent:main:main",
|
||||
@@ -973,7 +999,7 @@ describe("before_tool_call hook deduplication (#15502)", () => {
|
||||
|
||||
it("fires hook exactly once when tool goes through wrap + abort + toToolDefinitions", async () => {
|
||||
const execute = vi.fn().mockResolvedValue({ content: [], details: { ok: true } });
|
||||
const baseTool = { name: "Bash", execute, description: "bash", parameters: {} } as any;
|
||||
const baseTool = asAgentTool({ name: "Bash", execute, description: "bash", parameters: {} });
|
||||
|
||||
const abortController = new AbortController();
|
||||
const wrapped = wrapToolWithBeforeToolCallHook(baseTool, {
|
||||
@@ -998,7 +1024,7 @@ describe("before_tool_call hook deduplication (#15502)", () => {
|
||||
it("emits a tool-authored terminal presentation with the recorded outcome", async () => {
|
||||
const onToolOutcome = vi.fn();
|
||||
const sourceTool = setToolTerminalPresentation(
|
||||
{
|
||||
asAgentTool({
|
||||
name: "web_fetch",
|
||||
description: "fetch",
|
||||
parameters: {},
|
||||
@@ -1006,7 +1032,7 @@ describe("before_tool_call hook deduplication (#15502)", () => {
|
||||
content: [],
|
||||
details: { status: 200 },
|
||||
}),
|
||||
} as any,
|
||||
}),
|
||||
(_params, result) => ({
|
||||
text: `Fetched with status ${(result.details as { status: number }).status}`,
|
||||
}),
|
||||
@@ -1063,23 +1089,23 @@ describe("before_tool_call hook deduplication (#15502)", () => {
|
||||
};
|
||||
const presentationTool = wrapToolWithBeforeToolCallHook(
|
||||
setToolTerminalPresentation(
|
||||
{
|
||||
asAgentTool({
|
||||
name: "web_fetch",
|
||||
description: "fetch",
|
||||
parameters: {},
|
||||
execute: vi.fn(() => presentationExecution),
|
||||
} as any,
|
||||
}),
|
||||
() => ({ text: "Fetched with status 200" }),
|
||||
),
|
||||
hookContext,
|
||||
);
|
||||
const plainTool = wrapToolWithBeforeToolCallHook(
|
||||
{
|
||||
asAgentTool({
|
||||
name: "read_file",
|
||||
description: "read",
|
||||
parameters: {},
|
||||
execute: vi.fn(() => plainExecution),
|
||||
} as any,
|
||||
}),
|
||||
hookContext,
|
||||
);
|
||||
|
||||
@@ -1119,7 +1145,7 @@ describe("before_tool_call hook deduplication (#15502)", () => {
|
||||
|
||||
it("passes hook context for unwrapped tool definitions", async () => {
|
||||
const execute = vi.fn().mockResolvedValue({ content: [], details: { ok: true } });
|
||||
const baseTool = { name: "exec", execute, description: "exec", parameters: {} } as any;
|
||||
const baseTool = asAgentTool({ name: "exec", execute, description: "exec", parameters: {} });
|
||||
const def = expectDefined(
|
||||
toToolDefinitions([baseTool], {
|
||||
agentId: "code-agent",
|
||||
@@ -1162,7 +1188,7 @@ describe("before_tool_call hook deduplication (#15502)", () => {
|
||||
|
||||
it("preserves the hook marker when abort wrapping a hooked tool", () => {
|
||||
const execute = vi.fn().mockResolvedValue({ content: [], details: { ok: true } });
|
||||
const baseTool = { name: "Bash", execute, description: "bash", parameters: {} } as any;
|
||||
const baseTool = asAgentTool({ name: "Bash", execute, description: "bash", parameters: {} });
|
||||
const wrapped = wrapToolWithBeforeToolCallHook(baseTool, {
|
||||
agentId: "main",
|
||||
sessionKey: "main",
|
||||
|
||||
@@ -34,7 +34,7 @@ describe("compaction token accounting sanitization", () => {
|
||||
content: [{ type: "text", text: "ok" }],
|
||||
details: { raw: "x".repeat(50_000) },
|
||||
timestamp: 1,
|
||||
} as any,
|
||||
} as AgentMessage,
|
||||
{
|
||||
role: "user",
|
||||
content: "next",
|
||||
@@ -65,13 +65,13 @@ describe("compaction token accounting sanitization", () => {
|
||||
content: [{ type: "text", text: "ok" }],
|
||||
details: { raw: "x".repeat(50_000) },
|
||||
timestamp: 1,
|
||||
} as any,
|
||||
} as AgentMessage,
|
||||
{
|
||||
role: "custom",
|
||||
customType: "openclaw.runtime-context",
|
||||
content: "internal",
|
||||
timestamp: 2,
|
||||
} as any,
|
||||
} as AgentMessage,
|
||||
{
|
||||
role: "user",
|
||||
content: "next",
|
||||
|
||||
@@ -795,7 +795,9 @@ describe("downgradeOpenAIReasoningBlocks", () => {
|
||||
},
|
||||
];
|
||||
|
||||
expect(downgradeOpenAIReasoningBlocks(input as any)).toEqual(input);
|
||||
expect(
|
||||
downgradeOpenAIReasoningBlocks(input as Parameters<typeof downgradeOpenAIReasoningBlocks>[0]),
|
||||
).toEqual(input);
|
||||
});
|
||||
|
||||
it("drops replayable reasoning when requested even with following content", () => {
|
||||
@@ -813,9 +815,12 @@ describe("downgradeOpenAIReasoningBlocks", () => {
|
||||
},
|
||||
];
|
||||
|
||||
expect(downgradeOpenAIReasoningBlocks(input as any, { dropReplayableReasoning: true })).toEqual(
|
||||
[{ role: "assistant", content: [{ type: "text", text: "answer" }] }],
|
||||
);
|
||||
expect(
|
||||
downgradeOpenAIReasoningBlocks(
|
||||
input as Parameters<typeof downgradeOpenAIReasoningBlocks>[0],
|
||||
{ dropReplayableReasoning: true },
|
||||
),
|
||||
).toEqual([{ role: "assistant", content: [{ type: "text", text: "answer" }] }]);
|
||||
});
|
||||
|
||||
it("drops the paired message id when replayable reasoning is dropped", () => {
|
||||
@@ -837,9 +842,12 @@ describe("downgradeOpenAIReasoningBlocks", () => {
|
||||
},
|
||||
];
|
||||
|
||||
expect(downgradeOpenAIReasoningBlocks(input as any, { dropReplayableReasoning: true })).toEqual(
|
||||
[{ role: "assistant", content: [{ type: "text", text: "answer" }] }],
|
||||
);
|
||||
expect(
|
||||
downgradeOpenAIReasoningBlocks(
|
||||
input as Parameters<typeof downgradeOpenAIReasoningBlocks>[0],
|
||||
{ dropReplayableReasoning: true },
|
||||
),
|
||||
).toEqual([{ role: "assistant", content: [{ type: "text", text: "answer" }] }]);
|
||||
});
|
||||
|
||||
it("keeps the paired message id when reasoning is preserved", () => {
|
||||
@@ -861,7 +869,9 @@ describe("downgradeOpenAIReasoningBlocks", () => {
|
||||
},
|
||||
];
|
||||
|
||||
expect(downgradeOpenAIReasoningBlocks(input as any)).toEqual(input);
|
||||
expect(
|
||||
downgradeOpenAIReasoningBlocks(input as Parameters<typeof downgradeOpenAIReasoningBlocks>[0]),
|
||||
).toEqual(input);
|
||||
});
|
||||
|
||||
it("drops paired message ids across every text block when reasoning is dropped", () => {
|
||||
@@ -887,25 +897,28 @@ describe("downgradeOpenAIReasoningBlocks", () => {
|
||||
},
|
||||
];
|
||||
|
||||
expect(downgradeOpenAIReasoningBlocks(input as any, { dropReplayableReasoning: true })).toEqual(
|
||||
[
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "commentary",
|
||||
textSignature: JSON.stringify({ v: 1, phase: "commentary" }),
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
text: "final",
|
||||
textSignature: JSON.stringify({ v: 1, phase: "final_answer" }),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
);
|
||||
expect(
|
||||
downgradeOpenAIReasoningBlocks(
|
||||
input as Parameters<typeof downgradeOpenAIReasoningBlocks>[0],
|
||||
{ dropReplayableReasoning: true },
|
||||
),
|
||||
).toEqual([
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "commentary",
|
||||
textSignature: JSON.stringify({ v: 1, phase: "commentary" }),
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
text: "final",
|
||||
textSignature: JSON.stringify({ v: 1, phase: "final_answer" }),
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("drops orphaned reasoning blocks without following content", () => {
|
||||
@@ -922,9 +935,9 @@ describe("downgradeOpenAIReasoningBlocks", () => {
|
||||
{ role: "user", content: "next" },
|
||||
];
|
||||
|
||||
expect(downgradeOpenAIReasoningBlocks(input as any)).toEqual([
|
||||
{ role: "user", content: "next" },
|
||||
]);
|
||||
expect(
|
||||
downgradeOpenAIReasoningBlocks(input as Parameters<typeof downgradeOpenAIReasoningBlocks>[0]),
|
||||
).toEqual([{ role: "user", content: "next" }]);
|
||||
});
|
||||
|
||||
it("drops object-form orphaned signatures", () => {
|
||||
@@ -940,7 +953,11 @@ describe("downgradeOpenAIReasoningBlocks", () => {
|
||||
},
|
||||
];
|
||||
|
||||
expect(downgradeOpenAIReasoningBlocks(input as any)).toStrictEqual([]);
|
||||
expect(
|
||||
downgradeOpenAIReasoningBlocks(
|
||||
input as unknown as Parameters<typeof downgradeOpenAIReasoningBlocks>[0],
|
||||
),
|
||||
).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("keeps non-reasoning thinking signatures", () => {
|
||||
@@ -957,7 +974,9 @@ describe("downgradeOpenAIReasoningBlocks", () => {
|
||||
},
|
||||
];
|
||||
|
||||
expect(downgradeOpenAIReasoningBlocks(input as any)).toEqual(input);
|
||||
expect(
|
||||
downgradeOpenAIReasoningBlocks(input as Parameters<typeof downgradeOpenAIReasoningBlocks>[0]),
|
||||
).toEqual(input);
|
||||
});
|
||||
|
||||
it("is idempotent for orphaned reasoning cleanup", () => {
|
||||
@@ -974,8 +993,12 @@ describe("downgradeOpenAIReasoningBlocks", () => {
|
||||
{ role: "user", content: "next" },
|
||||
];
|
||||
|
||||
const once = downgradeOpenAIReasoningBlocks(input as any);
|
||||
const twice = downgradeOpenAIReasoningBlocks(once as any);
|
||||
const once = downgradeOpenAIReasoningBlocks(
|
||||
input as Parameters<typeof downgradeOpenAIReasoningBlocks>[0],
|
||||
);
|
||||
const twice = downgradeOpenAIReasoningBlocks(
|
||||
once as Parameters<typeof downgradeOpenAIReasoningBlocks>[0],
|
||||
);
|
||||
expect(twice).toEqual(once);
|
||||
});
|
||||
});
|
||||
@@ -1019,7 +1042,11 @@ describe("downgradeOpenAIFunctionCallReasoningPairs", () => {
|
||||
makeToolResult(callIdWithReasoning, "ok"),
|
||||
];
|
||||
|
||||
expect(downgradeOpenAIFunctionCallReasoningPairs(input as any)).toEqual([
|
||||
expect(
|
||||
downgradeOpenAIFunctionCallReasoningPairs(
|
||||
input as Parameters<typeof downgradeOpenAIFunctionCallReasoningPairs>[0],
|
||||
),
|
||||
).toEqual([
|
||||
makePlainAssistantTurn(callIdWithoutReasoning),
|
||||
makeToolResult(callIdWithoutReasoning, "ok"),
|
||||
]);
|
||||
@@ -1031,7 +1058,11 @@ describe("downgradeOpenAIFunctionCallReasoningPairs", () => {
|
||||
makeToolResult(callIdWithReasoning, "ok"),
|
||||
];
|
||||
|
||||
expect(downgradeOpenAIFunctionCallReasoningPairs(input as any)).toEqual(input);
|
||||
expect(
|
||||
downgradeOpenAIFunctionCallReasoningPairs(
|
||||
input as Parameters<typeof downgradeOpenAIFunctionCallReasoningPairs>[0],
|
||||
),
|
||||
).toEqual(input);
|
||||
});
|
||||
|
||||
it("only rewrites tool results paired to the downgraded assistant turn", () => {
|
||||
@@ -1042,7 +1073,11 @@ describe("downgradeOpenAIFunctionCallReasoningPairs", () => {
|
||||
makeToolResult(callIdWithReasoning, "turn2"),
|
||||
];
|
||||
|
||||
expect(downgradeOpenAIFunctionCallReasoningPairs(input as any)).toEqual([
|
||||
expect(
|
||||
downgradeOpenAIFunctionCallReasoningPairs(
|
||||
input as Parameters<typeof downgradeOpenAIFunctionCallReasoningPairs>[0],
|
||||
),
|
||||
).toEqual([
|
||||
makePlainAssistantTurn(callIdWithoutReasoning),
|
||||
makeToolResult(callIdWithoutReasoning, "turn1"),
|
||||
makeReasoningAssistantTurn(callIdWithReasoning),
|
||||
|
||||
@@ -13,6 +13,9 @@ import { loadOpenClawPlugins } from "../plugins/loader.js";
|
||||
import { deleteTestEnvValue, setTestEnvValue } from "../test-utils/env.js";
|
||||
import { guardSessionManager } from "./session-tool-result-guard-wrapper.js";
|
||||
|
||||
type ToolResultMessage = Extract<AgentMessage, { role: "toolResult" }>;
|
||||
type PersistedToolResultMessage = ToolResultMessage & { details: Record<string, unknown> };
|
||||
|
||||
const EMPTY_PLUGIN_SCHEMA = { type: "object", additionalProperties: false, properties: {} };
|
||||
const originalBundledPluginsDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
const originalConfigPath = process.env.OPENCLAW_CONFIG_PATH;
|
||||
@@ -53,7 +56,7 @@ function appendToolCallAndResult(sm: ReturnType<typeof SessionManager.inMemory>)
|
||||
isError: false,
|
||||
content: [{ type: "text", text: "ok" }],
|
||||
details: { big: "x".repeat(10_000) },
|
||||
} as any);
|
||||
} as ToolResultMessage);
|
||||
}
|
||||
|
||||
function appendToolResultWithTail(
|
||||
@@ -71,7 +74,7 @@ function appendToolResultWithTail(
|
||||
isError: false,
|
||||
content: [{ type: "text", text: "visible output stays small" }],
|
||||
details: { status: "completed", tail },
|
||||
} as any);
|
||||
} as ToolResultMessage);
|
||||
}
|
||||
|
||||
function getPersistedToolResult(sm: ReturnType<typeof SessionManager.inMemory>) {
|
||||
@@ -80,10 +83,18 @@ function getPersistedToolResult(sm: ReturnType<typeof SessionManager.inMemory>)
|
||||
.filter((e) => e.type === "message")
|
||||
.map((e) => (e as { message: AgentMessage }).message);
|
||||
|
||||
return messages.find((m) => (m as any).role === "toolResult") as any;
|
||||
return messages.find((message): message is ToolResultMessage => message.role === "toolResult");
|
||||
}
|
||||
|
||||
function requirePersistedToolResult(sm: ReturnType<typeof SessionManager.inMemory>) {
|
||||
function hasRecordDetails(message: ToolResultMessage): message is PersistedToolResultMessage {
|
||||
return (
|
||||
typeof message.details === "object" &&
|
||||
message.details !== null &&
|
||||
!Array.isArray(message.details)
|
||||
);
|
||||
}
|
||||
|
||||
function requirePersistedToolResultMessage(sm: ReturnType<typeof SessionManager.inMemory>) {
|
||||
const toolResult = getPersistedToolResult(sm);
|
||||
if (!toolResult) {
|
||||
throw new Error("expected persisted toolResult message");
|
||||
@@ -91,6 +102,22 @@ function requirePersistedToolResult(sm: ReturnType<typeof SessionManager.inMemor
|
||||
return toolResult;
|
||||
}
|
||||
|
||||
function requirePersistedToolResult(sm: ReturnType<typeof SessionManager.inMemory>) {
|
||||
const toolResult = requirePersistedToolResultMessage(sm);
|
||||
if (!hasRecordDetails(toolResult)) {
|
||||
throw new Error("expected persisted toolResult message with object details");
|
||||
}
|
||||
return toolResult;
|
||||
}
|
||||
|
||||
function requireToolResultText(message: ToolResultMessage): string {
|
||||
const text = message.content.find((block) => block.type === "text")?.text;
|
||||
if (text === undefined) {
|
||||
throw new Error("expected persisted toolResult text content");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function initializeTempPlugin(params: { tmpPrefix: string; id: string; body: string }) {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), params.tmpPrefix));
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins";
|
||||
@@ -114,8 +141,7 @@ function initializeTempPlugin(params: { tmpPrefix: string; id: string; body: str
|
||||
|
||||
function expectPersistedToolResultTextCapped(sm: ReturnType<typeof SessionManager.inMemory>) {
|
||||
const toolResult = requirePersistedToolResult(sm);
|
||||
const text = toolResult.content.find((block: { type: string }) => block.type === "text")?.text;
|
||||
expect(typeof text).toBe("string");
|
||||
const text = requireToolResultText(toolResult);
|
||||
expect(text.length).toBeLessThanOrEqual(120);
|
||||
expect(text).toContain("truncated");
|
||||
}
|
||||
@@ -123,7 +149,7 @@ function expectPersistedToolResultTextCapped(sm: ReturnType<typeof SessionManage
|
||||
function expectPersistedToolResultDetailsCapped(sm: ReturnType<typeof SessionManager.inMemory>) {
|
||||
// Large details are summarized before persistence to keep transcript files bounded.
|
||||
const toolResult = requirePersistedToolResult(sm);
|
||||
const details = toolResult.details as Record<string, unknown>;
|
||||
const details = toolResult.details;
|
||||
expect(details.persistedDetailsTruncated).toBe(true);
|
||||
expect(details.aggregated).toBeUndefined();
|
||||
expect(Buffer.byteLength(JSON.stringify(details), "utf-8")).toBeLessThan(8_192);
|
||||
@@ -184,7 +210,7 @@ describe("tool_result_persist hook", () => {
|
||||
error: null,
|
||||
payload: "x".repeat(10_000),
|
||||
},
|
||||
} as any);
|
||||
} as ToolResultMessage);
|
||||
|
||||
const details = requirePersistedToolResult(sm).details;
|
||||
expect(details.persistedDetailsTruncated).toBe(true);
|
||||
@@ -229,11 +255,11 @@ describe("tool_result_persist hook", () => {
|
||||
items: [`curl --token ${tokenValue} https://example.test`],
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
} as ToolResultMessage);
|
||||
|
||||
const toolResult = requirePersistedToolResult(sm);
|
||||
const serialized = JSON.stringify(toolResult.details);
|
||||
expect(toolResult.content[0]?.text).toBe("visible output stays small");
|
||||
expect(requireToolResultText(toolResult)).toBe("visible output stays small");
|
||||
expect(serialized).toContain("GITHUB_TOKEN=");
|
||||
expect(serialized).toContain("Bearer");
|
||||
expect(serialized).toContain("…");
|
||||
@@ -268,7 +294,7 @@ describe("tool_result_persist hook", () => {
|
||||
details: {
|
||||
diagnostic: customSecret,
|
||||
},
|
||||
} as any);
|
||||
} as ToolResultMessage);
|
||||
|
||||
const toolResult = requirePersistedToolResult(sm);
|
||||
const serialized = JSON.stringify(toolResult);
|
||||
@@ -303,7 +329,7 @@ describe("tool_result_persist hook", () => {
|
||||
details: {
|
||||
token: { value: "shortsecret" },
|
||||
},
|
||||
} as any);
|
||||
} as ToolResultMessage);
|
||||
|
||||
const toolResult = requirePersistedToolResult(sm);
|
||||
const serialized = JSON.stringify(toolResult.details);
|
||||
@@ -335,7 +361,7 @@ describe("tool_result_persist hook", () => {
|
||||
[`https://example.test/callback?token=${tokenValue}`]: "ok",
|
||||
deepDetails,
|
||||
},
|
||||
} as any);
|
||||
} as ToolResultMessage);
|
||||
|
||||
const toolResult = requirePersistedToolResult(sm);
|
||||
const serialized = JSON.stringify(toolResult.details);
|
||||
@@ -375,10 +401,10 @@ describe("tool_result_persist hook", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
} as any);
|
||||
} as ToolResultMessage);
|
||||
|
||||
const toolResult = getPersistedToolResult(sm);
|
||||
expect(toolResult.content[0]?.text).toBe("visible output stays small");
|
||||
const toolResult = requirePersistedToolResult(sm);
|
||||
expect(requireToolResultText(toolResult)).toBe("visible output stays small");
|
||||
expectPersistedToolResultDetailsCapped(sm);
|
||||
});
|
||||
|
||||
@@ -443,11 +469,11 @@ describe("tool_result_persist hook", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
} as any);
|
||||
} as ToolResultMessage);
|
||||
|
||||
const toolResult = requirePersistedToolResult(sm);
|
||||
const serialized = JSON.stringify(toolResult.details);
|
||||
expect(toolResult.content[0]?.text).toBe("visible output stays small");
|
||||
expect(requireToolResultText(toolResult)).toBe("visible output stays small");
|
||||
expect(toolResult.details.persistedDetailsTruncated).toBe(true);
|
||||
expect(serialized).toContain("token=***");
|
||||
expect(serialized).toContain("partial secret span omitted");
|
||||
@@ -490,14 +516,14 @@ describe("tool_result_persist hook", () => {
|
||||
command: `node script-${i}.js ${"x".repeat(6_000)}`,
|
||||
})),
|
||||
},
|
||||
} as any);
|
||||
} as ToolResultMessage);
|
||||
|
||||
const toolResult = requirePersistedToolResult(sm);
|
||||
const details = toolResult.details;
|
||||
const serialized = JSON.stringify(details);
|
||||
expect(details.persistedDetailsTruncated).toBe(true);
|
||||
expect(details.finalDetailsTruncated).toBe(true);
|
||||
expect(details.status?.token).toBe("***");
|
||||
expect(details.status).toMatchObject({ token: "***" });
|
||||
expect(details.spilledChars).toBe(2_000_000);
|
||||
expect(details.spillTruncated).toBe(true);
|
||||
expect(serialized).not.toContain(tokenValue);
|
||||
@@ -531,7 +557,7 @@ describe("tool_result_persist hook", () => {
|
||||
aggregated: "x".repeat(120_000),
|
||||
tail,
|
||||
},
|
||||
} as any);
|
||||
} as ToolResultMessage);
|
||||
|
||||
const toolResult = requirePersistedToolResult(sm);
|
||||
const serialized = JSON.stringify(toolResult.details);
|
||||
@@ -561,7 +587,7 @@ describe("tool_result_persist hook", () => {
|
||||
status: "completed",
|
||||
tail: `${"x".repeat(1_000)}{"token":"${longSecret}${"z".repeat(1_000)}`,
|
||||
},
|
||||
} as any);
|
||||
} as ToolResultMessage);
|
||||
|
||||
const toolResult = requirePersistedToolResult(sm);
|
||||
const serialized = JSON.stringify(toolResult.details);
|
||||
@@ -606,13 +632,13 @@ describe("tool_result_persist hook", () => {
|
||||
isError: false,
|
||||
content: [{ type: "text", text: "visible output stays small" }],
|
||||
details: oversizedDetails,
|
||||
} as any);
|
||||
} as ToolResultMessage);
|
||||
} finally {
|
||||
stringifySpy.mockRestore();
|
||||
}
|
||||
|
||||
const toolResult = getPersistedToolResult(sm);
|
||||
expect(toolResult.content[0]?.text).toBe("visible output stays small");
|
||||
const toolResult = requirePersistedToolResult(sm);
|
||||
expect(requireToolResultText(toolResult)).toBe("visible output stays small");
|
||||
expectPersistedToolResultDetailsCapped(sm);
|
||||
expect(stringifySpy).not.toHaveBeenCalledWith(oversizedDetails);
|
||||
});
|
||||
@@ -656,14 +682,14 @@ describe("tool_result_persist hook", () => {
|
||||
isError: false,
|
||||
content: [{ type: "text", text: "visible output stays small" }],
|
||||
details: wideDetails,
|
||||
} as any);
|
||||
} as ToolResultMessage);
|
||||
} finally {
|
||||
entriesSpy.mockRestore();
|
||||
keysSpy.mockRestore();
|
||||
}
|
||||
|
||||
const toolResult = getPersistedToolResult(sm);
|
||||
const details = toolResult.details as Record<string, unknown>;
|
||||
const toolResult = requirePersistedToolResult(sm);
|
||||
const details = toolResult.details;
|
||||
expect(details.persistedDetailsTruncated).toBe(true);
|
||||
expect(details.originalDetailKeys).toContain("status");
|
||||
expect(details.originalDetailKeys).toContain("sessionId");
|
||||
@@ -706,10 +732,10 @@ describe("tool_result_persist hook", () => {
|
||||
tail: "z".repeat(10_000),
|
||||
})),
|
||||
},
|
||||
} as any);
|
||||
} as ToolResultMessage);
|
||||
|
||||
const toolResult = getPersistedToolResult(sm);
|
||||
const details = toolResult.details as Record<string, unknown>;
|
||||
const toolResult = requirePersistedToolResult(sm);
|
||||
const details = toolResult.details;
|
||||
expect(details.persistedDetailsTruncated).toBe(true);
|
||||
expect(details.finalDetailsTruncated).toBe(true);
|
||||
expect(details.aggregated).toBeUndefined();
|
||||
@@ -765,7 +791,7 @@ describe("tool_result_persist hook", () => {
|
||||
});
|
||||
|
||||
appendToolCallAndResult(sm);
|
||||
const toolResult = requirePersistedToolResult(sm);
|
||||
const toolResult = requirePersistedToolResultMessage(sm);
|
||||
|
||||
// Hook registration should preserve a valid toolResult message shape.
|
||||
expect(toolResult.role).toBe("toolResult");
|
||||
|
||||
@@ -376,7 +376,7 @@ describe("edit tool", () => {
|
||||
|
||||
const tc0 = expectDefined(result.content[0], "result.content[0] test invariant");
|
||||
expect("text" in tc0 ? tc0.text : "").toContain("No changes made");
|
||||
expect((result as any).terminate).toBe(true);
|
||||
expect((result as { terminate?: boolean }).terminate).toBe(true);
|
||||
await expect(fs.readFile(filePath, "utf-8")).resolves.toBe("unchanged content\n");
|
||||
});
|
||||
|
||||
@@ -504,7 +504,7 @@ describe("edit tool", () => {
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect((result as any).terminate).toBe(true);
|
||||
expect((result as { terminate?: boolean }).terminate).toBe(true);
|
||||
await expect(fs.readFile(filePath, "utf-8")).resolves.toBe("foo\n");
|
||||
});
|
||||
|
||||
@@ -602,7 +602,7 @@ describe("edit tool", () => {
|
||||
|
||||
const tcText = expectDefined(result.content[0], "result.content[0] test invariant");
|
||||
expect("text" in tcText ? tcText.text : "").toContain("Successfully replaced");
|
||||
expect((result as any).terminate).toBeFalsy();
|
||||
expect((result as { terminate?: boolean }).terminate).toBeFalsy();
|
||||
await expect(fs.readFile(filePath, "utf-8")).resolves.toBe("alpha beta GAMMA\n");
|
||||
});
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ describe("write tool", () => {
|
||||
|
||||
const tc0 = expectDefined(result.content[0], "result.content[0] test invariant");
|
||||
expect("text" in tc0 ? tc0.text : "").toContain("No changes made");
|
||||
expect((result as any).terminate).toBe(true);
|
||||
expect((result as { terminate?: boolean }).terminate).toBe(true);
|
||||
await expect(fs.readFile(filePath, "utf-8")).resolves.toBe("hello\n");
|
||||
});
|
||||
|
||||
|
||||
@@ -22,6 +22,11 @@ vi.mock("../logging/subsystem.js", () => ({
|
||||
}));
|
||||
|
||||
type DummyTool = { name: string };
|
||||
type PolicyTool = Parameters<typeof applyToolPolicyPipeline>[0]["tools"][number];
|
||||
|
||||
function asPolicyTools(tools: DummyTool[]): PolicyTool[] {
|
||||
return tools as PolicyTool[];
|
||||
}
|
||||
|
||||
function runAllowlistWarningStep(params: {
|
||||
allow: string[];
|
||||
@@ -33,7 +38,7 @@ function runAllowlistWarningStep(params: {
|
||||
const warnings: string[] = [];
|
||||
const tools = [{ name: "exec" }] as unknown as DummyTool[];
|
||||
applyToolPolicyPipeline({
|
||||
tools: tools as any,
|
||||
tools: asPolicyTools(tools),
|
||||
toolMeta: () => undefined,
|
||||
warn: (msg) => warnings.push(msg),
|
||||
steps: [
|
||||
@@ -61,7 +66,7 @@ describe("tool-policy-pipeline", () => {
|
||||
test("preserves plugin-only allowlists instead of silently stripping them", () => {
|
||||
const tools = [{ name: "exec" }, { name: "plugin_tool" }] as unknown as DummyTool[];
|
||||
const filtered = applyToolPolicyPipeline({
|
||||
tools: tools as any,
|
||||
tools: asPolicyTools(tools),
|
||||
toolMeta: (t: any) => (t.name === "plugin_tool" ? { pluginId: "foo" } : undefined),
|
||||
warn: () => {},
|
||||
steps: [
|
||||
@@ -80,7 +85,7 @@ describe("tool-policy-pipeline", () => {
|
||||
const warnings: string[] = [];
|
||||
const tools = [{ name: "exec" }] as unknown as DummyTool[];
|
||||
applyToolPolicyPipeline({
|
||||
tools: tools as any,
|
||||
tools: asPolicyTools(tools),
|
||||
toolMeta: () => undefined,
|
||||
warn: (msg) => warnings.push(msg),
|
||||
steps: [
|
||||
@@ -142,7 +147,7 @@ describe("tool-policy-pipeline", () => {
|
||||
const warnings: string[] = [];
|
||||
const profilePolicy = resolveToolProfilePolicy("coding");
|
||||
applyToolPolicyPipeline({
|
||||
tools: [{ name: "exec" }] as any,
|
||||
tools: asPolicyTools([{ name: "exec" }]),
|
||||
toolMeta: () => undefined,
|
||||
warn: (msg) => warnings.push(msg),
|
||||
steps: buildDefaultToolPolicyPipelineSteps({
|
||||
@@ -158,7 +163,7 @@ describe("tool-policy-pipeline", () => {
|
||||
test("does not warn for declared plugin tools that are not materialized yet", () => {
|
||||
const warnings: string[] = [];
|
||||
applyToolPolicyPipeline({
|
||||
tools: [{ name: "exec" }] as any,
|
||||
tools: asPolicyTools([{ name: "exec" }]),
|
||||
toolMeta: () => undefined,
|
||||
warn: (msg) => warnings.push(msg),
|
||||
declaredToolAllowlist: { pluginToolNames: ["llm-task"] },
|
||||
@@ -177,7 +182,7 @@ describe("tool-policy-pipeline", () => {
|
||||
test("does not warn for declared MCP server namespace globs", () => {
|
||||
const warnings: string[] = [];
|
||||
applyToolPolicyPipeline({
|
||||
tools: [{ name: "exec" }] as any,
|
||||
tools: asPolicyTools([{ name: "exec" }]),
|
||||
toolMeta: () => undefined,
|
||||
warn: (msg) => warnings.push(msg),
|
||||
declaredToolAllowlist: { mcpServerNames: ["paperless", "Home Assistant"] },
|
||||
@@ -196,7 +201,7 @@ describe("tool-policy-pipeline", () => {
|
||||
test("still warns for undeclared MCP namespace globs", () => {
|
||||
const warnings: string[] = [];
|
||||
applyToolPolicyPipeline({
|
||||
tools: [{ name: "exec" }] as any,
|
||||
tools: asPolicyTools([{ name: "exec" }]),
|
||||
toolMeta: () => undefined,
|
||||
warn: (msg) => warnings.push(msg),
|
||||
declaredToolAllowlist: { mcpServerNames: ["paperless"] },
|
||||
@@ -260,7 +265,7 @@ describe("tool-policy-pipeline", () => {
|
||||
});
|
||||
|
||||
applyToolPolicyPipeline({
|
||||
tools: [{ name: "exec" }] as any,
|
||||
tools: asPolicyTools([{ name: "exec" }]),
|
||||
toolMeta: () => undefined,
|
||||
warn: (msg) => warnings.push(msg),
|
||||
declaredToolAllowlist: declared,
|
||||
@@ -289,7 +294,7 @@ describe("tool-policy-pipeline", () => {
|
||||
});
|
||||
|
||||
applyToolPolicyPipeline({
|
||||
tools: [{ name: "exec" }] as any,
|
||||
tools: asPolicyTools([{ name: "exec" }]),
|
||||
toolMeta: () => undefined,
|
||||
warn: (msg) => warnings.push(msg),
|
||||
declaredToolAllowlist: declared,
|
||||
@@ -318,7 +323,7 @@ describe("tool-policy-pipeline", () => {
|
||||
});
|
||||
|
||||
applyToolPolicyPipeline({
|
||||
tools: [{ name: "exec" }] as any,
|
||||
tools: asPolicyTools([{ name: "exec" }]),
|
||||
toolMeta: () => undefined,
|
||||
warn: (msg) => warnings.push(msg),
|
||||
declaredToolAllowlist: declared,
|
||||
@@ -347,7 +352,7 @@ describe("tool-policy-pipeline", () => {
|
||||
});
|
||||
|
||||
applyToolPolicyPipeline({
|
||||
tools: [{ name: "exec" }] as any,
|
||||
tools: asPolicyTools([{ name: "exec" }]),
|
||||
toolMeta: () => undefined,
|
||||
warn: (msg) => warnings.push(msg),
|
||||
declaredToolAllowlist: declared,
|
||||
@@ -376,7 +381,7 @@ describe("tool-policy-pipeline", () => {
|
||||
});
|
||||
|
||||
applyToolPolicyPipeline({
|
||||
tools: [{ name: "exec" }] as any,
|
||||
tools: asPolicyTools([{ name: "exec" }]),
|
||||
toolMeta: () => undefined,
|
||||
warn: (msg) => warnings.push(msg),
|
||||
declaredToolAllowlist: declared,
|
||||
@@ -403,7 +408,7 @@ describe("tool-policy-pipeline", () => {
|
||||
});
|
||||
|
||||
applyToolPolicyPipeline({
|
||||
tools: [{ name: "exec" }] as any,
|
||||
tools: asPolicyTools([{ name: "exec" }]),
|
||||
toolMeta: () => undefined,
|
||||
warn: (msg) => warnings.push(msg),
|
||||
declaredToolAllowlist: declared,
|
||||
@@ -439,7 +444,7 @@ describe("tool-policy-pipeline", () => {
|
||||
expect(Array.from(declared?.mcpServerNames ?? [])).toEqual(["vigil-harbor"]);
|
||||
|
||||
applyToolPolicyPipeline({
|
||||
tools: [{ name: "exec" }] as any,
|
||||
tools: asPolicyTools([{ name: "exec" }]),
|
||||
toolMeta: () => undefined,
|
||||
warn: (msg) => warnings.push(msg),
|
||||
declaredToolAllowlist: declared,
|
||||
@@ -461,7 +466,7 @@ describe("tool-policy-pipeline", () => {
|
||||
const warnings: string[] = [];
|
||||
const tools = [{ name: "exec" }] as unknown as DummyTool[];
|
||||
const params = {
|
||||
tools: tools as any,
|
||||
tools: asPolicyTools(tools),
|
||||
toolMeta: () => undefined,
|
||||
warn: (msg: string) => warnings.push(msg),
|
||||
steps: [
|
||||
@@ -487,7 +492,7 @@ describe("tool-policy-pipeline", () => {
|
||||
|
||||
for (let i = 0; i < 257; i += 1) {
|
||||
applyToolPolicyPipeline({
|
||||
tools: tools as any,
|
||||
tools: asPolicyTools(tools),
|
||||
toolMeta: () => undefined,
|
||||
warn: (msg: string) => warnings.push(msg),
|
||||
steps: [
|
||||
@@ -501,7 +506,7 @@ describe("tool-policy-pipeline", () => {
|
||||
}
|
||||
|
||||
applyToolPolicyPipeline({
|
||||
tools: tools as any,
|
||||
tools: asPolicyTools(tools),
|
||||
toolMeta: () => undefined,
|
||||
warn: (msg: string) => warnings.push(msg),
|
||||
steps: [
|
||||
@@ -522,7 +527,7 @@ describe("tool-policy-pipeline", () => {
|
||||
|
||||
for (let i = 0; i < 256; i += 1) {
|
||||
applyToolPolicyPipeline({
|
||||
tools: tools as any,
|
||||
tools: asPolicyTools(tools),
|
||||
toolMeta: () => undefined,
|
||||
warn: (msg: string) => warnings.push(msg),
|
||||
steps: [
|
||||
@@ -538,7 +543,7 @@ describe("tool-policy-pipeline", () => {
|
||||
warnings.length = 0;
|
||||
|
||||
applyToolPolicyPipeline({
|
||||
tools: tools as any,
|
||||
tools: asPolicyTools(tools),
|
||||
toolMeta: () => undefined,
|
||||
warn: (msg: string) => warnings.push(msg),
|
||||
steps: [
|
||||
@@ -550,7 +555,7 @@ describe("tool-policy-pipeline", () => {
|
||||
],
|
||||
});
|
||||
applyToolPolicyPipeline({
|
||||
tools: tools as any,
|
||||
tools: asPolicyTools(tools),
|
||||
toolMeta: () => undefined,
|
||||
warn: (msg: string) => warnings.push(msg),
|
||||
steps: [
|
||||
@@ -567,7 +572,7 @@ describe("tool-policy-pipeline", () => {
|
||||
test("applies allowlist filtering when core tools are explicitly listed", () => {
|
||||
const tools = [{ name: "exec" }, { name: "process" }] as unknown as DummyTool[];
|
||||
const filtered = applyToolPolicyPipeline({
|
||||
tools: tools as any,
|
||||
tools: asPolicyTools(tools),
|
||||
toolMeta: () => undefined,
|
||||
warn: () => {},
|
||||
steps: [
|
||||
@@ -584,7 +589,7 @@ describe("tool-policy-pipeline", () => {
|
||||
test("applies deny filtering after allow filtering", () => {
|
||||
const tools = [{ name: "exec" }, { name: "process" }] as unknown as DummyTool[];
|
||||
const filtered = applyToolPolicyPipeline({
|
||||
tools: tools as any,
|
||||
tools: asPolicyTools(tools),
|
||||
toolMeta: () => undefined,
|
||||
warn: () => {},
|
||||
steps: [
|
||||
@@ -607,7 +612,7 @@ describe("tool-policy-pipeline", () => {
|
||||
] as unknown as DummyTool[];
|
||||
|
||||
applyToolPolicyPipeline({
|
||||
tools: tools as any,
|
||||
tools: asPolicyTools(tools),
|
||||
toolMeta: () => undefined,
|
||||
warn: () => {},
|
||||
steps: [
|
||||
@@ -635,7 +640,7 @@ describe("tool-policy-pipeline", () => {
|
||||
const tools = [{ name: "exec" }, { name: "browser" }] as unknown as DummyTool[];
|
||||
|
||||
applyToolPolicyPipeline({
|
||||
tools: tools as any,
|
||||
tools: asPolicyTools(tools),
|
||||
toolMeta: () => undefined,
|
||||
warn: () => {},
|
||||
auditLogLevel: "debug",
|
||||
@@ -664,7 +669,7 @@ describe("tool-policy-pipeline", () => {
|
||||
const tools = [{ name: "exec" }, { name: "browser" }] as unknown as DummyTool[];
|
||||
|
||||
applyToolPolicyPipeline({
|
||||
tools: tools as any,
|
||||
tools: asPolicyTools(tools),
|
||||
toolMeta: () => undefined,
|
||||
warn: () => {},
|
||||
steps: [
|
||||
@@ -697,7 +702,7 @@ describe("tool-policy-pipeline", () => {
|
||||
] as unknown as DummyTool[];
|
||||
|
||||
applyToolPolicyPipeline({
|
||||
tools: tools as any,
|
||||
tools: asPolicyTools(tools),
|
||||
toolMeta: () => undefined,
|
||||
warn: () => {},
|
||||
steps: [
|
||||
@@ -736,7 +741,7 @@ describe("tool-policy-pipeline", () => {
|
||||
const tools = [{ name: "exec" }] as unknown as DummyTool[];
|
||||
|
||||
applyToolPolicyPipeline({
|
||||
tools: tools as any,
|
||||
tools: asPolicyTools(tools),
|
||||
toolMeta: () => undefined,
|
||||
warn: () => {},
|
||||
steps: [
|
||||
@@ -755,7 +760,7 @@ describe("tool-policy-pipeline", () => {
|
||||
const tools = [{ name: "exec\nbad" }] as unknown as DummyTool[];
|
||||
|
||||
applyToolPolicyPipeline({
|
||||
tools: tools as any,
|
||||
tools: asPolicyTools(tools),
|
||||
toolMeta: () => undefined,
|
||||
warn: () => {},
|
||||
steps: [
|
||||
@@ -784,7 +789,7 @@ describe("tool-policy-pipeline", () => {
|
||||
const labelPrefix = "a".repeat(159);
|
||||
|
||||
applyToolPolicyPipeline({
|
||||
tools: tools as any,
|
||||
tools: asPolicyTools(tools),
|
||||
toolMeta: () => undefined,
|
||||
warn: () => {},
|
||||
steps: [
|
||||
|
||||
@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
import type { Insertable } from "kysely";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
executeSqliteQuerySync,
|
||||
@@ -609,7 +610,7 @@ describe("channel ingress queue", () => {
|
||||
claim_owner: overrides.claim_owner ?? null,
|
||||
claimed_at: overrides.claimed_at ?? null,
|
||||
completed_at: overrides.completed_at ?? null,
|
||||
} as any),
|
||||
} as Insertable<OpenClawStateKyselyDatabase["channel_ingress_events"]>),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
// Setup group access configure tests cover channel setup writes for group access config.
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createWizardPrompter } from "../../../test/helpers/wizard-prompter.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { configureChannelAccessWithAllowlist } from "./setup-group-access-configure.js";
|
||||
import type { ChannelAccessPolicy } from "./setup-group-access.js";
|
||||
|
||||
function createPrompter(params: { confirm: boolean; policy?: ChannelAccessPolicy; text?: string }) {
|
||||
const confirm = vi.fn(async () => params.confirm);
|
||||
const text = vi.fn(async () => params.text ?? "");
|
||||
const note = vi.fn();
|
||||
const prompter = createWizardPrompter(
|
||||
{ confirm, text, note },
|
||||
{ defaultSelect: params.policy ?? "allowlist" },
|
||||
);
|
||||
return {
|
||||
confirm: vi.fn(async () => params.confirm),
|
||||
select: vi.fn(async () => params.policy ?? "allowlist"),
|
||||
text: vi.fn(async () => params.text ?? ""),
|
||||
note: vi.fn(),
|
||||
...prompter,
|
||||
confirm,
|
||||
select: vi.mocked(prompter.select),
|
||||
text,
|
||||
note,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,7 +33,7 @@ async function runConfigureChannelAccess<TResolved>(params: {
|
||||
}) {
|
||||
return await configureChannelAccessWithAllowlist({
|
||||
cfg: params.cfg,
|
||||
prompter: params.prompter as any,
|
||||
prompter: params.prompter,
|
||||
label: params.label ?? "Slack channels",
|
||||
currentPolicy: "allowlist",
|
||||
currentEntries: [],
|
||||
@@ -106,7 +115,7 @@ describe("configureChannelAccessWithAllowlist", () => {
|
||||
|
||||
const next = await configureChannelAccessWithAllowlist({
|
||||
cfg,
|
||||
prompter: prompter as any,
|
||||
prompter,
|
||||
label: "Twitch chat",
|
||||
currentPolicy: "disabled",
|
||||
currentEntries: [],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Setup group access tests cover group access setup flow decisions and outputs.
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createWizardPrompter } from "../../../test/helpers/wizard-prompter.js";
|
||||
import {
|
||||
formatAllowlistEntries,
|
||||
parseAllowlistEntries,
|
||||
@@ -9,22 +10,27 @@ import {
|
||||
} from "./setup-group-access.js";
|
||||
|
||||
function createPrompter(params?: {
|
||||
confirm?: (options: { message: string; initialValue: boolean }) => Promise<boolean>;
|
||||
select?: (options: {
|
||||
message: string;
|
||||
options: Array<{ value: string; label: string }>;
|
||||
initialValue?: string;
|
||||
}) => Promise<string>;
|
||||
text?: (options: {
|
||||
message: string;
|
||||
placeholder?: string;
|
||||
initialValue?: string;
|
||||
}) => Promise<string>;
|
||||
confirm?: boolean;
|
||||
select?: string;
|
||||
text?: string;
|
||||
textError?: string;
|
||||
}) {
|
||||
const confirm = vi.fn(async () => params?.confirm ?? true);
|
||||
const text = vi.fn(async () => {
|
||||
if (params?.textError) {
|
||||
throw new Error(params.textError);
|
||||
}
|
||||
return params?.text ?? "";
|
||||
});
|
||||
const prompter = createWizardPrompter(
|
||||
{ confirm, text },
|
||||
{ defaultSelect: params?.select ?? "allowlist" },
|
||||
);
|
||||
return {
|
||||
confirm: vi.fn(params?.confirm ?? (async () => true)),
|
||||
select: vi.fn(params?.select ?? (async () => "allowlist")),
|
||||
text: vi.fn(params?.text ?? (async () => "")),
|
||||
...prompter,
|
||||
confirm,
|
||||
select: vi.mocked(prompter.select),
|
||||
text,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -48,11 +54,11 @@ describe("formatAllowlistEntries", () => {
|
||||
describe("promptChannelAllowlist", () => {
|
||||
it("uses existing entries as initial value", async () => {
|
||||
const prompter = createPrompter({
|
||||
text: async () => "one,two",
|
||||
text: "one,two",
|
||||
});
|
||||
|
||||
const result = await promptChannelAllowlist({
|
||||
prompter: prompter as any,
|
||||
prompter,
|
||||
label: "Test",
|
||||
currentEntries: ["alpha", "beta"],
|
||||
});
|
||||
@@ -69,11 +75,11 @@ describe("promptChannelAllowlist", () => {
|
||||
describe("promptChannelAccessPolicy", () => {
|
||||
it("returns selected policy", async () => {
|
||||
const prompter = createPrompter({
|
||||
select: async () => "open",
|
||||
select: "open",
|
||||
});
|
||||
|
||||
const result = await promptChannelAccessPolicy({
|
||||
prompter: prompter as any,
|
||||
prompter,
|
||||
label: "Discord",
|
||||
currentPolicy: "allowlist",
|
||||
});
|
||||
@@ -85,15 +91,13 @@ describe("promptChannelAccessPolicy", () => {
|
||||
describe("promptChannelAccessConfig policy-only entries", () => {
|
||||
it("skips the allowlist text prompt when entries are policy-only", async () => {
|
||||
const prompter = createPrompter({
|
||||
confirm: async () => true,
|
||||
select: async () => "allowlist",
|
||||
text: async () => {
|
||||
throw new Error("text prompt should not run");
|
||||
},
|
||||
confirm: true,
|
||||
select: "allowlist",
|
||||
textError: "text prompt should not run",
|
||||
});
|
||||
|
||||
const result = await promptChannelAccessConfig({
|
||||
prompter: prompter as any,
|
||||
prompter,
|
||||
label: "Twitch chat",
|
||||
skipAllowlistEntries: true,
|
||||
});
|
||||
@@ -105,11 +109,11 @@ describe("promptChannelAccessConfig policy-only entries", () => {
|
||||
describe("promptChannelAccessConfig skip flow", () => {
|
||||
it("returns null when user skips configuration", async () => {
|
||||
const prompter = createPrompter({
|
||||
confirm: async () => false,
|
||||
confirm: false,
|
||||
});
|
||||
|
||||
const result = await promptChannelAccessConfig({
|
||||
prompter: prompter as any,
|
||||
prompter,
|
||||
label: "Slack",
|
||||
});
|
||||
|
||||
@@ -118,13 +122,13 @@ describe("promptChannelAccessConfig skip flow", () => {
|
||||
|
||||
it("returns allowlist entries when policy is allowlist", async () => {
|
||||
const prompter = createPrompter({
|
||||
confirm: async () => true,
|
||||
select: async () => "allowlist",
|
||||
text: async () => "c1, c2",
|
||||
confirm: true,
|
||||
select: "allowlist",
|
||||
text: "c1, c2",
|
||||
});
|
||||
|
||||
const result = await promptChannelAccessConfig({
|
||||
prompter: prompter as any,
|
||||
prompter,
|
||||
label: "Slack",
|
||||
});
|
||||
|
||||
@@ -136,12 +140,12 @@ describe("promptChannelAccessConfig skip flow", () => {
|
||||
|
||||
it("returns non-allowlist policy with empty entries", async () => {
|
||||
const prompter = createPrompter({
|
||||
confirm: async () => true,
|
||||
select: async () => "open",
|
||||
confirm: true,
|
||||
select: "open",
|
||||
});
|
||||
|
||||
const result = await promptChannelAccessConfig({
|
||||
prompter: prompter as any,
|
||||
prompter,
|
||||
label: "Slack",
|
||||
allowDisabled: true,
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
resolveSetupWizardGroupAllowlist,
|
||||
} from "openclaw/plugin-sdk/plugin-test-runtime";
|
||||
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createWizardPrompter } from "../../../test/helpers/wizard-prompter.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
|
||||
@@ -149,9 +150,12 @@ afterAll(() => {
|
||||
});
|
||||
|
||||
function createPrompter(inputs: string[]) {
|
||||
const text = vi.fn(async () => inputs.shift() ?? "");
|
||||
const note = vi.fn(async () => undefined);
|
||||
return {
|
||||
text: vi.fn(async () => inputs.shift() ?? ""),
|
||||
note: vi.fn(async () => undefined),
|
||||
...createWizardPrompter(),
|
||||
text,
|
||||
note,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -181,7 +185,6 @@ type AllowFromResolver = (params: {
|
||||
token: string;
|
||||
entries: string[];
|
||||
}) => Promise<Array<{ input: string; resolved: boolean; id?: string | null }>>;
|
||||
|
||||
function asAllowFromResolver(resolveEntries: ReturnType<typeof vi.fn>): AllowFromResolver {
|
||||
return resolveEntries as AllowFromResolver;
|
||||
}
|
||||
@@ -191,7 +194,7 @@ async function runPromptResolvedAllowFromWithToken(params: {
|
||||
resolveEntries: AllowFromResolver;
|
||||
}) {
|
||||
return await promptResolvedAllowFrom({
|
||||
prompter: params.prompter as any,
|
||||
prompter: params.prompter,
|
||||
existing: [],
|
||||
token: "xoxb-test",
|
||||
message: "msg",
|
||||
@@ -229,11 +232,19 @@ function createSecretInputPrompter(params: {
|
||||
const selects = [...params.selects];
|
||||
const confirms = [...(params.confirms ?? [])];
|
||||
const texts = [...(params.texts ?? [])];
|
||||
const confirm = vi.fn(async () => confirms.shift() ?? false);
|
||||
const text = vi.fn(async () => texts.shift() ?? "");
|
||||
const note = vi.fn(async () => undefined);
|
||||
const prompter = createWizardPrompter(undefined, {
|
||||
defaultSelect: "plaintext",
|
||||
selectValues: selects,
|
||||
});
|
||||
return {
|
||||
select: vi.fn(async () => selects.shift() ?? "plaintext"),
|
||||
confirm: vi.fn(async () => confirms.shift() ?? false),
|
||||
text: vi.fn(async () => texts.shift() ?? ""),
|
||||
note: vi.fn(async () => undefined),
|
||||
...prompter,
|
||||
select: vi.mocked(prompter.select),
|
||||
confirm,
|
||||
text,
|
||||
note,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -248,7 +259,7 @@ async function runPromptSingleChannelSecretInput(params: {
|
||||
}) {
|
||||
return await promptSingleChannelSecretInput({
|
||||
cfg: {},
|
||||
prompter: params.prompter as any,
|
||||
prompter: params.prompter,
|
||||
providerHint: params.providerHint,
|
||||
credentialLabel: params.credentialLabel,
|
||||
accountConfigured: params.accountConfigured,
|
||||
@@ -310,7 +321,7 @@ async function runPromptLegacyAllowFrom(params: {
|
||||
return await promptLegacyChannelAllowFrom({
|
||||
cfg: params.cfg ?? {},
|
||||
channel: params.channel,
|
||||
prompter: params.prompter as any,
|
||||
prompter: params.prompter,
|
||||
existing: params.existing,
|
||||
token: params.token,
|
||||
noteTitle: params.noteTitle,
|
||||
@@ -329,7 +340,7 @@ describe("promptResolvedAllowFrom", () => {
|
||||
const resolveEntries = vi.fn();
|
||||
|
||||
const result = await promptResolvedAllowFrom({
|
||||
prompter: prompter as any,
|
||||
prompter,
|
||||
existing: ["111"],
|
||||
token: "",
|
||||
message: "msg",
|
||||
@@ -338,7 +349,9 @@ describe("promptResolvedAllowFrom", () => {
|
||||
parseInputs: parseCsvInputs,
|
||||
parseId: (value) => (/^\d+$/.test(value.trim()) ? value.trim() : null),
|
||||
invalidWithoutTokenNote: "ids only",
|
||||
resolveEntries: resolveEntries as any,
|
||||
resolveEntries: resolveEntries as Parameters<
|
||||
typeof promptResolvedAllowFrom
|
||||
>[0]["resolveEntries"],
|
||||
});
|
||||
|
||||
expect(result).toEqual(["111", "123"]);
|
||||
@@ -442,7 +455,7 @@ describe("promptLegacyChannelAllowFromForAccount", () => {
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
channel: "slack",
|
||||
prompter: prompter as any,
|
||||
prompter,
|
||||
defaultAccountId: DEFAULT_ACCOUNT_ID,
|
||||
resolveAccount: () => ({
|
||||
botToken: "xoxb-token",
|
||||
@@ -810,7 +823,7 @@ describe("createPromptParsedAllowFromForAccount", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
prompter: prompter as any,
|
||||
prompter,
|
||||
});
|
||||
|
||||
expect(
|
||||
@@ -839,7 +852,7 @@ describe("parsed allowFrom prompt builders", () => {
|
||||
const prompter = createPrompter(["npub1"]);
|
||||
const next = await promptAllowFrom({
|
||||
cfg: {},
|
||||
prompter: prompter as any,
|
||||
prompter,
|
||||
});
|
||||
|
||||
expect(next.channels?.nostr?.allowFrom).toEqual(["npub1"]);
|
||||
@@ -859,7 +872,7 @@ describe("parsed allowFrom prompt builders", () => {
|
||||
|
||||
const next = await promptAllowFrom({
|
||||
cfg: {},
|
||||
prompter: createPrompter(["users/123"]) as any,
|
||||
prompter: createPrompter(["users/123"]),
|
||||
});
|
||||
|
||||
expect(next.channels?.googlechat?.enabled).toBe(true);
|
||||
@@ -2060,7 +2073,7 @@ describe("resolveAccountIdForConfigure", () => {
|
||||
it("uses normalized override without prompting", async () => {
|
||||
const accountId = await resolveAccountIdForConfigure({
|
||||
cfg: {},
|
||||
prompter: {} as any,
|
||||
prompter: createWizardPrompter(),
|
||||
label: "Signal",
|
||||
accountOverride: " Team Primary ",
|
||||
shouldPromptAccountIds: true,
|
||||
@@ -2073,7 +2086,7 @@ describe("resolveAccountIdForConfigure", () => {
|
||||
it("uses default account when override is missing and prompting disabled", async () => {
|
||||
const accountId = await resolveAccountIdForConfigure({
|
||||
cfg: {},
|
||||
prompter: {} as any,
|
||||
prompter: createWizardPrompter(),
|
||||
label: "Signal",
|
||||
shouldPromptAccountIds: false,
|
||||
listAccountIds: () => ["default"],
|
||||
@@ -2083,15 +2096,15 @@ describe("resolveAccountIdForConfigure", () => {
|
||||
});
|
||||
|
||||
it("prompts for account id when prompting is enabled and no override is provided", async () => {
|
||||
const basePrompter = createWizardPrompter(undefined, { defaultSelect: "prompted-id" });
|
||||
const prompter = {
|
||||
select: vi.fn(async () => "prompted-id"),
|
||||
text: vi.fn(async () => ""),
|
||||
note: vi.fn(async () => undefined),
|
||||
...basePrompter,
|
||||
select: vi.mocked(basePrompter.select),
|
||||
};
|
||||
|
||||
const accountId = await resolveAccountIdForConfigure({
|
||||
cfg: {},
|
||||
prompter: prompter as any,
|
||||
prompter,
|
||||
label: "Signal",
|
||||
shouldPromptAccountIds: true,
|
||||
listAccountIds: () => ["default", "prompted-id"],
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// Doctor repair flow tests cover repair plan output and repair execution.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, expectTypeOf, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { runDoctorHealthRepairs } from "./doctor-repair-flow.js";
|
||||
import { defineSplitHealthCheck, normalizeHealthCheck } from "./health-check-adapter.js";
|
||||
import type { RunnableHealthCheck } from "./health-check-runner-types.js";
|
||||
import type { RunnableHealthCheck, SplitHealthCheckInput } from "./health-check-runner-types.js";
|
||||
import type { HealthCheck, HealthRepairContext } from "./health-checks.js";
|
||||
|
||||
function ctx(cfg: OpenClawConfig): HealthRepairContext {
|
||||
@@ -107,29 +107,11 @@ describe("runDoctorHealthRepairs", () => {
|
||||
});
|
||||
|
||||
it("keeps repairable out of split repair result types", () => {
|
||||
const check = defineSplitHealthCheck({
|
||||
id: "test/repair-result-status-boundary",
|
||||
kind: "core",
|
||||
description: "repair result status boundary",
|
||||
async detect() {
|
||||
return [
|
||||
{
|
||||
checkId: "test/repair-result-status-boundary",
|
||||
severity: "warning",
|
||||
message: "needs repair",
|
||||
},
|
||||
];
|
||||
},
|
||||
// @ts-expect-error repairable is a run-result preview status, not a split repair result.
|
||||
async repair() {
|
||||
return {
|
||||
status: "repairable",
|
||||
changes: [],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
expect(check.id).toBe("test/repair-result-status-boundary");
|
||||
type SplitRepair = NonNullable<SplitHealthCheckInput["repair"]>;
|
||||
expectTypeOf(async () => ({
|
||||
status: "repairable" as const,
|
||||
changes: [],
|
||||
})).not.toMatchTypeOf<SplitRepair>();
|
||||
});
|
||||
|
||||
it("leaves non-repairable checks for legacy doctor behavior", async () => {
|
||||
|
||||
@@ -1830,7 +1830,7 @@ describe("OpenAI-compatible HTTP API (e2e)", () => {
|
||||
mode: "token",
|
||||
token: "secret",
|
||||
rateLimit: { maxAttempts: 1, windowMs: 60_000, lockoutMs: 60_000, exemptLoopback: false },
|
||||
} as any;
|
||||
};
|
||||
await withGatewayServer(
|
||||
async ({ port }) => {
|
||||
const headers = {
|
||||
|
||||
@@ -2556,12 +2556,12 @@ describe("timestampOptsFromConfig", () => {
|
||||
it.each([
|
||||
{
|
||||
name: "extracts timezone from config",
|
||||
cfg: { agents: { defaults: { userTimezone: "America/Chicago" } } } as any,
|
||||
cfg: { agents: { defaults: { userTimezone: "America/Chicago" } } } as OpenClawConfig,
|
||||
expected: "America/Chicago",
|
||||
},
|
||||
{
|
||||
name: "falls back gracefully with empty config",
|
||||
cfg: {} as any,
|
||||
cfg: {} as OpenClawConfig,
|
||||
expected: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
|
||||
},
|
||||
])("$name", ({ cfg, expected }) => {
|
||||
|
||||
@@ -1007,7 +1007,7 @@ describe("sanitizeHostExecEnv", () => {
|
||||
baseEnv: {
|
||||
PATH: "/usr/bin:/bin",
|
||||
GOOD: "1",
|
||||
BAD_NUMBER: 1 as any,
|
||||
BAD_NUMBER: 1 as unknown as string,
|
||||
"NOT-PORTABLE": "x",
|
||||
"ProgramFiles(x86)": "C:\\Program Files (x86)",
|
||||
},
|
||||
|
||||
@@ -25,7 +25,13 @@ describe("path prepend helpers", () => {
|
||||
|
||||
it("normalizes prepend lists by trimming, skipping blanks, and deduping", () => {
|
||||
expect(
|
||||
normalizePathPrepend([" /custom/bin ", "", " /custom/bin ", "/opt/bin", 42 as any]),
|
||||
normalizePathPrepend([
|
||||
" /custom/bin ",
|
||||
"",
|
||||
" /custom/bin ",
|
||||
"/opt/bin",
|
||||
42 as unknown as string,
|
||||
]),
|
||||
).toEqual(["/custom/bin", "/opt/bin"]);
|
||||
expect(normalizePathPrepend()).toStrictEqual([]);
|
||||
});
|
||||
|
||||
@@ -6,42 +6,42 @@ describe("readSqliteUserVersion", () => {
|
||||
it("returns 0 when row is undefined", () => {
|
||||
const db = {
|
||||
prepare: () => ({ get: () => undefined }),
|
||||
} as any;
|
||||
};
|
||||
expect(readSqliteUserVersion(db)).toBe(0);
|
||||
});
|
||||
|
||||
it("returns 0 when user_version is null", () => {
|
||||
const db = {
|
||||
prepare: () => ({ get: () => ({ user_version: null }) }),
|
||||
} as any;
|
||||
};
|
||||
expect(readSqliteUserVersion(db)).toBe(0);
|
||||
});
|
||||
|
||||
it("returns numeric user_version", () => {
|
||||
const db = {
|
||||
prepare: () => ({ get: () => ({ user_version: 5 }) }),
|
||||
} as any;
|
||||
};
|
||||
expect(readSqliteUserVersion(db)).toBe(5);
|
||||
});
|
||||
|
||||
it("returns 0 when user_version is 0", () => {
|
||||
const db = {
|
||||
prepare: () => ({ get: () => ({ user_version: 0 }) }),
|
||||
} as any;
|
||||
};
|
||||
expect(readSqliteUserVersion(db)).toBe(0);
|
||||
});
|
||||
|
||||
it("converts string user_version to number", () => {
|
||||
const db = {
|
||||
prepare: () => ({ get: () => ({ user_version: "3" }) }),
|
||||
} as any;
|
||||
};
|
||||
expect(readSqliteUserVersion(db)).toBe(3);
|
||||
});
|
||||
|
||||
it("returns 0 for empty object", () => {
|
||||
const db = {
|
||||
prepare: () => ({ get: () => ({}) }),
|
||||
} as any;
|
||||
};
|
||||
expect(readSqliteUserVersion(db)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
type SqliteUserVersionReader = {
|
||||
prepare: (sql: string) => { get: () => unknown };
|
||||
};
|
||||
|
||||
export function readSqliteUserVersion(db: DatabaseSync): number {
|
||||
export function readSqliteUserVersion(db: SqliteUserVersionReader): number {
|
||||
const row = db.prepare("PRAGMA user_version").get() as { user_version?: unknown } | undefined;
|
||||
return Number(row?.user_version ?? 0);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { ConnectErrorDetailCodes } from "../../packages/gateway-protocol/src/connect-error-details.js";
|
||||
import type { GatewayClientOptions } from "../gateway/client.js";
|
||||
import type { ensureNodeHostConfig } from "./config.js";
|
||||
import { startNodeHostMcpManager, type NodeHostMcpManager } from "./mcp.js";
|
||||
import {
|
||||
resolveNodeHostGatewayDeviceFamily,
|
||||
@@ -490,7 +491,7 @@ describe("runNodeHost", () => {
|
||||
version: 1,
|
||||
nodeId: "node-test",
|
||||
gateway: { contextPath: "/old-path" },
|
||||
} as any);
|
||||
} as Awaited<ReturnType<typeof ensureNodeHostConfig>>);
|
||||
|
||||
await expect(
|
||||
runNodeHost({
|
||||
@@ -510,7 +511,7 @@ describe("runNodeHost", () => {
|
||||
version: 1,
|
||||
nodeId: "node-test",
|
||||
gateway: { contextPath: "/old-path" },
|
||||
} as any);
|
||||
} as Awaited<ReturnType<typeof ensureNodeHostConfig>>);
|
||||
|
||||
await expect(
|
||||
runNodeHost({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, expectTypeOf, it } from "vitest";
|
||||
import { loadTranscriptEvents } from "../../config/sessions/session-accessor.js";
|
||||
import { createGatewaySession } from "../../gateway/session-create-service.js";
|
||||
import {
|
||||
@@ -20,21 +20,22 @@ function createDeferred(): { promise: Promise<void>; resolve: () => void } {
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
function assertRecoveryInitializerTypeContract(
|
||||
create: ReturnType<typeof createRuntimeAgent>["session"]["createSessionEntry"],
|
||||
): void {
|
||||
// @ts-expect-error Recovery must return the final trusted plugin extension patch.
|
||||
void create({
|
||||
cfg: {},
|
||||
key: "type-contract-only",
|
||||
recoverMatchingInitialEntry: true,
|
||||
initialEntry: { agentHarnessId: "codex" },
|
||||
afterCreate: async () => {},
|
||||
});
|
||||
}
|
||||
void assertRecoveryInitializerTypeContract;
|
||||
|
||||
describe("plugin runtime session creation", () => {
|
||||
it("requires recovery initialization to return the final trusted patch", () => {
|
||||
type CreateSessionParams = Parameters<
|
||||
ReturnType<typeof createRuntimeAgent>["session"]["createSessionEntry"]
|
||||
>[0];
|
||||
const invalidRecoveryInitializer = {
|
||||
cfg: {},
|
||||
key: "type-contract-only",
|
||||
recoverMatchingInitialEntry: true as const,
|
||||
initialEntry: { agentHarnessId: "codex" },
|
||||
afterCreate: async () => {},
|
||||
};
|
||||
|
||||
expectTypeOf(invalidRecoveryInitializer).not.toMatchTypeOf<CreateSessionParams>();
|
||||
});
|
||||
|
||||
it("creates a canonical transcript with trusted initial session state", async () => {
|
||||
await withOpenClawTestState({ label: "plugin-runtime-session-create" }, async () => {
|
||||
const runtime = createRuntimeAgent();
|
||||
|
||||
@@ -6,7 +6,7 @@ const theme = {
|
||||
dim: (s: string) => `<d>${s}</d>`,
|
||||
bold: (s: string) => `<b>${s}</b>`,
|
||||
accentSoft: (s: string) => `<a>${s}</a>`,
|
||||
} as any;
|
||||
} satisfies Parameters<typeof buildWaitingStatusMessage>[0]["theme"];
|
||||
|
||||
describe("tui-waiting", () => {
|
||||
it("pickWaitingPhrase rotates every 10 ticks", () => {
|
||||
|
||||
@@ -7,10 +7,11 @@ import type { WizardPrompter } from "../../src/wizard/prompts.js";
|
||||
/** Create a WizardPrompter with default mocked responses and optional overrides. */
|
||||
export function createWizardPrompter(
|
||||
overrides?: Partial<WizardPrompter>,
|
||||
options?: { defaultSelect?: string },
|
||||
options?: { defaultSelect?: string; selectValues?: string[] },
|
||||
): WizardPrompter {
|
||||
const selectValues = [...(options?.selectValues ?? [])];
|
||||
const select = vi.fn(
|
||||
async () => options?.defaultSelect ?? "quickstart",
|
||||
async () => selectValues.shift() ?? options?.defaultSelect ?? "quickstart",
|
||||
) as unknown as WizardPrompter["select"];
|
||||
return {
|
||||
intro: vi.fn(async () => {}),
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
// Type Suppression Inventory tests cover AST detection and the repository suppression ratchet.
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { collectTypeSuppressionReport } from "../../scripts/type-suppression-inventory.js";
|
||||
|
||||
const repoRoot = path.resolve(import.meta.dirname, "../..");
|
||||
const temporaryDirectories: string[] = [];
|
||||
|
||||
function createFixture(files: Record<string, string>): string {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-type-suppressions-"));
|
||||
temporaryDirectories.push(root);
|
||||
for (const [relativePath, source] of Object.entries(files)) {
|
||||
const absolutePath = path.join(root, relativePath);
|
||||
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
||||
fs.writeFileSync(absolutePath, source);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const directory of temporaryDirectories.splice(0)) {
|
||||
fs.rmSync(directory, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe("type suppression inventory", () => {
|
||||
it("detects syntax suppressions without counting prose", () => {
|
||||
const fixtureRoot = createFixture({
|
||||
"src/example.ts": `
|
||||
const prose = "as any and @ts-expect-error";
|
||||
const first = value as any;
|
||||
const second = <any>value;
|
||||
// @ts-expect-error invalid contract fixture
|
||||
consume({ invalid: true });
|
||||
`,
|
||||
});
|
||||
|
||||
const report = collectTypeSuppressionReport({
|
||||
files: ["src/example.ts"],
|
||||
repoRoot: fixtureRoot,
|
||||
});
|
||||
|
||||
expect(report.summary).toMatchObject({
|
||||
findingCount: 3,
|
||||
kindCounts: {
|
||||
"as-any": 1,
|
||||
"expect-error": 1,
|
||||
"type-assertion-any": 1,
|
||||
},
|
||||
touchedFileCount: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps unchecked any casts at zero and negative type assertions explicit", () => {
|
||||
const report = collectTypeSuppressionReport({ repoRoot });
|
||||
|
||||
expect(report.summary.kindCounts["as-any"]).toBe(0);
|
||||
expect(report.summary.kindCounts["type-assertion-any"]).toBe(0);
|
||||
expect(
|
||||
report.findings
|
||||
.filter((finding) => finding.kind === "expect-error")
|
||||
.map((finding) => `${finding.file}:${finding.line}:${finding.excerpt}`),
|
||||
).toEqual([
|
||||
"src/infra/kysely-sync.types.test.ts:49:@ts-expect-error Kysely checks selected column string literals.",
|
||||
"src/infra/kysely-sync.types.test.ts:52:@ts-expect-error Kysely checks table string literals.",
|
||||
"src/infra/kysely-sync.types.test.ts:55:@ts-expect-error Kysely checks where-reference string literals.",
|
||||
"src/infra/kysely-sync.types.test.ts:58:@ts-expect-error Kysely checks grouped column string literals.",
|
||||
"src/infra/kysely-sync.types.test.ts:61:@ts-expect-error Kysely checks order references and selected aliases.",
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user