From be5558cb4e258668d0f8ec02ce97adb335f6fed4 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 13 Jul 2026 21:21:09 -0700 Subject: [PATCH] fix(plugin-sdk): preserve runtime contexts in test mocks (#107059) * test(plugin-sdk): make runtime context mock stateful * docs(agents): record Testbox invocation contracts --- AGENTS.md | 2 ++ .../test-helpers/plugin-runtime-mock.test.ts | 26 +++++++++++++++++++ .../test-helpers/plugin-runtime-mock.ts | 18 +++++++------ 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d7efc9a93f0e..8cb4180e4921 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -172,6 +172,8 @@ Skills own workflows; root owns hard policy and routing. - PR refs: `gh pr view/diff` or `gh api`, not web search. Prefer `gitcrawl` for maintainer discovery; missing/stale `gitcrawl` falls through to live `gh`, not contributor setup. Verify live with `gh` before mutation. - `gh pr view` takes the branch positionally; no `--head` flag. - zsh: quote `gh api` endpoints containing `?` or brackets; otherwise glob expansion corrupts the invocation. +- Blacksmith Testbox status/stop: `--id `; no status JSON flag. +- Crabbox final timing JSON = proof complete; if portal sync hangs after it, interrupt wrapper only. - GitHub Actions: resolve workflow files from `.github/workflows` or API; never infer filenames from display names. - zsh: quote command globs; unmatched patterns abort before the tool runs. - zsh: don't use `path` as a variable; it rewrites `$PATH`. diff --git a/src/plugin-sdk/test-helpers/plugin-runtime-mock.test.ts b/src/plugin-sdk/test-helpers/plugin-runtime-mock.test.ts index da0d04eb654f..7b7930564cdc 100644 --- a/src/plugin-sdk/test-helpers/plugin-runtime-mock.test.ts +++ b/src/plugin-sdk/test-helpers/plugin-runtime-mock.test.ts @@ -49,6 +49,32 @@ describe("createPluginRuntimeMock", () => { expect(vi.isMockFunction(debouncer.cancelKey)).toBe(true); }); + it("tracks channel runtime contexts through the mock registry", () => { + const runtime = createPluginRuntimeMock(); + const key = { + channelId: "whatsapp", + accountId: "default", + capability: "connection-controller", + }; + const context = { getActiveListener: vi.fn() }; + const onEvent = vi.fn(); + const unsubscribe = runtime.channel.runtimeContexts.watch({ ...key, onEvent }); + + const lease = runtime.channel.runtimeContexts.register({ ...key, context }); + + expect(runtime.channel.runtimeContexts.get(key)).toBe(context); + expect(onEvent).toHaveBeenCalledWith({ type: "registered", key, context }); + expect(vi.isMockFunction(runtime.channel.runtimeContexts.register)).toBe(true); + expect(vi.isMockFunction(runtime.channel.runtimeContexts.get)).toBe(true); + expect(vi.isMockFunction(runtime.channel.runtimeContexts.watch)).toBe(true); + + lease.dispose(); + + expect(runtime.channel.runtimeContexts.get(key)).toBeUndefined(); + expect(onEvent).toHaveBeenLastCalledWith({ type: "unregistered", key }); + unsubscribe(); + }); + it("exposes channel inbound helpers without the removed turn aliases", async () => { const runtime = createPluginRuntimeMock(); const channel = "test"; diff --git a/src/plugin-sdk/test-helpers/plugin-runtime-mock.ts b/src/plugin-sdk/test-helpers/plugin-runtime-mock.ts index 330fd0ae6e61..5556bed14ab9 100644 --- a/src/plugin-sdk/test-helpers/plugin-runtime-mock.ts +++ b/src/plugin-sdk/test-helpers/plugin-runtime-mock.ts @@ -5,6 +5,7 @@ import { sanitizeInboundSystemTags, } from "../../auto-reply/reply/inbound-text.js"; import { resolveSessionEntryResetFreshness } from "../../config/sessions/entry-freshness.js"; +import { createChannelRuntimeContextRegistry } from "../../plugins/runtime/channel-runtime-contexts.js"; import { implicitMentionKindWhen, resolveInboundMentionDecision, @@ -142,6 +143,7 @@ export function createPluginRuntimeMediaMock( } export function createPluginRuntimeMock(overrides: DeepPartial = {}): PluginRuntime { + const runtimeContexts = createChannelRuntimeContextRegistry(); const runEmbeddedAgentMock = vi.fn().mockResolvedValue({ payloads: [], meta: {}, @@ -773,14 +775,14 @@ export function createPluginRuntimeMock(overrides: DeepPartial = vi.fn() as unknown as PluginRuntime["channel"]["threadBindings"]["setMaxAgeBySessionKey"], }, runtimeContexts: { - register: vi.fn(({ abortSignal }: { abortSignal?: AbortSignal }) => { - const lease = { dispose: vi.fn() }; - abortSignal?.addEventListener("abort", lease.dispose, { once: true }); - return lease; - }) as unknown as PluginRuntime["channel"]["runtimeContexts"]["register"], - get: vi.fn() as unknown as PluginRuntime["channel"]["runtimeContexts"]["get"], - watch: vi.fn(() => - vi.fn(), + register: vi.fn( + runtimeContexts.register, + ) as unknown as PluginRuntime["channel"]["runtimeContexts"]["register"], + get: vi.fn( + runtimeContexts.get, + ) as unknown as PluginRuntime["channel"]["runtimeContexts"]["get"], + watch: vi.fn( + runtimeContexts.watch, ) as unknown as PluginRuntime["channel"]["runtimeContexts"]["watch"], }, activity: {} as PluginRuntime["channel"]["activity"],