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 <zim@o526.net>
Co-authored-by: George Pickett <gpickett00@gmail.com>
This commit is contained in:
ShihChi Huang
2026-04-14 08:33:49 +08:00
committed by GitHub
parent 5577d81ab6
commit df3e65c8d3
4 changed files with 33 additions and 0 deletions

View File

@@ -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

View File

@@ -0,0 +1 @@
export { normalizeCompatibilityConfig, legacyConfigRules } from "./src/doctor-contract.js";

View File

@@ -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({

View File

@@ -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({