diff --git a/extensions/browser/src/browser/server-context.test-harness.ts b/extensions/browser/src/browser/server-context.test-harness.ts index f891826cd5f8..ac0053de8f66 100644 --- a/extensions/browser/src/browser/server-context.test-harness.ts +++ b/extensions/browser/src/browser/server-context.test-harness.ts @@ -65,7 +65,7 @@ export function makeBrowserServerState(params?: { }, }; return { - server: null as any, + server: null as unknown as BrowserServerState["server"], port: 0, resolved: { ...resolvedBase, diff --git a/extensions/copilot/harness.test.ts b/extensions/copilot/harness.test.ts index ad032b43210c..1277b3362ea8 100644 --- a/extensions/copilot/harness.test.ts +++ b/extensions/copilot/harness.test.ts @@ -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, ), 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): AgentHarnessAttemptParams { + return value as unknown as AgentHarnessAttemptParams; +} + +function asAttemptResult(value: Record): 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 = {}): 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(); + const deferred = createDeferred(); 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, }); diff --git a/extensions/discord/src/internal/command-deploy.test.ts b/extensions/discord/src/internal/command-deploy.test.ts index fe9292f89af5..f5ec636d43c1 100644 --- a/extensions/discord/src/internal/command-deploy.test.ts +++ b/extensions/discord/src/internal/command-deploy.test.ts @@ -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({ diff --git a/extensions/discord/src/targets.test.ts b/extensions/discord/src/targets.test.ts index 69a4bd3af879..2dc414aa2933 100644 --- a/extensions/discord/src/targets.test.ts +++ b/extensions/discord/src/targets.test.ts @@ -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({ diff --git a/extensions/googlechat/src/setup.test.ts b/extensions/googlechat/src/setup.test.ts index ab06d28f6a4a..636f7c1d3342 100644 --- a/extensions/googlechat/src/setup.test.ts +++ b/extensions/googlechat/src/setup.test.ts @@ -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"]); diff --git a/extensions/googlechat/src/targets.test.ts b/extensions/googlechat/src/targets.test.ts index 879da19788ac..62df514bae6a 100644 --- a/extensions/googlechat/src/targets.test.ts +++ b/extensions/googlechat/src/targets.test.ts @@ -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); diff --git a/extensions/imessage/src/targets.test.ts b/extensions/imessage/src/targets.test.ts index 5a7c4f7183ca..d584c77a2c36 100644 --- a/extensions/imessage/src/targets.test.ts +++ b/extensions/imessage/src/targets.test.ts @@ -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); diff --git a/extensions/line/src/channel.logout.test.ts b/extensions/line/src/channel.logout.test.ts index e9cd89b805a1..c7b1901c3623 100644 --- a/extensions/line/src/channel.logout.test.ts +++ b/extensions/line/src/channel.logout.test.ts @@ -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" }; }, ); diff --git a/extensions/line/src/group-keys.test.ts b/extensions/line/src/group-keys.test.ts index 8bb9eb359e4c..6e0126a61f91 100644 --- a/extensions/line/src/group-keys.test.ts +++ b/extensions/line/src/group-keys.test.ts @@ -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); }); diff --git a/extensions/line/src/webhook-node.test.ts b/extensions/line/src/webhook-node.test.ts index 2935f54d4449..017092168c80 100644 --- a/extensions/line/src/webhook-node.test.ts +++ b/extensions/line/src/webhook-node.test.ts @@ -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(); + const json = vi.fn(); 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 & { 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" }); diff --git a/extensions/llm-task/src/llm-task-tool.test.ts b/extensions/llm-task/src/llm-task-tool.test.ts index 4fc05da50ce2..43276a73810e 100644 --- a/extensions/llm-task/src/llm-task-tool.test.ts +++ b/extensions/llm-task/src/llm-task-tool.test.ts @@ -16,8 +16,11 @@ afterAll(() => { import { createLlmTaskTool } from "./llm-task-tool.js"; -const runEmbeddedAgent = vi.fn(async () => ({ - meta: { startedAt: Date.now() }, +type LlmTaskApi = Parameters[0]; +type RunEmbeddedAgent = LlmTaskApi["runtime"]["agent"]["runEmbeddedAgent"]; + +const runEmbeddedAgent = vi.fn(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 = {}): 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) { 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"); diff --git a/extensions/lobster/src/lobster-tool.test.ts b/extensions/lobster/src/lobster-tool.test.ts index 0436fb72ca89..d30c710edb9d 100644 --- a/extensions/lobster/src/lobster-tool.test.ts +++ b/extensions/lobster/src/lobster-tool.test.ts @@ -10,7 +10,7 @@ function fakeApi(overrides: Partial = {}): OpenClawPluginApi id: "lobster", name: "lobster", source: "test", - runtime: { version: "test" } as any, + runtime: { version: "test" } as OpenClawPluginApi["runtime"], resolvePath: (p) => p, ...overrides, }); diff --git a/extensions/memory-core/src/memory-tool-manager.test-mocks.ts b/extensions/memory-core/src/memory-tool-manager.test-mocks.ts index 77ac340ba380..1928f7d585b6 100644 --- a/extensions/memory-core/src/memory-tool-manager.test-mocks.ts +++ b/extensions/memory-core/src/memory-tool-manager.test-mocks.ts @@ -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>["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 = 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; diff --git a/extensions/memory-core/src/memory/manager-sync-ops.startup-catchup.test.ts b/extensions/memory-core/src/memory/manager-sync-ops.startup-catchup.test.ts index cfa0627703a3..702fc7f0e3c2 100644 --- a/extensions/memory-core/src/memory/manager-sync-ops.startup-catchup.test.ts +++ b/extensions/memory-core/src/memory/manager-sync-ops.startup-catchup.test.ts @@ -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; + 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, diff --git a/extensions/memory-core/src/tools.test.ts b/extensions/memory-core/src/tools.test.ts index 274b01b17e25..8769e13d12ad 100644 --- a/extensions/memory-core/src/tools.test.ts +++ b/extensions/memory-core/src/tools.test.ts @@ -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", diff --git a/extensions/memory-lancedb/index.test.ts b/extensions/memory-lancedb/index.test.ts index 3a5c6d689683..6ad04cd8f735 100644 --- a/extensions/memory-lancedb/index.test.ts +++ b/extensions/memory-lancedb/index.test.ts @@ -87,6 +87,10 @@ function createRuntimeLoader( type MockCallSource = { mock: { calls: Array> } }; +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"); diff --git a/extensions/memory-lancedb/memory-lancedb.live.test.ts b/extensions/memory-lancedb/memory-lancedb.live.test.ts index 86d2758fe00a..e20bc1ee34b1 100644 --- a/extensions/memory-lancedb/memory-lancedb.live.test.ts +++ b/extensions/memory-lancedb/memory-lancedb.live.test.ts @@ -62,7 +62,7 @@ describeLive("memory plugin live tests", () => { }; // Register plugin - memoryPlugin.register(mockApi as any); + memoryPlugin.register(mockApi as unknown as Parameters[0]); // Check registration expect(registeredTools.length).toBe(3); diff --git a/extensions/msteams/src/token.test.ts b/extensions/msteams/src/token.test.ts index efde22498550..cff5f2455753 100644 --- a/extensions/msteams/src/token.test.ts +++ b/extensions/msteams/src/token.test.ts @@ -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", diff --git a/extensions/opencode-go/stream-termination.test.ts b/extensions/opencode-go/stream-termination.test.ts index bba5c55954f4..feb2c74e5dbd 100644 --- a/extensions/opencode-go/stream-termination.test.ts +++ b/extensions/opencode-go/stream-termination.test.ts @@ -9,6 +9,19 @@ import { createOpencodeGoStalledStreamWrapper } from "./stream-termination.js"; type AnyEvent = AssistantMessageEvent; type StreamLike = AssistantMessageEventStreamContract; +type ProviderStreamFn = Parameters[0]; +type ProviderModel = Parameters[0]; +type ProviderContext = Parameters[1]; +type ProviderCallOptions = Parameters[2]; +type ErrorEvent = Extract; + +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); diff --git a/extensions/signal/src/client-adapter.test.ts b/extensions/signal/src/client-adapter.test.ts index 41355b0b11df..f4a67b8a5717 100644 --- a/extensions/signal/src/client-adapter.test.ts +++ b/extensions/signal/src/client-adapter.test.ts @@ -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(); +const mockNativeRpcRequest = vi.fn(); +const mockNativeStreamEvents = vi.fn(); +const mockContainerCheck = vi.fn(); +const mockContainerRpcRequest = vi.fn(); +const mockContainerFetchAttachment = vi.fn(); +const mockStreamContainerEvents = vi.fn(); 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, ); }); diff --git a/extensions/signal/src/monitor/event-handler.inbound-context.test.ts b/extensions/signal/src/monitor/event-handler.inbound-context.test.ts index beff92b29f23..34fea6ae5d86 100644 --- a/extensions/signal/src/monitor/event-handler.inbound-context.test.ts +++ b/extensions/signal/src/monitor/event-handler.inbound-context.test.ts @@ -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", diff --git a/extensions/signal/src/monitor/event-handler.reply-session-conflict.test.ts b/extensions/signal/src/monitor/event-handler.reply-session-conflict.test.ts index 83b953b56ff8..49a0d40c23fe 100644 --- a/extensions/signal/src/monitor/event-handler.reply-session-conflict.test.ts +++ b/extensions/signal/src/monitor/event-handler.reply-session-conflict.test.ts @@ -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, }), ); diff --git a/extensions/signal/src/monitor/event-handler.test-harness.ts b/extensions/signal/src/monitor/event-handler.test-harness.ts index 7387a8b01712..16888020beed 100644 --- a/extensions/signal/src/monitor/event-handler.test-harness.ts +++ b/extensions/signal/src/monitor/event-handler.test-harness.ts @@ -5,7 +5,7 @@ export function createBaseSignalEventHandlerDeps( overrides: Partial = {}, ): SignalEventHandlerDeps { return { - runtime: { log: () => {}, error: () => {} } as any, + runtime: { log: () => {}, error: () => {} } as SignalEventHandlerDeps["runtime"], cfg: {}, baseUrl: "http://localhost", accountId: "default", diff --git a/extensions/slack/src/group-policy.test.ts b/extensions/slack/src/group-policy.test.ts index 87ed8f00f5e2..ebc0daaebb16 100644 --- a/extensions/slack/src/group-policy.test.ts +++ b/extensions/slack/src/group-policy.test.ts @@ -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", () => { diff --git a/extensions/slack/src/monitor/message-handler/prepare.test.ts b/extensions/slack/src/monitor/message-handler/prepare.test.ts index 5b1efc36ecfa..ea8d9064e172 100644 --- a/extensions/slack/src/monitor/message-handler/prepare.test.ts +++ b/extensions/slack/src/monitor/message-handler/prepare.test.ts @@ -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; } diff --git a/extensions/slack/src/monitor/monitor.thread-resolution.test.ts b/extensions/slack/src/monitor/monitor.thread-resolution.test.ts index 1bc1ddbfd688..f6b774c13e67 100644 --- a/extensions/slack/src/monitor/monitor.thread-resolution.test.ts +++ b/extensions/slack/src/monitor/monitor.thread-resolution.test.ts @@ -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[0]["client"]; + +function createThreadClient(history: ReturnType): 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, }); diff --git a/extensions/telegram/src/bot/helpers.test.ts b/extensions/telegram/src/bot/helpers.test.ts index 54947d1c0329..1f80131b5b52 100644 --- a/extensions/telegram/src/bot/helpers.test.ts +++ b/extensions/telegram/src/bot/helpers.test.ts @@ -19,6 +19,12 @@ import { shouldUseTelegramDmThreadSession, } from "./helpers.js"; +type TelegramMessage = Parameters[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); diff --git a/extensions/telegram/src/targets.test.ts b/extensions/telegram/src/targets.test.ts index 2667b8c4bb30..2f34c51a0e88 100644 --- a/extensions/telegram/src/targets.test.ts +++ b/extensions/telegram/src/targets.test.ts @@ -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({ diff --git a/extensions/twitch/src/twitch-client.test.ts b/extensions/twitch/src/twitch-client.test.ts index 1addc8990262..e2e67e352ca4 100644 --- a/extensions/twitch/src/twitch-client.test.ts +++ b/extensions/twitch/src/twitch-client.test.ts @@ -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; + messageHandlers: Map 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; diff --git a/extensions/whatsapp/src/inbound/message-aliases.test.ts b/extensions/whatsapp/src/inbound/message-aliases.test.ts index 598ea71971a3..d537503f8adc 100644 --- a/extensions/whatsapp/src/inbound/message-aliases.test.ts +++ b/extensions/whatsapp/src/inbound/message-aliases.test.ts @@ -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(); + }); + it("keeps deprecated admission fields typed on monitor callbacks", () => { expectTypeOf().toMatchTypeOf<{ admission: NonNullable; diff --git a/extensions/xai/image-generation-provider.test.ts b/extensions/xai/image-generation-provider.test.ts index 5467047c4c30..9af74b38a20b 100644 --- a/extensions/xai/image-generation-provider.test.ts +++ b/extensions/xai/image-generation-provider.test.ts @@ -2,6 +2,10 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { buildXaiImageGenerationProvider } from "./image-generation-provider.js"; +type GenerateImageParams = Parameters< + ReturnType["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(); }); diff --git a/package.json b/package.json index a8a887a7d23f..7d11ffb592ca 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/type-suppression-inventory.ts b/scripts/type-suppression-inventory.ts new file mode 100644 index 000000000000..874422f8e70b --- /dev/null +++ b/scripts/type-suppression-inventory.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; + 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 = { + "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(); +} diff --git a/src/agents/agent-tools.before-tool-call.e2e.test.ts b/src/agents/agent-tools.before-tool-call.e2e.test.ts index a88642ba0a42..a4284ecd6baa 100644 --- a/src/agents/agent-tools.before-tool-call.e2e.test.ts +++ b/src/agents/agent-tools.before-tool-call.e2e.test.ts @@ -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>; + runBeforeToolCall: ReturnType>; +}; + +function createTestHookRunner(): TestHookRunner { + return { + ...createHookRunner(createEmptyPluginRegistry()), + hasHooks: vi.fn(), + runBeforeToolCall: vi.fn(), + }; +} + +function asAgentTool(tool: { name: string; execute: ReturnType }): AnyAgentTool { + return tool as unknown as AnyAgentTool; +} + afterEach(() => { setGlobalHookRunnerForTest(null); mockGetGlobalHookRunner.mockReset(); - mockGetGlobalHookRunner.mockImplementation( - () => getGlobalHookRunnerForTest() as ReturnType, - ); + mockGetGlobalHookRunner.mockImplementation(() => getGlobalHookRunnerForTest()); }); describe("before_tool_call loop detection behavior", () => { - let hookRunner: { - hasHooks: ReturnType; - runBeforeToolCall: ReturnType; - }; + 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; - runBeforeToolCall: ReturnType; - }; + let hookRunner: TestHookRunner; const mockCallGateway = vi.mocked(callGatewayTool); function requireRecord(value: unknown, label: string): Record { @@ -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; + onResolution?: (decision: PluginApprovalResolution) => void | Promise; }) { 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", diff --git a/src/agents/agent-tools.before-tool-call.integration.e2e.test.ts b/src/agents/agent-tools.before-tool-call.integration.e2e.test.ts index ec5e9b6573d4..e71d25dc5e52 100644 --- a/src/agents/agent-tools.before-tool-call.integration.e2e.test.ts +++ b/src/agents/agent-tools.before-tool-call.integration.e2e.test.ts @@ -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; +function asAgentTool(tool: { + description?: string; + execute: ReturnType; + 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[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[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[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[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[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", diff --git a/src/agents/compaction.token-sanitize.test.ts b/src/agents/compaction.token-sanitize.test.ts index a140b026d478..e487905a503c 100644 --- a/src/agents/compaction.token-sanitize.test.ts +++ b/src/agents/compaction.token-sanitize.test.ts @@ -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", diff --git a/src/agents/embedded-agent-helpers.sanitizeuserfacingtext.test.ts b/src/agents/embedded-agent-helpers.sanitizeuserfacingtext.test.ts index 07342441c695..6238816e3d07 100644 --- a/src/agents/embedded-agent-helpers.sanitizeuserfacingtext.test.ts +++ b/src/agents/embedded-agent-helpers.sanitizeuserfacingtext.test.ts @@ -795,7 +795,9 @@ describe("downgradeOpenAIReasoningBlocks", () => { }, ]; - expect(downgradeOpenAIReasoningBlocks(input as any)).toEqual(input); + expect( + downgradeOpenAIReasoningBlocks(input as Parameters[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[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[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[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[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[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[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[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[0], + ); + const twice = downgradeOpenAIReasoningBlocks( + once as Parameters[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[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[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[0], + ), + ).toEqual([ makePlainAssistantTurn(callIdWithoutReasoning), makeToolResult(callIdWithoutReasoning, "turn1"), makeReasoningAssistantTurn(callIdWithReasoning), diff --git a/src/agents/session-tool-result-guard.tool-result-persist-hook.test.ts b/src/agents/session-tool-result-guard.tool-result-persist-hook.test.ts index e760c8b437e5..1eb8b15d5f90 100644 --- a/src/agents/session-tool-result-guard.tool-result-persist-hook.test.ts +++ b/src/agents/session-tool-result-guard.tool-result-persist-hook.test.ts @@ -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; +type PersistedToolResultMessage = ToolResultMessage & { details: Record }; + 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) 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) { @@ -80,10 +83,18 @@ function getPersistedToolResult(sm: ReturnType) .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) { +function hasRecordDetails(message: ToolResultMessage): message is PersistedToolResultMessage { + return ( + typeof message.details === "object" && + message.details !== null && + !Array.isArray(message.details) + ); +} + +function requirePersistedToolResultMessage(sm: ReturnType) { const toolResult = getPersistedToolResult(sm); if (!toolResult) { throw new Error("expected persisted toolResult message"); @@ -91,6 +102,22 @@ function requirePersistedToolResult(sm: ReturnType) { + 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) { 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) { // Large details are summarized before persistence to keep transcript files bounded. const toolResult = requirePersistedToolResult(sm); - const details = toolResult.details as Record; + 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; + 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; + 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"); diff --git a/src/agents/sessions/tools/edit.test.ts b/src/agents/sessions/tools/edit.test.ts index bf7d4c99864a..ea1eebfb19ab 100644 --- a/src/agents/sessions/tools/edit.test.ts +++ b/src/agents/sessions/tools/edit.test.ts @@ -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"); }); diff --git a/src/agents/sessions/tools/write.test.ts b/src/agents/sessions/tools/write.test.ts index 561e36687b9a..a549df2c7678 100644 --- a/src/agents/sessions/tools/write.test.ts +++ b/src/agents/sessions/tools/write.test.ts @@ -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"); }); diff --git a/src/agents/tool-policy-pipeline.test.ts b/src/agents/tool-policy-pipeline.test.ts index 389ed9656c38..c34ba9a71c4f 100644 --- a/src/agents/tool-policy-pipeline.test.ts +++ b/src/agents/tool-policy-pipeline.test.ts @@ -22,6 +22,11 @@ vi.mock("../logging/subsystem.js", () => ({ })); type DummyTool = { name: string }; +type PolicyTool = Parameters[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: [ diff --git a/src/channels/message/ingress-queue.test.ts b/src/channels/message/ingress-queue.test.ts index d401edbceed5..4e3199c86e86 100644 --- a/src/channels/message/ingress-queue.test.ts +++ b/src/channels/message/ingress-queue.test.ts @@ -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), ); } diff --git a/src/channels/plugins/setup-group-access-configure.test.ts b/src/channels/plugins/setup-group-access-configure.test.ts index 15654c022e64..9e7c3be5b41e 100644 --- a/src/channels/plugins/setup-group-access-configure.test.ts +++ b/src/channels/plugins/setup-group-access-configure.test.ts @@ -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(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: [], diff --git a/src/channels/plugins/setup-group-access.test.ts b/src/channels/plugins/setup-group-access.test.ts index 15b8e65c2d9a..2eb62ee8853c 100644 --- a/src/channels/plugins/setup-group-access.test.ts +++ b/src/channels/plugins/setup-group-access.test.ts @@ -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; - select?: (options: { - message: string; - options: Array<{ value: string; label: string }>; - initialValue?: string; - }) => Promise; - text?: (options: { - message: string; - placeholder?: string; - initialValue?: string; - }) => Promise; + 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, }); diff --git a/src/channels/plugins/setup-wizard-helpers.test.ts b/src/channels/plugins/setup-wizard-helpers.test.ts index b5c05107fc72..40a3e541ed08 100644 --- a/src/channels/plugins/setup-wizard-helpers.test.ts +++ b/src/channels/plugins/setup-wizard-helpers.test.ts @@ -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>; - function asAllowFromResolver(resolveEntries: ReturnType): 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"], diff --git a/src/flows/doctor-repair-flow.test.ts b/src/flows/doctor-repair-flow.test.ts index 2de0cc2d73af..6eaa65921440 100644 --- a/src/flows/doctor-repair-flow.test.ts +++ b/src/flows/doctor-repair-flow.test.ts @@ -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; + expectTypeOf(async () => ({ + status: "repairable" as const, + changes: [], + })).not.toMatchTypeOf(); }); it("leaves non-repairable checks for legacy doctor behavior", async () => { diff --git a/src/gateway/openai-http.test.ts b/src/gateway/openai-http.test.ts index 6de668348877..f1e408f27327 100644 --- a/src/gateway/openai-http.test.ts +++ b/src/gateway/openai-http.test.ts @@ -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 = { diff --git a/src/gateway/server-methods/server-methods.test.ts b/src/gateway/server-methods/server-methods.test.ts index 64575010ef95..486abe71e05e 100644 --- a/src/gateway/server-methods/server-methods.test.ts +++ b/src/gateway/server-methods/server-methods.test.ts @@ -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 }) => { diff --git a/src/infra/host-env-security.test.ts b/src/infra/host-env-security.test.ts index c90b6a2d64c6..c977291b541f 100644 --- a/src/infra/host-env-security.test.ts +++ b/src/infra/host-env-security.test.ts @@ -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)", }, diff --git a/src/infra/path-prepend.test.ts b/src/infra/path-prepend.test.ts index b83d93372bb0..a693cf6a07ed 100644 --- a/src/infra/path-prepend.test.ts +++ b/src/infra/path-prepend.test.ts @@ -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([]); }); diff --git a/src/infra/sqlite-user-version.test.ts b/src/infra/sqlite-user-version.test.ts index acea767c13e0..9538faf419f8 100644 --- a/src/infra/sqlite-user-version.test.ts +++ b/src/infra/sqlite-user-version.test.ts @@ -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); }); }); diff --git a/src/infra/sqlite-user-version.ts b/src/infra/sqlite-user-version.ts index d33e89ee521c..66fc0e12a781 100644 --- a/src/infra/sqlite-user-version.ts +++ b/src/infra/sqlite-user-version.ts @@ -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); } diff --git a/src/node-host/runner.test.ts b/src/node-host/runner.test.ts index d536ecb392cc..f5b197e87cb2 100644 --- a/src/node-host/runner.test.ts +++ b/src/node-host/runner.test.ts @@ -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>); await expect( runNodeHost({ @@ -510,7 +511,7 @@ describe("runNodeHost", () => { version: 1, nodeId: "node-test", gateway: { contextPath: "/old-path" }, - } as any); + } as Awaited>); await expect( runNodeHost({ diff --git a/src/plugins/runtime/runtime-agent.test.ts b/src/plugins/runtime/runtime-agent.test.ts index fe98796b1909..7f8a6a521818 100644 --- a/src/plugins/runtime/runtime-agent.test.ts +++ b/src/plugins/runtime/runtime-agent.test.ts @@ -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; resolve: () => void } { return { promise, resolve }; } -function assertRecoveryInitializerTypeContract( - create: ReturnType["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["session"]["createSessionEntry"] + >[0]; + const invalidRecoveryInitializer = { + cfg: {}, + key: "type-contract-only", + recoverMatchingInitialEntry: true as const, + initialEntry: { agentHarnessId: "codex" }, + afterCreate: async () => {}, + }; + + expectTypeOf(invalidRecoveryInitializer).not.toMatchTypeOf(); + }); + it("creates a canonical transcript with trusted initial session state", async () => { await withOpenClawTestState({ label: "plugin-runtime-session-create" }, async () => { const runtime = createRuntimeAgent(); diff --git a/src/tui/tui-waiting.test.ts b/src/tui/tui-waiting.test.ts index 17c29d185e07..526e41668ed0 100644 --- a/src/tui/tui-waiting.test.ts +++ b/src/tui/tui-waiting.test.ts @@ -6,7 +6,7 @@ const theme = { dim: (s: string) => `${s}`, bold: (s: string) => `${s}`, accentSoft: (s: string) => `${s}`, -} as any; +} satisfies Parameters[0]["theme"]; describe("tui-waiting", () => { it("pickWaitingPhrase rotates every 10 ticks", () => { diff --git a/test/helpers/wizard-prompter.ts b/test/helpers/wizard-prompter.ts index dae6b5dcee98..b2edd6ff9e24 100644 --- a/test/helpers/wizard-prompter.ts +++ b/test/helpers/wizard-prompter.ts @@ -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, - 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 () => {}), diff --git a/test/scripts/type-suppression-inventory.test.ts b/test/scripts/type-suppression-inventory.test.ts new file mode 100644 index 000000000000..f459b6df1122 --- /dev/null +++ b/test/scripts/type-suppression-inventory.test.ts @@ -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 { + 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 = 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.", + ]); + }); +});