From 7b5f706398f712ffd574d9810e2c2f6806283a4e Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 29 Apr 2026 06:32:59 -0700 Subject: [PATCH] fix(inspector): accept channel factory seams --- CHANGELOG.md | 1 + src/inspector.js | 16 +++++++++++++++- test/inspector.test.js | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2db8cc7..67e55dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed - Classify `createChatChannelPlugin` as channel factory metadata in synthetic probe plans so channel-core plugins do not fail as unknown registrars. +- Treat `createChatChannelPlugin` and `defineChannelPluginEntry` as channel registration equivalents when validating fixture expectations. ## 0.3.4 - 2026-04-29 diff --git a/src/inspector.js b/src/inspector.js index 95e58b4..f1d3148 100644 --- a/src/inspector.js +++ b/src/inspector.js @@ -11,6 +11,9 @@ import { readOpenClawTargetSurface } from "./openclaw-target.js"; import { buildCompatibilityReport, buildReport } from "./report.js"; const execFileAsync = promisify(execFile); +const registrationEquivalents = new Map([ + ["registerChannel", new Set(["createChatChannelPlugin", "defineChannelPluginEntry", "registerChannel"])], +]); export async function inspectFixtureSet(config, options = {}) { const { inspections, failures } = await inspectConfiguredFixtures(config, options); @@ -58,7 +61,7 @@ async function inspectConfiguredFixtures(config, options = {}) { ["manifestContracts", inspection.manifestContracts], ]) { const expected = fixture.expect?.[key] ?? []; - const missing = expected.filter((value) => !observed.includes(value)); + const missing = expected.filter((value) => !satisfiesExpectedSeam(key, value, observed)); if (missing.length > 0) { failures.push(`${fixture.id}: missing ${key}: ${missing.join(", ")}`); } @@ -68,6 +71,17 @@ async function inspectConfiguredFixtures(config, options = {}) { return { inspections, failures }; } +function satisfiesExpectedSeam(key, expected, observed) { + if (observed.includes(expected)) { + return true; + } + if (key !== "registrations") { + return false; + } + const equivalents = registrationEquivalents.get(expected); + return Boolean(equivalents && observed.some((value) => equivalents.has(value))); +} + export async function inspectPlugin(fixture, options = {}) { const config = options.config ?? { rootDir: options.rootDir ?? process.cwd() }; const checkoutPath = fixtureCheckoutPath(config, fixture); diff --git a/test/inspector.test.js b/test/inspector.test.js index c9f529f..9743c0e 100644 --- a/test/inspector.test.js +++ b/test/inspector.test.js @@ -65,6 +65,42 @@ test("fixture set inspection reports missing expected seams", async () => { assert.match(report.breakages[0].message, /llm_output/); }); +test("fixture set inspection treats channel factories as channel registration coverage", async () => { + const dir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-channel-factory-")); + await mkdir(path.join(dir, "fixture"), { recursive: true }); + await writeFile( + path.join(dir, "fixture", "index.js"), + [ + 'import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";', + "", + "export const channel = createChatChannelPlugin({ id: 'fixture-channel' });", + ].join("\n"), + "utf8", + ); + + const report = await inspectFixtureSet({ + version: 1, + submoduleRoot: ".", + rootDir: dir, + fixtures: [ + { + id: "fixture", + path: "fixture", + repo: "https://github.com/openclaw/fixture.git", + priority: "high", + seams: ["channel"], + expect: { + registrations: ["registerChannel"], + }, + }, + ], + }); + + assert.equal(report.status, "pass"); + assert.deepEqual(report.breakages, []); + assert.deepEqual(report.fixtures[0].registrations, ["createChatChannelPlugin"]); +}); + test("capture entrypoint imports a local fixture and records registrations", async () => { const dir = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-capture-")); const entrypoint = path.join(dir, "fixture.mjs");