Files
openclaw/extensions/diffs/src/config.test.ts
Peter Steinberger 508dd471b0 feat(slack): live session cards as the default progress mode (#122552)
* feat(slack): render live session cards as the default Slack progress mode

Slack streaming.mode default flips partial->progress.
Progress mode renders one live Block Kit session card with a status header, narration, plan, activity, diff stat, and elapsed time; it is edited in place and finalized to success or error with an Open in OpenClaw button when gateway.publicOrigin is set.
Final assistant text always delivers separately.
The shared progress compositor gains a success-only additive per-turn diffStat mirroring the task ledger fold.
resolveGatewayPublicOrigin is exported through the plugin SDK.
The diffs viewer URL falls back to publicOrigin.
The old rich/text progress render fork is deleted.
Native task cards remain unchanged and opt-in.

* chore(config): regenerate bundled channel config metadata

* refactor(slack): keep session card state type internal

* refactor(slack): split session-card and diff-stat owners under lint ceilings

* refactor(channels): reuse diff-stat type from its owner module

Import ChannelProgressDraftDiffStat from progress-draft-diffstat instead of
redeclaring it in the compositor, resolving the all-exports deadcode scan.

* chore(plugin-sdk): regenerate api baselines for channel barrels

Baselines drifted after the rebase reconciled them against main; regenerate to
match the branch's actual channel-message/channel-outbound surface.

* fix(slack): drop a session card that cannot terminalize after final delivery

If the final reply is delivered but the terminal card edit fails, the caller
now clears the stale card instead of leaving it stuck in its Working state
(mirrors the pre-card preview cleanup). Adds a transport-failure regression and
corrects three tests that asserted the prior ignore-the-result behavior.

Documents resolveGatewayPublicOrigin as a dependency-light runtime helper on the
config-contracts SDK subpath, which previously described a type-only surface.

Addresses ClawSweeper P2 (unfinalized card) and P1 (runtime SDK contract).

* fix(slack): suppress default tool messages under the default progress card

resolveChannelStreamingSuppressDefaultToolProgressMessages re-derived the stream
mode from config with an "off" default, unlike its sibling resolvers which take
a caller-resolved mode override. After this branch made progress the Slack
default, a default-config channel turn saw mode "off" and left a stray
"Using tool: X" plain message posting alongside the session card. Thread the
caller-resolved mode through (compositor passes params.mode; Slack dispatch passes
slackStreaming.mode), matching resolveChannelStreamingPreviewToolProgress.

Retarget the progress-session-card delivery-trace golden at an EMPTY Slack config
so it proves the real default path; the regenerated golden is byte-identical,
confirming defaults now yield the clean card sequence (one card post, separate
final text, one terminal update with the Open in OpenClaw button, no stray tool
message). Switch the dispatch delivery-mode mock to the real resolver so the card
tests exercise the true channel default (automatic), not a hand-rolled one.

* chore(plugin-sdk): regenerate api baselines for the streaming mode param

resolveChannelStreamingSuppressDefaultToolProgressMessages gained an optional
mode override; the changed signature reflows the surface hash of every barrel
that re-exports it, so regenerate the affected baselines.

* chore(config): regenerate config baselines
2026-08-12 10:38:31 -07:00

579 lines
16 KiB
TypeScript

import fs from "node:fs";
import {
validateJsonSchemaValue,
type JsonSchemaObject,
} from "openclaw/plugin-sdk/json-schema-runtime";
// Diffs tests cover config plugin behavior.
import { createRequireRecord } from "openclaw/plugin-sdk/test-fixtures";
import { beforeAll, describe, expect, it } from "vitest";
import {
diffsPluginConfigSchema,
resolveDiffImageRenderOptions,
resolveDiffsPluginDefaults,
resolveDiffsPluginSecurity,
resolveDiffsPluginViewerBaseUrl,
} from "./config.js";
import { ensureCuratedViewerRuntimeForTests } from "./test-helpers.js";
import { buildViewerUrl, normalizeViewerBaseUrl } from "./url.js";
import {
getServedLanguagePackViewerAsset,
getServedViewerAsset,
LANGUAGE_PACK_VIEWER_ASSET_PREFIX,
VIEWER_ASSET_PREFIX,
VIEWER_RUNTIME_PATH,
} from "./viewer-assets.js";
import { parseViewerPayloadJson } from "./viewer-payload.js";
const DEFAULT_DIFFS_TOOL_DEFAULTS = resolveDiffsPluginDefaults(undefined);
const DEFAULT_DIFFS_PLUGIN_SECURITY = resolveDiffsPluginSecurity(undefined);
const VIEWER_LOADER_PATH = `${VIEWER_ASSET_PREFIX}viewer.js`;
const LANGUAGE_PACK_VIEWER_LOADER_PATH = `${LANGUAGE_PACK_VIEWER_ASSET_PREFIX}viewer.js`;
const FULL_DEFAULTS = {
fontFamily: "JetBrains Mono",
fontSize: 17,
lineSpacing: 1.8,
layout: "split",
showLineNumbers: false,
diffIndicators: "classic",
wordWrap: false,
background: false,
theme: "light",
fileFormat: "pdf",
fileQuality: "hq",
fileScale: 2.6,
fileMaxWidth: 1280,
mode: "file",
ttlSeconds: 21_600,
} as const;
beforeAll(async () => {
await ensureCuratedViewerRuntimeForTests();
});
function compileManifestConfigSchema() {
const manifest = JSON.parse(
fs.readFileSync(new URL("../openclaw.plugin.json", import.meta.url), "utf8"),
) as { configSchema: JsonSchemaObject };
return (value: unknown) =>
validateJsonSchemaValue({
cacheKey: "diffs.manifest.config.test",
schema: manifest.configSchema,
value,
applyDefaults: true,
});
}
const requireRecord = createRequireRecord("object", "expected-label");
function expectFields(value: unknown, fields: Record<string, unknown>) {
const record = requireRecord(value, "record");
for (const [key, expected] of Object.entries(fields)) {
expect(record[key]).toEqual(expected);
}
}
describe("resolveDiffsPluginDefaults", () => {
it("returns built-in defaults when config is missing", () => {
expect(resolveDiffsPluginDefaults(undefined)).toEqual(DEFAULT_DIFFS_TOOL_DEFAULTS);
});
it("applies configured defaults from plugin config", () => {
expect(
resolveDiffsPluginDefaults({
defaults: FULL_DEFAULTS,
}),
).toEqual(FULL_DEFAULTS);
});
it("clamps and falls back for invalid line spacing and indicators", () => {
expectFields(
resolveDiffsPluginDefaults({
defaults: {
lineSpacing: -5,
diffIndicators: "unknown",
},
}),
{
lineSpacing: 1,
diffIndicators: "bars",
},
);
expectFields(
resolveDiffsPluginDefaults({
defaults: {
lineSpacing: 9,
},
}),
{
lineSpacing: 3,
},
);
expectFields(
resolveDiffsPluginDefaults({
defaults: {
lineSpacing: Number.NaN,
},
}),
{
lineSpacing: DEFAULT_DIFFS_TOOL_DEFAULTS.lineSpacing,
},
);
});
it("derives file defaults from quality preset and clamps explicit overrides", () => {
expectFields(
resolveDiffsPluginDefaults({
defaults: {
fileQuality: "print",
},
}),
{
fileQuality: "print",
fileScale: 3,
fileMaxWidth: 1400,
},
);
expectFields(
resolveDiffsPluginDefaults({
defaults: {
fileQuality: "hq",
fileScale: 99,
fileMaxWidth: 99999,
},
}),
{
fileQuality: "hq",
fileScale: 4,
fileMaxWidth: 2400,
},
);
});
it("falls back to png for invalid file format defaults", () => {
expectFields(
resolveDiffsPluginDefaults({
defaults: {
fileFormat: "invalid" as "png",
},
}),
{
fileFormat: "png",
},
);
});
it("resolves file render format from defaults and explicit overrides", () => {
const defaults = resolveDiffsPluginDefaults({
defaults: {
fileFormat: "pdf",
},
});
expect(resolveDiffImageRenderOptions({ defaults }).format).toBe("pdf");
expect(resolveDiffImageRenderOptions({ defaults, fileFormat: "png" }).format).toBe("png");
});
it("accepts format as a config alias for fileFormat", () => {
expectFields(
resolveDiffsPluginDefaults({
defaults: {
format: "pdf",
},
}),
{
fileFormat: "pdf",
},
);
});
it("accepts image* config aliases for backward compatibility", () => {
expectFields(
resolveDiffsPluginDefaults({
defaults: {
imageFormat: "pdf",
imageQuality: "hq",
imageScale: 2.2,
imageMaxWidth: 1024,
},
}),
{
fileFormat: "pdf",
fileQuality: "hq",
fileScale: 2.2,
fileMaxWidth: 1024,
},
);
});
it("prefers an explicit canonical default value over a deprecated alias", () => {
expectFields(
resolveDiffsPluginDefaults({
defaults: {
fileFormat: "png",
imageFormat: "pdf",
},
}),
{
fileFormat: "png",
},
);
});
it("accepts plugin-wide artifact TTL defaults", () => {
expectFields(
resolveDiffsPluginDefaults({
defaults: {
ttlSeconds: 21_600,
},
}),
{
ttlSeconds: 21_600,
},
);
expectFields(
resolveDiffsPluginDefaults({
defaults: {
ttlSeconds: 99_999,
},
}),
{
ttlSeconds: 21_600,
},
);
});
it("keeps alias-only config values after manifest validation", () => {
const validate = compileManifestConfigSchema();
const aliasOnly = {
defaults: {
format: "pdf",
imageQuality: "hq",
},
};
const validatedAliasOnly = validate(aliasOnly);
if (!validatedAliasOnly.ok) {
throw new Error("Expected alias-only config to pass manifest validation.");
}
expectFields(resolveDiffsPluginDefaults(validatedAliasOnly.value), {
fileFormat: "pdf",
fileQuality: "hq",
fileScale: 2.5,
fileMaxWidth: 1200,
});
const qualityOnly = {
defaults: {
fileQuality: "hq",
},
};
const validatedQualityOnly = validate(qualityOnly);
if (!validatedQualityOnly.ok) {
throw new Error("Expected quality-only config to pass manifest validation.");
}
expectFields(resolveDiffsPluginDefaults(validatedQualityOnly.value), {
fileQuality: "hq",
fileScale: 2.5,
fileMaxWidth: 1200,
});
});
});
describe("resolveDiffsPluginSecurity", () => {
it("defaults to local-only viewer access", () => {
expect(resolveDiffsPluginSecurity(undefined)).toEqual(DEFAULT_DIFFS_PLUGIN_SECURITY);
});
it("allows opt-in remote viewer access", () => {
expect(resolveDiffsPluginSecurity({ security: { allowRemoteViewer: true } })).toEqual({
allowRemoteViewer: true,
});
});
});
describe("resolveDiffsPluginViewerBaseUrl", () => {
it("defaults to undefined when config is missing", () => {
expect(resolveDiffsPluginViewerBaseUrl(undefined)).toBeUndefined();
});
it("normalizes configured viewer base URLs", () => {
expect(
resolveDiffsPluginViewerBaseUrl({
viewerBaseUrl: "https://example.com/openclaw/",
}),
).toBe("https://example.com/openclaw");
});
});
describe("diffs plugin schema surfaces", () => {
it("rejects invalid viewerBaseUrl values at manifest-validation time too", () => {
const validate = compileManifestConfigSchema();
expect(validate({ viewerBaseUrl: "javascript:alert(1)" }).ok).toBe(false);
expect(validate({ viewerBaseUrl: "https://example.com/openclaw?x=1" }).ok).toBe(false);
expect(validate({ viewerBaseUrl: "https://example.com/openclaw#frag" }).ok).toBe(false);
expect(validate({ viewerBaseUrl: "https://example.com/openclaw/" }).ok).toBe(true);
});
it("preserves defaults and security for direct safeParse callers", () => {
const parsed = requireRecord(
diffsPluginConfigSchema.safeParse?.({
viewerBaseUrl: "https://example.com/openclaw/",
defaults: {
theme: "light",
ttlSeconds: 21_600,
},
security: {
allowRemoteViewer: true,
},
}),
"parse result",
);
expect(parsed.success).toBe(true);
const data = requireRecord(parsed.data, "parse data");
expect(data.viewerBaseUrl).toBe("https://example.com/openclaw");
expectFields(data.defaults, {
fontFamily: "Fira Code",
fontSize: 15,
lineSpacing: 1.6,
layout: "unified",
showLineNumbers: true,
diffIndicators: "bars",
wordWrap: true,
background: true,
theme: "light",
fileFormat: "png",
fileQuality: "standard",
fileScale: 2,
fileMaxWidth: 960,
mode: "both",
ttlSeconds: 21_600,
});
expectFields(data.security, { allowRemoteViewer: true });
});
it("resolves deprecated aliases before safeParse applies runtime defaults", () => {
const parsed = requireRecord(
diffsPluginConfigSchema.safeParse?.({
defaults: {
format: "pdf",
imageQuality: "hq",
},
}),
"parse result",
);
expect(parsed.success).toBe(true);
const data = requireRecord(parsed.data, "parse data");
expectFields(data.defaults, {
fileFormat: "pdf",
fileQuality: "hq",
fileScale: 2.5,
fileMaxWidth: 1200,
});
});
it("rejects invalid viewerBaseUrl config values", () => {
const parsed = requireRecord(
diffsPluginConfigSchema.safeParse?.({
viewerBaseUrl: "javascript:alert(1)",
}),
"parse result",
);
expect(parsed.success).toBe(false);
const error = requireRecord(parsed.error, "parse error");
const issues = error.issues as Array<{ path?: unknown; message?: unknown }>;
expect(issues).toHaveLength(1);
expect(issues[0]?.path).toEqual(["viewerBaseUrl"]);
expect(issues[0]?.message).toBe("viewerBaseUrl must use http or https: javascript:alert(1)");
});
it("keeps the runtime json schema in sync with the manifest config schema", () => {
const manifest = JSON.parse(
fs.readFileSync(new URL("../openclaw.plugin.json", import.meta.url), "utf8"),
) as { configSchema?: unknown };
expect(diffsPluginConfigSchema.jsonSchema).toEqual(manifest.configSchema);
});
});
describe("diffs viewer URL helpers", () => {
it("defaults to loopback for lan/tailnet bind modes", () => {
expect(
buildViewerUrl({
config: { gateway: { bind: "lan", port: 18789 } },
viewerPath: "/plugins/diffs/view/id/token",
}),
).toBe("http://127.0.0.1:18789/plugins/diffs/view/id/token");
expect(
buildViewerUrl({
config: { gateway: { bind: "tailnet", port: 24444 } },
viewerPath: "/plugins/diffs/view/id/token",
}),
).toBe("http://127.0.0.1:24444/plugins/diffs/view/id/token");
});
it("resolves explicit, plugin, public, then bind-aware viewer bases", () => {
expect(
buildViewerUrl({
config: { gateway: { publicOrigin: "https://public.example.com" } },
baseUrl: "https://explicit.example.com/review",
viewerBaseUrl: "https://plugin.example.com/viewer",
viewerPath: "/plugins/diffs/view/id/token",
}),
).toBe("https://explicit.example.com/review/plugins/diffs/view/id/token");
expect(
buildViewerUrl({
config: { gateway: { publicOrigin: "https://public.example.com" } },
viewerBaseUrl: "https://plugin.example.com/viewer",
viewerPath: "/plugins/diffs/view/id/token",
}),
).toBe("https://plugin.example.com/viewer/plugins/diffs/view/id/token");
expect(
buildViewerUrl({
config: { gateway: { publicOrigin: "https://public.example.com" } },
viewerPath: "/plugins/diffs/view/id/token",
}),
).toBe("https://public.example.com/plugins/diffs/view/id/token");
expect(
buildViewerUrl({
config: {
gateway: {
bind: "custom",
customBindHost: "gateway.example.com",
port: 443,
tls: { enabled: true },
},
},
viewerPath: "/plugins/diffs/view/id/token",
}),
).toBe("https://gateway.example.com/plugins/diffs/view/id/token");
});
it("joins viewer path under baseUrl pathname", () => {
expect(
buildViewerUrl({
config: {},
baseUrl: "https://example.com/openclaw",
viewerPath: "/plugins/diffs/view/id/token",
}),
).toBe("https://example.com/openclaw/plugins/diffs/view/id/token");
});
it("prefers normalized viewerBaseUrl strings too", () => {
expect(
buildViewerUrl({
config: {},
baseUrl: "https://example.com/openclaw/",
viewerPath: "/plugins/diffs/view/id/token",
}),
).toBe("https://example.com/openclaw/plugins/diffs/view/id/token");
});
it("rejects base URLs with query/hash", () => {
expect(() => normalizeViewerBaseUrl("https://example.com?a=1")).toThrow(
"baseUrl must not include query/hash",
);
expect(() => normalizeViewerBaseUrl("https://example.com#frag")).toThrow(
"baseUrl must not include query/hash",
);
});
it("uses the configured field name in viewerBaseUrl validation errors", () => {
expect(() => normalizeViewerBaseUrl("https://example.com?a=1", "viewerBaseUrl")).toThrow(
"viewerBaseUrl must not include query/hash",
);
});
});
describe("viewer assets", () => {
it("serves a stable loader that points at the current runtime bundle", async () => {
const loader = await getServedViewerAsset(VIEWER_LOADER_PATH);
expect(loader?.contentType).toBe("text/javascript; charset=utf-8");
expect(String(loader?.body)).toContain(`./viewer-runtime.js?v=`);
});
it("serves the runtime bundle body", async () => {
const runtime = await getServedViewerAsset(VIEWER_RUNTIME_PATH);
expect(runtime?.contentType).toBe("text/javascript; charset=utf-8");
expect(String(runtime?.body)).toContain("openclawDiffsReady");
expect(String(runtime?.body)).toContain('style.width="24px"');
expect(String(runtime?.body)).toContain('style.gap="6px"');
});
it("serves the optional language-pack loader only when its generated runtime is present", async () => {
const loader = await getServedLanguagePackViewerAsset(LANGUAGE_PACK_VIEWER_LOADER_PATH);
if (!loader) {
expect(loader).toBeNull();
return;
}
expect(loader.contentType).toBe("text/javascript; charset=utf-8");
expect(String(loader.body)).toContain(`./viewer-runtime.js?v=`);
});
it("returns null for unknown asset paths", async () => {
await expect(getServedViewerAsset("/plugins/diffs/assets/not-real.js")).resolves.toBeNull();
});
});
describe("parseViewerPayloadJson", () => {
function buildValidPayload(): Record<string, unknown> {
return {
prerenderedHTML: "<div>ok</div>",
langs: ["text"],
oldFile: {
name: "README.md",
contents: "before",
},
newFile: {
name: "README.md",
contents: "after",
},
options: {
theme: {
light: "pierre-light",
dark: "pierre-dark",
},
diffStyle: "unified",
diffIndicators: "bars",
disableLineNumbers: false,
expandUnchanged: false,
themeType: "dark",
backgroundEnabled: true,
overflow: "wrap",
unsafeCSS: ":host{}",
},
};
}
it("accepts valid payload JSON", () => {
const parsed = parseViewerPayloadJson(JSON.stringify(buildValidPayload()));
expect(parsed.options.diffStyle).toBe("unified");
expect(parsed.options.diffIndicators).toBe("bars");
});
it("rejects payloads with invalid shape", () => {
const broken = buildValidPayload();
broken.options = {
...(broken.options as Record<string, unknown>),
diffIndicators: "invalid",
};
expect(() => parseViewerPayloadJson(JSON.stringify(broken))).toThrow(
"Diff payload has invalid shape.",
);
});
it("rejects invalid JSON", () => {
expect(() => parseViewerPayloadJson("{not-json")).toThrow("Diff payload is not valid JSON.");
});
});