From 71e7cf3b23c54fc66c89ccf2457b82dbf341aaed Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 23 Sep 2026 02:05:05 -0700 Subject: [PATCH] refactor: check type contracts without runtime test startup (#155099) * test(plugin-sdk): move type checks into compiler contracts * test: complete compiler contract migration * fix: update keyed-store contract evidence path --- packages/workboard-contract/src/index.test.ts | 24 --------- .../agent-runtime-ingress-contract.test.ts | 41 --------------- src/plugin-sdk/agent-sessions.test.ts | 18 ------- ...re-host-engine-foundation.contract.test.ts | 51 ------------------ .../plugin-state-store-runtime.test.ts | 26 ---------- src/plugins/compat/registry-records.ts | 2 +- .../type-suppression-inventory.test.ts | 6 +-- test/type-contracts/agent-runtime-ingress.ts | 38 ++++++++++++++ test/type-contracts/agent-sessions.ts | 15 ++++++ .../memory-core-host-engine-foundation.ts | 44 ++++++++++++++++ .../plugin-entry-hook-options.ts | 52 ++++++++++--------- .../plugin-state-store-runtime.ts | 26 ++++++++++ .../workboard-board-automation.ts | 24 +++++++++ 13 files changed, 178 insertions(+), 189 deletions(-) delete mode 100644 packages/workboard-contract/src/index.test.ts delete mode 100644 src/plugin-sdk/agent-runtime-ingress-contract.test.ts delete mode 100644 src/plugin-sdk/agent-sessions.test.ts delete mode 100644 src/plugin-sdk/memory-core-host-engine-foundation.contract.test.ts delete mode 100644 src/plugin-sdk/plugin-state-store-runtime.test.ts create mode 100644 test/type-contracts/agent-runtime-ingress.ts create mode 100644 test/type-contracts/agent-sessions.ts create mode 100644 test/type-contracts/memory-core-host-engine-foundation.ts rename src/plugin-sdk/plugin-entry.reply-trigger.test.ts => test/type-contracts/plugin-entry-hook-options.ts (52%) create mode 100644 test/type-contracts/plugin-state-store-runtime.ts create mode 100644 test/type-contracts/workboard-board-automation.ts diff --git a/packages/workboard-contract/src/index.test.ts b/packages/workboard-contract/src/index.test.ts deleted file mode 100644 index a45c551d6ff4..000000000000 --- a/packages/workboard-contract/src/index.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { describe, expect, expectTypeOf, it } from "vitest"; -import type { WorkboardBoardMetadata, WorkboardBoardSummary } from "./index.js"; - -describe("workboard board automation contract", () => { - it("carries the owning automation job reference in metadata and summaries", () => { - const metadata: WorkboardBoardMetadata = { - id: "planning", - automationJobId: "job-categorize-planning", - createdAt: 1, - updatedAt: 1, - }; - const summary: WorkboardBoardSummary = { - id: metadata.id, - automationJobId: metadata.automationJobId, - total: 0, - active: 0, - archived: 0, - byStatus: {}, - }; - - expect(summary.automationJobId).toBe("job-categorize-planning"); - expectTypeOf(summary.automationJobId).toEqualTypeOf(); - }); -}); diff --git a/src/plugin-sdk/agent-runtime-ingress-contract.test.ts b/src/plugin-sdk/agent-runtime-ingress-contract.test.ts deleted file mode 100644 index 2bcee4c00195..000000000000 --- a/src/plugin-sdk/agent-runtime-ingress-contract.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { describe, expect, expectTypeOf, it } from "vitest"; -import type { agentCommandFromIngress } from "./agent-runtime.js"; -import type { PluginRuntime } from "./core.js"; - -type PublicIngressOptions = Parameters[0]; -type PublicExecutionStarted = NonNullable; -type RuntimeIngressExecutionStarted = NonNullable< - Parameters[0]["onExecutionStarted"] ->; -type RuntimeEmbeddedExecutionStarted = NonNullable< - Parameters[0]["onExecutionStarted"] ->; -const optionalRunIdCaller: PublicIngressOptions = { - message: "hello", - sessionKey: "agent:main:plugin-session", - allowModelOverride: false, -}; -const privateRecoveryCorrelationIsHidden: "executionIdentityAdmission" extends keyof PublicIngressOptions - ? false - : true = true; - -describe("public agent ingress correlation contract", () => { - it("keeps runId optional and private execution recovery state unavailable", () => { - expect(optionalRunIdCaller).not.toHaveProperty("runId"); - expect(privateRecoveryCorrelationIsHidden).toBe(true); - }); - - it("accepts ignored synchronous and asynchronous execution-start returns", () => { - const observed: string[] = []; - const synchronous = () => observed.push("started"); - const asynchronous = async () => { - observed.push("started"); - }; - expectTypeOf(synchronous).toExtend(); - expectTypeOf(asynchronous).toExtend(); - expectTypeOf(synchronous).toExtend(); - expectTypeOf(asynchronous).toExtend(); - expectTypeOf(synchronous).toExtend(); - expectTypeOf(asynchronous).toExtend(); - }); -}); diff --git a/src/plugin-sdk/agent-sessions.test.ts b/src/plugin-sdk/agent-sessions.test.ts deleted file mode 100644 index ea2366ee9366..000000000000 --- a/src/plugin-sdk/agent-sessions.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { SessionManager } from "openclaw/plugin-sdk/agent-sessions"; -import { describe, expectTypeOf, it } from "vitest"; - -describe("agent sessions SDK", () => { - it("keeps SessionManager.persist public", () => { - expectTypeOf().toBeFunction(); - }); - - it("keeps tool-result preparation internal", () => { - expectTypeOf().not.toHaveProperty("prepareModelVisibleToolText"); - expectTypeOf().not.toHaveProperty( - "prepareSessionToolResult", - ); - expectTypeOf().not.toHaveProperty( - "setSessionToolTextPreparer", - ); - }); -}); diff --git a/src/plugin-sdk/memory-core-host-engine-foundation.contract.test.ts b/src/plugin-sdk/memory-core-host-engine-foundation.contract.test.ts deleted file mode 100644 index aa19b364b185..000000000000 --- a/src/plugin-sdk/memory-core-host-engine-foundation.contract.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { describe, expectTypeOf, it } from "vitest"; -import { - resolveMemorySearchConfig, - resolveMemorySearchSyncConfig, - type ResolvedMemorySearchConfig, -} from "./memory-core-host-engine-foundation.js"; - -type IsOptional = Pick extends Required> ? false : true; - -describe("memory core host engine foundation contracts", () => { - it("preserves the released resolved-config authoring shapes", () => { - expectTypeOf>().toEqualTypeOf(); - expectTypeOf>().toEqualTypeOf(); - expectTypeOf< - IsOptional - >().toEqualTypeOf(); - expectTypeOf< - IsOptional - >().toEqualTypeOf(); - expectTypeOf< - IsOptional - >().toEqualTypeOf(); - expectTypeOf< - IsOptional - >().toEqualTypeOf(); - expectTypeOf< - ResolvedMemorySearchConfig["sync"]["embeddingBatchTimeoutSeconds"] - >().toEqualTypeOf(); - - const local: ResolvedMemorySearchConfig["local"] = {}; - const vector: ResolvedMemorySearchConfig["store"]["vector"] = { enabled: true }; - expectTypeOf(local).toMatchTypeOf(); - expectTypeOf(vector).toMatchTypeOf(); - }); - - it("preserves the released resolver return contracts", () => { - expectTypeOf< - ReturnType - >().toEqualTypeOf(); - expectTypeOf>().toEqualTypeOf< - ResolvedMemorySearchConfig["sync"] | null - >(); - - expectTypeOf< - NonNullable>["local"]["contextSize"] - >().toEqualTypeOf(); - expectTypeOf< - NonNullable>["embeddingBatchTimeoutSeconds"] - >().toEqualTypeOf(); - }); -}); diff --git a/src/plugin-sdk/plugin-state-store-runtime.test.ts b/src/plugin-sdk/plugin-state-store-runtime.test.ts deleted file mode 100644 index e125ecc8c7be..000000000000 --- a/src/plugin-sdk/plugin-state-store-runtime.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { describe, expectTypeOf, it } from "vitest"; -import type { PluginStateKeyedStore, PluginStateSyncKeyedStore } from "./plugin-state-runtime.js"; -import type { - createPluginStateKeyedStore, - createPluginStateSyncKeyedStore, -} from "./plugin-state-store-runtime.js"; - -describe("plugin state store type contracts", () => { - it("guarantees atomic capabilities from the concrete SDK factory", () => { - expectTypeOf>>().toEqualTypeOf< - Required> - >(); - expectTypeOf< - ReturnType> - >().toEqualTypeOf>>(); - }); - - it("allows general stores without optional capabilities", () => { - expectTypeOf< - Omit, "update" | "deleteIf" | "lookupMany"> - >().toExtend>(); - expectTypeOf< - Omit, "update" | "deleteIf" | "lookupMany"> - >().toExtend>(); - }); -}); diff --git a/src/plugins/compat/registry-records.ts b/src/plugins/compat/registry-records.ts index 1b99940d3c5a..29db4e25972d 100644 --- a/src/plugins/compat/registry-records.ts +++ b/src/plugins/compat/registry-records.ts @@ -121,7 +121,7 @@ export const PLUGIN_COMPAT_RECORDS = [ "src/plugins/compat/registry.test.ts", "src/plugin-state/plugin-state-store.test.ts", "src/plugin-state/plugin-state-store.runtime.test.ts", - "src/plugin-sdk/plugin-state-store-runtime.test.ts", + "test/type-contracts/plugin-state-store-runtime.ts", "src/plugins/loader.runtime-registry.test.ts", ], releaseNote: diff --git a/test/scripts/type-suppression-inventory.test.ts b/test/scripts/type-suppression-inventory.test.ts index 611efa57efd5..4b306ab09ecb 100644 --- a/test/scripts/type-suppression-inventory.test.ts +++ b/test/scripts/type-suppression-inventory.test.ts @@ -133,9 +133,9 @@ describe("type suppression inventory", () => { "src/infra/net/fetch-guard.socks.test.ts:@ts-expect-error Undici's Node TLS intersection rejects its runtime-valid null timeout.", "src/infra/net/fetch-guard.socks.test.ts:@ts-expect-error Undici's Node TLS intersection rejects its runtime-valid null timeout.", "src/infra/net/fetch-guard.socks.test.ts:@ts-expect-error Undici's Node TLS intersection rejects its runtime-valid null timeout.", - "src/plugin-sdk/plugin-entry.reply-trigger.test.ts:@ts-expect-error Trigger eligibility is only supported for before_agent_reply.", - "src/plugin-sdk/plugin-entry.reply-trigger.test.ts:@ts-expect-error An empty trigger list cannot prove that a hook is inactive.", - "src/plugin-sdk/plugin-entry.reply-trigger.test.ts:@ts-expect-error Tool authority is only supported for before_prompt_build.", + "test/type-contracts/plugin-entry-hook-options.ts:@ts-expect-error Trigger eligibility is only supported for before_agent_reply.", + "test/type-contracts/plugin-entry-hook-options.ts:@ts-expect-error An empty trigger list cannot prove that a hook is inactive.", + "test/type-contracts/plugin-entry-hook-options.ts:@ts-expect-error Tool authority is only supported for before_prompt_build.", "src/plugins/registry.diagnostics.test.ts:@ts-expect-error JavaScript plugins may omit the required supplement builder.", "src/plugins/registry.diagnostics.test.ts:@ts-expect-error JavaScript plugins may omit the required hosted-media resolver.", "src/plugins/registry.diagnostics.test.ts:@ts-expect-error Unknown JavaScript hook names must produce a diagnostic.", diff --git a/test/type-contracts/agent-runtime-ingress.ts b/test/type-contracts/agent-runtime-ingress.ts new file mode 100644 index 000000000000..b8a05966cc5a --- /dev/null +++ b/test/type-contracts/agent-runtime-ingress.ts @@ -0,0 +1,38 @@ +import { expectTypeOf } from "vitest"; +import type { agentCommandFromIngress } from "../../src/plugin-sdk/agent-runtime.js"; +import type { PluginRuntime } from "../../src/plugin-sdk/core.js"; + +type PublicIngressOptions = Parameters[0]; +type PublicExecutionStarted = NonNullable; +type RuntimeIngressExecutionStarted = NonNullable< + Parameters[0]["onExecutionStarted"] +>; +type RuntimeEmbeddedExecutionStarted = NonNullable< + Parameters[0]["onExecutionStarted"] +>; +// public agent ingress correlation contract +// keeps runId optional and private execution recovery state unavailable +const optionalRunIdCaller: PublicIngressOptions = { + message: "hello", + sessionKey: "agent:main:plugin-session", + allowModelOverride: false, +}; +const privateRecoveryCorrelationIsHidden: "executionIdentityAdmission" extends keyof PublicIngressOptions + ? false + : true = true; + +void optionalRunIdCaller; +void privateRecoveryCorrelationIsHidden; + +// accepts ignored synchronous and asynchronous execution-start returns +const observed: string[] = []; +const synchronous = () => observed.push("started"); +const asynchronous = async () => { + observed.push("started"); +}; +expectTypeOf(synchronous).toExtend(); +expectTypeOf(asynchronous).toExtend(); +expectTypeOf(synchronous).toExtend(); +expectTypeOf(asynchronous).toExtend(); +expectTypeOf(synchronous).toExtend(); +expectTypeOf(asynchronous).toExtend(); diff --git a/test/type-contracts/agent-sessions.ts b/test/type-contracts/agent-sessions.ts new file mode 100644 index 000000000000..ed67348ac5b5 --- /dev/null +++ b/test/type-contracts/agent-sessions.ts @@ -0,0 +1,15 @@ +import type { SessionManager } from "openclaw/plugin-sdk/agent-sessions"; +import { expectTypeOf } from "vitest"; + +// agent sessions SDK +// keeps SessionManager.persist public +expectTypeOf().toBeFunction(); + +// keeps tool-result preparation internal +expectTypeOf().not.toHaveProperty("prepareModelVisibleToolText"); +expectTypeOf().not.toHaveProperty( + "prepareSessionToolResult", +); +expectTypeOf().not.toHaveProperty( + "setSessionToolTextPreparer", +); diff --git a/test/type-contracts/memory-core-host-engine-foundation.ts b/test/type-contracts/memory-core-host-engine-foundation.ts new file mode 100644 index 000000000000..a5cb3dc4d545 --- /dev/null +++ b/test/type-contracts/memory-core-host-engine-foundation.ts @@ -0,0 +1,44 @@ +import { expectTypeOf } from "vitest"; +import { + resolveMemorySearchConfig, + resolveMemorySearchSyncConfig, + type ResolvedMemorySearchConfig, +} from "../../src/plugin-sdk/memory-core-host-engine-foundation.js"; + +type IsOptional = Pick extends Required> ? false : true; + +// memory core host engine foundation contracts +// preserves the released resolved-config authoring shapes +expectTypeOf>().toEqualTypeOf(); +expectTypeOf>().toEqualTypeOf(); +expectTypeOf>().toEqualTypeOf(); +expectTypeOf< + IsOptional +>().toEqualTypeOf(); +expectTypeOf>().toEqualTypeOf(); +expectTypeOf< + IsOptional +>().toEqualTypeOf(); +expectTypeOf().toEqualTypeOf< + number | undefined +>(); + +const local: ResolvedMemorySearchConfig["local"] = {}; +const vector: ResolvedMemorySearchConfig["store"]["vector"] = { enabled: true }; +expectTypeOf(local).toMatchTypeOf(); +expectTypeOf(vector).toMatchTypeOf(); + +// preserves the released resolver return contracts +expectTypeOf< + ReturnType +>().toEqualTypeOf(); +expectTypeOf>().toEqualTypeOf< + ResolvedMemorySearchConfig["sync"] | null +>(); + +expectTypeOf< + NonNullable>["local"]["contextSize"] +>().toEqualTypeOf(); +expectTypeOf< + NonNullable>["embeddingBatchTimeoutSeconds"] +>().toEqualTypeOf(); diff --git a/src/plugin-sdk/plugin-entry.reply-trigger.test.ts b/test/type-contracts/plugin-entry-hook-options.ts similarity index 52% rename from src/plugin-sdk/plugin-entry.reply-trigger.test.ts rename to test/type-contracts/plugin-entry-hook-options.ts index 99727fabd23b..e01e11a24ce2 100644 --- a/src/plugin-sdk/plugin-entry.reply-trigger.test.ts +++ b/test/type-contracts/plugin-entry-hook-options.ts @@ -1,6 +1,10 @@ -import { describe, expectTypeOf, it } from "vitest"; -import type { OpenClawPluginApi, WorkerMachineOption, WorkerProvider } from "./plugin-entry.js"; -import type { PluginHookAgentTrigger } from "./types.js"; +import { expectTypeOf } from "vitest"; +import type { + OpenClawPluginApi, + WorkerMachineOption, + WorkerProvider, +} from "../../src/plugin-sdk/plugin-entry.js"; +import type { PluginHookAgentTrigger } from "../../src/plugin-sdk/types.js"; function registerScopedReplyHook(api: OpenClawPluginApi): void { api.on("before_agent_reply", async () => undefined, { eligibleTriggers: ["heartbeat", "cron"] }); @@ -32,25 +36,23 @@ function registerAuthorizedPromptHook(api: OpenClawPluginApi): void { void registerScopedReplyHook; void registerAuthorizedPromptHook; -describe("plugin entry hook option contracts", () => { - it("exposes scoped reply and prompt authority options through the public plugin API", () => { - expectTypeOf().toBeFunction(); - expectTypeOf().toEqualTypeOf<"cron" | "heartbeat" | "user">(); - expectTypeOf().toEqualTypeOf<{ - readonly id: string; - readonly label: string; - readonly cpu?: number; - readonly memoryGb?: number; - readonly default?: boolean; - readonly os?: string; - }>(); - expectTypeOf< - Awaited>>[number] - >().toEqualTypeOf<{ - readonly id: string; - readonly label: string; - readonly default?: boolean; - readonly disabledReason?: string; - }>(); - }); -}); +// plugin entry hook option contracts +// exposes scoped reply and prompt authority options through the public plugin API +expectTypeOf().toBeFunction(); +expectTypeOf().toEqualTypeOf<"cron" | "heartbeat" | "user">(); +expectTypeOf().toEqualTypeOf<{ + readonly id: string; + readonly label: string; + readonly cpu?: number; + readonly memoryGb?: number; + readonly default?: boolean; + readonly os?: string; +}>(); +expectTypeOf< + Awaited>>[number] +>().toEqualTypeOf<{ + readonly id: string; + readonly label: string; + readonly default?: boolean; + readonly disabledReason?: string; +}>(); diff --git a/test/type-contracts/plugin-state-store-runtime.ts b/test/type-contracts/plugin-state-store-runtime.ts new file mode 100644 index 000000000000..6c07b7c142f7 --- /dev/null +++ b/test/type-contracts/plugin-state-store-runtime.ts @@ -0,0 +1,26 @@ +import { expectTypeOf } from "vitest"; +import type { + PluginStateKeyedStore, + PluginStateSyncKeyedStore, +} from "../../src/plugin-sdk/plugin-state-runtime.js"; +import type { + createPluginStateKeyedStore, + createPluginStateSyncKeyedStore, +} from "../../src/plugin-sdk/plugin-state-store-runtime.js"; + +// plugin state store type contracts +// guarantees atomic capabilities from the concrete SDK factory +expectTypeOf>>().toEqualTypeOf< + Required> +>(); +expectTypeOf>>().toEqualTypeOf< + Required> +>(); + +// allows general stores without optional capabilities +expectTypeOf< + Omit, "update" | "deleteIf" | "lookupMany"> +>().toExtend>(); +expectTypeOf< + Omit, "update" | "deleteIf" | "lookupMany"> +>().toExtend>(); diff --git a/test/type-contracts/workboard-board-automation.ts b/test/type-contracts/workboard-board-automation.ts new file mode 100644 index 000000000000..fa0be05b5573 --- /dev/null +++ b/test/type-contracts/workboard-board-automation.ts @@ -0,0 +1,24 @@ +import { expectTypeOf } from "vitest"; +import type { + WorkboardBoardMetadata, + WorkboardBoardSummary, +} from "../../packages/workboard-contract/src/index.js"; + +// workboard board automation contract +// carries the owning automation job reference in metadata and summaries +const metadata: WorkboardBoardMetadata = { + id: "planning", + automationJobId: "job-categorize-planning", + createdAt: 1, + updatedAt: 1, +}; +const summary: WorkboardBoardSummary = { + id: metadata.id, + automationJobId: metadata.automationJobId, + total: 0, + active: 0, + archived: 0, + byStatus: {}, +}; + +expectTypeOf(summary.automationJobId).toEqualTypeOf();