Files
openclaw/src/plugins/loader-records.test.ts
T
Vincent Koc 14940edf15 feat(skills): add Skill Workshop lifecycle hooks (#115606)
* feat(skills): add lifecycle hook contracts

* feat(plugins): expose skill hook contracts

* feat(plugins): identify skill evaluators

* feat(skills): persist proposal evaluation lifecycle

* feat(skills): add agent evaluation action

* feat(skills): emit committed skill lifecycle changes

* feat(gateway): expose skill proposal evaluation lifecycle

* feat(ui): add Skill Workshop evaluations

* fix(skills): bind lifecycle state to proposal revisions

* fix(skills): preserve lifecycle events without artifacts

* feat(cli): evaluate skill proposals

* fix(ui): bind evaluations to proposal revisions

* docs(skills): document lifecycle hook primitives

* chore(plugin-sdk): refresh skill hook surface

* fix(skills): harden proposal evaluator execution

* fix(plugins): isolate skill evaluator inputs

* fix(cli): align skill lifecycle deadlines

* fix(skills): preserve evaluation replay invariants

* test(ui): capture Skill Workshop evaluation proof

* fix(skills): bind apply to evaluated target tree

* fix(skills): preserve evaluation contract edges

* fix(skills): bound evaluation event storage

* chore(skills): keep lifecycle helpers internal

* refactor(skills): isolate evaluation persistence

* fix(skills): satisfy lifecycle validation gates

* chore(protocol): refresh Skill Workshop clients

* docs: refresh Skill Workshop map

* chore: keep release notes in PR metadata

* docs: refresh merged docs map

* fix(ci): type Code Mode catch errors

* fix(skills): freeze lifecycle observation payloads

* fix(protocol): keep proposal inspect backward-decodable

* fix(skills): enforce final evaluator bundle limits

* fix(skills): preserve lifecycle caller attribution

* chore: drop subsumed Code Mode formatting

* test(plugins): adapt lifecycle hook mocks
2026-07-29 15:49:44 +08:00

129 lines
4.8 KiB
TypeScript

/** Verifies plugin loader records expose stable metadata for registered plugin surfaces. */
import { afterEach, describe, expect, it, vi } from "vitest";
import { createPluginRecord, recordPluginError } from "./loader-records.js";
import { createEmptyPluginRegistry } from "./registry-empty.js";
describe("plugin loader records", () => {
afterEach(() => {
vi.unstubAllEnvs();
});
it("keeps package attribution separate from the runtime version", () => {
const record = createPluginRecord({
id: "versioned",
name: "Versioned",
source: "test",
origin: "workspace",
enabled: true,
configSchema: false,
packageVersion: "1.2.3",
version: "runtime-4",
});
expect(record).toMatchObject({
packageVersion: "1.2.3",
version: "runtime-4",
});
});
it("preserves manifest-declared channel ids before runtime registration", () => {
const record = createPluginRecord({
id: "kitchen-sink",
name: "Kitchen Sink",
source: "/tmp/kitchen-sink/index.js",
origin: "global",
enabled: true,
channelIds: ["kitchen-sink-channel"],
configSchema: false,
});
expect(record.channelIds).toEqual(["kitchen-sink-channel"]);
});
it("preserves manifest-declared provider ids before runtime registration", () => {
const record = createPluginRecord({
id: "kitchen-sink",
name: "Kitchen Sink",
source: "/tmp/kitchen-sink/index.js",
origin: "global",
enabled: true,
providerIds: ["kitchen-sink-provider"],
configSchema: false,
});
expect(record.providerIds).toEqual(["kitchen-sink-provider"]);
});
it("preserves manifest-declared capability provider ids before runtime registration", () => {
const record = createPluginRecord({
id: "kitchen-sink",
name: "Kitchen Sink",
source: "/tmp/kitchen-sink/index.js",
origin: "global",
enabled: true,
contracts: {
embeddingProviders: ["kitchen-sink-embedding-provider"],
speechProviders: ["kitchen-sink-speech-provider"],
realtimeTranscriptionProviders: ["kitchen-sink-transcription-provider"],
realtimeVoiceProviders: ["kitchen-sink-voice-provider"],
mediaUnderstandingProviders: ["kitchen-sink-media-provider"],
imageGenerationProviders: ["kitchen-sink-image-provider"],
videoGenerationProviders: ["kitchen-sink-video-provider"],
musicGenerationProviders: ["kitchen-sink-music-provider"],
webFetchProviders: ["kitchen-sink-web-fetch-provider"],
webSearchProviders: ["kitchen-sink-web-search-provider"],
migrationProviders: ["kitchen-sink-migration-provider"],
memoryEmbeddingProviders: ["kitchen-sink-memory-provider"],
},
configSchema: false,
});
expect(record.embeddingProviderIds).toEqual(["kitchen-sink-embedding-provider"]);
expect(record.speechProviderIds).toEqual(["kitchen-sink-speech-provider"]);
expect(record.realtimeTranscriptionProviderIds).toEqual([
"kitchen-sink-transcription-provider",
]);
expect(record.realtimeVoiceProviderIds).toEqual(["kitchen-sink-voice-provider"]);
expect(record.mediaUnderstandingProviderIds).toEqual(["kitchen-sink-media-provider"]);
expect(record.imageGenerationProviderIds).toEqual(["kitchen-sink-image-provider"]);
expect(record.videoGenerationProviderIds).toEqual(["kitchen-sink-video-provider"]);
expect(record.musicGenerationProviderIds).toEqual(["kitchen-sink-music-provider"]);
expect(record.webFetchProviderIds).toEqual(["kitchen-sink-web-fetch-provider"]);
expect(record.webSearchProviderIds).toEqual(["kitchen-sink-web-search-provider"]);
expect(record.migrationProviderIds).toEqual(["kitchen-sink-migration-provider"]);
expect(record.memoryEmbeddingProviderIds).toEqual(["kitchen-sink-memory-provider"]);
});
it.each([
{ diagnostics: "", expected: "Error: boom" },
{ diagnostics: "1", expected: "Error: boom\n at plugin-entry.ts:1:1" },
])("uses lifecycle tracing for loader error stacks", ({ diagnostics, expected }) => {
vi.stubEnv("OPENCLAW_PLUGIN_LIFECYCLE_TRACE", diagnostics);
const registry = createEmptyPluginRegistry();
const record = createPluginRecord({
id: "broken-plugin",
source: "/tmp/broken-plugin/index.js",
origin: "global",
enabled: true,
configSchema: false,
});
const error = new Error("boom");
error.stack = "Error: boom\n at plugin-entry.ts:1:1";
recordPluginError({
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
registry,
record,
seenIds: new Map(),
pluginId: record.id,
origin: record.origin,
phase: "load",
error,
logPrefix: "",
diagnosticMessagePrefix: "",
});
expect(record.error).toBe(expected);
});
});