From df3e65c8d3d853467fbb5e4350fb50969365e015 Mon Sep 17 00:00:00 2001 From: ShihChi Huang Date: Tue, 14 Apr 2026 08:33:49 +0800 Subject: [PATCH] fix(slack): isolate doctor contract API (#63192) * Slack: isolate doctor contract API * chore: changelog * fix(slack): move doctor changelog entry to Unreleased * Plugins: lock Slack doctor sidecar metadata * Slack: fix changelog entry placement --------- Co-authored-by: @zimeg Co-authored-by: George Pickett --- CHANGELOG.md | 1 + extensions/slack/doctor-contract-api.ts | 1 + src/plugins/bundled-plugin-metadata.test.ts | 7 ++++++ src/plugins/doctor-contract-registry.test.ts | 24 ++++++++++++++++++++ 4 files changed, 33 insertions(+) create mode 100644 extensions/slack/doctor-contract-api.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4347d9d2b2a..c69d6e89f86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ Docs: https://docs.openclaw.ai - TTS/reply media: persist OpenClaw temp voice outputs into managed outbound media and allow them through reply-media normalization, so voice-note replies stop silently dropping. (#63511) Thanks @jetd1. - Agents/OpenAI: recover embedded GPT-style runs when reasoning-only or empty turns need bounded continuation, with replay-safe retry gating and incomplete-turn fallback when no visible answer arrives. (#66167) thanks @jalehman - Outbound/relay-status: suppress internal relay-status placeholder payloads (`No channel reply.`, `Replied in-thread.`, `Replied in #...`, wiki-update status variants ending in `No channel reply.`) before channel delivery so internal housekeeping text does not leak to users. +- Slack/doctor: add a dedicated doctor-contract sidecar so config warmup paths such as `openclaw cron` no longer fall back to Slack's broader contract surface, which could trigger Slack-related config-read crashes on affected setups. (#63192) Thanks @shhtheonlyperson. ## 2026.4.12 diff --git a/extensions/slack/doctor-contract-api.ts b/extensions/slack/doctor-contract-api.ts new file mode 100644 index 00000000000..a7a56f23442 --- /dev/null +++ b/extensions/slack/doctor-contract-api.ts @@ -0,0 +1 @@ +export { normalizeCompatibilityConfig, legacyConfigRules } from "./src/doctor-contract.js"; diff --git a/src/plugins/bundled-plugin-metadata.test.ts b/src/plugins/bundled-plugin-metadata.test.ts index 20aa988f0f4..939dac4c3d1 100644 --- a/src/plugins/bundled-plugin-metadata.test.ts +++ b/src/plugins/bundled-plugin-metadata.test.ts @@ -157,6 +157,13 @@ describe("bundled plugin metadata", () => { ); }); + it("keeps Slack's doctor contract sidecar on the bundled public surface", () => { + const slack = listRepoBundledPluginMetadata().find((entry) => entry.dirName === "slack"); + expectArtifactPresence(slack?.publicSurfaceArtifacts, { + contains: ["doctor-contract-api.js"], + }); + }); + it("loads tlon channel config metadata from the lightweight schema surface", () => { expect(collectRepoBundledChannelConfigsForTest("tlon")?.tlon).toEqual( expect.objectContaining({ diff --git a/src/plugins/doctor-contract-registry.test.ts b/src/plugins/doctor-contract-registry.test.ts index f8a22fc11c7..d83eb2ad937 100644 --- a/src/plugins/doctor-contract-registry.test.ts +++ b/src/plugins/doctor-contract-registry.test.ts @@ -61,6 +61,30 @@ describe("doctor-contract-registry getJiti", () => { ); }); + it("prefers doctor-contract-api over the broader contract-api surface", () => { + const pluginRoot = makeTempDir(); + fs.writeFileSync( + path.join(pluginRoot, "doctor-contract-api.js"), + "export default {};\n", + "utf-8", + ); + fs.writeFileSync(path.join(pluginRoot, "contract-api.js"), "export default {};\n", "utf-8"); + mocks.loadPluginManifestRegistry.mockReturnValue({ + plugins: [{ id: "test-plugin", rootDir: pluginRoot }], + diagnostics: [], + }); + + listPluginDoctorLegacyConfigRules({ + workspaceDir: pluginRoot, + env: {}, + }); + + expect(mocks.createJiti).toHaveBeenCalledTimes(1); + expect(mocks.createJiti.mock.calls[0]?.[0]).toBe( + path.join(pluginRoot, "doctor-contract-api.js"), + ); + }); + it("narrows touched-path doctor ids for scoped dry-run validation", () => { expect( collectRelevantDoctorPluginIdsForTouchedPaths({