Files
openclaw/src/plugins/config-contracts.test.ts
T
Peter Steinberger 1ca60fbc3a refactor(agents): make multi-agent ownership explicit (H2-1 core) (#114388)
* refactor(agents): make roster ownership explicit

* feat(config): materialize legacy agent roles

* fix(cron): migrate legacy owners at startup

* feat(gateway): expose agent selection contracts

* fix(gateway): enforce agent-scoped authorization

* docs(config): document explicit agent ownership

* fix(config): pin retained owner workspace

* fix(gateway): target hook wakes at effective agent

* fix(sessions): preserve fixed-store ownership

* fix: preserve retained agent ownership

* fix: preserve legacy agent ownership across runtime surfaces

* fix: fail closed on ambiguous session ownership

* fix: preserve compatibility owners across dispatch and writes

* fix: preserve retained agent projections

* fix: preserve agent ownership compatibility

* fix: preserve per-agent heartbeat guidance

* fix: preserve compatibility owners in generic paths

* fix: enforce configured ownership in session paths

* fix: defer remote roster selection

* fix: preserve ownership across session and config writes

* fix: fail closed on ambiguous restored ownership

* fix: preserve explicit ACP and legacy ownership

* fix: honor durable fixed-store ownership

* fix: enforce fixed-store owner authority

* fix: preserve ownership evidence boundaries

* fix: honor resolved session ownership

* fix: align compatibility ownership paths

* fix: persist legacy main store ownership

* fix: close ownership fallback gaps

* fix(agents): close retained owner compatibility gaps

* fix(agents): enforce session owner resolution

* fix(agents): complete session owner resolution sweep

* fix(agents): preserve durable session ownership

* fix: complete persisted session owner routing

* fix: thread prepared session owners

* fix: preserve stable session ownership

* fix: enforce session ownership boundaries

* fix: close session ownership delta gaps

* fix: reconcile session ownership after rebase

* fix: reconcile ownership with current main

* fix: align session store path imports

* fix: align session store config path import

* fix: reconcile explicit ownership CI

* fix: reconcile ownership rebase checks

* fix: align ownership ci contracts

* fix: align ownership rebase checks

* fix: preserve compatibility owner during setup

* fix(doctor): migrate ownerless heartbeat monitors

* fix(gateway): preserve explicit session ownership

* test: align ownership fixtures after rebase

* test: complete plugin manifest fixture

* test: align runtime context mocks

* fix(gateway): preserve alias routing for existing sessions

* style: format agent routing update

* fix(gateway): preserve selected owner during alias routing

* style: normalize rebased ownership files

* fix(gateway): preserve owner through global alias routing

* fix(gateway): preserve explicit ownership at HTTP boundaries

* fix(gateway): validate compatibility model ownership

* fix(agents): reconcile strict session ownership

* fix(agents): contain media yield callback failures

* fix(agents): avoid eager bare-key owner resolution

* chore: refresh rebased ownership baselines

* chore: align hosted plugin SDK baseline

* chore: refresh ownership baselines after main sync

* chore: refresh ownership baselines after main sync

* test: align routed event owner fixtures

* chore: retrigger CI after runner startup failure

* chore: refresh ownership SDK budgets after main sync

* fix(tasks): require agent identity for bare owners

* chore: align Linux plugin SDK baseline

* chore: remove release-owned changelog entry
2026-08-12 15:55:16 -07:00

405 lines
12 KiB
TypeScript

// Covers plugin config contract validation and ownership boundaries.
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { PluginManifestRegistry } from "./manifest-registry.js";
const mocks = vi.hoisted(() => {
const loadManifestRegistry = vi.fn();
return {
discoverOpenClawPlugins: vi.fn(() => ({ candidates: [], diagnostics: [] })),
findBundledPluginMetadataById: vi.fn(),
loadBundledManifestRegistry: vi.fn(),
loadPluginManifestRegistryForInstalledIndex: loadManifestRegistry,
loadPluginManifestRegistryForPluginRegistry: loadManifestRegistry,
loadPluginRegistrySnapshot: vi.fn(() => ({ plugins: [] })),
};
});
vi.mock("./discovery.js", () => ({
discoverOpenClawPlugins: mocks.discoverOpenClawPlugins,
}));
vi.mock("./bundled-plugin-metadata.js", () => ({
findBundledPluginMetadataById: mocks.findBundledPluginMetadataById,
}));
vi.mock("./manifest-registry.js", () => ({
loadPluginManifestRegistryCore: mocks.loadBundledManifestRegistry,
}));
vi.mock("./manifest-registry-installed.js", () => ({
loadPluginManifestRegistryForInstalledIndex: mocks.loadPluginManifestRegistryForInstalledIndex,
}));
vi.mock("./plugin-registry.js", () => ({
loadPluginManifestRegistryForPluginRegistry: mocks.loadPluginManifestRegistryForPluginRegistry,
loadPluginRegistrySnapshot: mocks.loadPluginRegistrySnapshot,
}));
import {
collectPluginConfigContractMatches,
resolvePluginConfigContractsById,
} from "./config-contracts.js";
type PluginManifestRecord = PluginManifestRegistry["plugins"][number];
function createRegistry(plugins: PluginManifestRegistry["plugins"]): PluginManifestRegistry {
return {
plugins,
diagnostics: [],
};
}
function createPluginRecord(
overrides: Pick<PluginManifestRecord, "id" | "origin"> & Partial<PluginManifestRecord>,
): PluginManifestRecord {
return {
rootDir: `/tmp/${overrides.id}`,
manifestPath: `/tmp/${overrides.id}/openclaw.plugin.json`,
channelConfigs: undefined,
configUiHints: undefined,
configSchema: undefined,
configContracts: undefined,
contracts: undefined,
name: undefined,
description: undefined,
version: undefined,
enabledByDefault: undefined,
autoEnableWhenConfiguredProviders: undefined,
legacyPluginIds: undefined,
format: undefined,
bundleFormat: undefined,
bundleCapabilities: undefined,
kind: undefined,
channels: [],
providers: [],
modelSupport: undefined,
cliBackends: [],
providerAuthAliases: undefined,
providerAuthChoices: undefined,
skills: [],
settingsFiles: undefined,
hooks: [],
source: `/tmp/${overrides.id}/openclaw.plugin.json`,
setupSource: undefined,
channelCatalogMeta: undefined,
...overrides,
};
}
describe("resolvePluginConfigContractsById", () => {
beforeEach(() => {
mocks.discoverOpenClawPlugins.mockReset();
mocks.discoverOpenClawPlugins.mockReturnValue({ candidates: [], diagnostics: [] });
mocks.findBundledPluginMetadataById.mockReset();
mocks.loadBundledManifestRegistry.mockReset();
mocks.loadBundledManifestRegistry.mockReturnValue(createRegistry([]));
mocks.loadPluginManifestRegistryForInstalledIndex.mockReset();
mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue(createRegistry([]));
mocks.loadPluginRegistrySnapshot.mockReset();
mocks.loadPluginRegistrySnapshot.mockReturnValue({ plugins: [] });
});
it("uses a supplied manifest registry as the authoritative contract source", () => {
const manifestRegistry = createRegistry([
createPluginRecord({
id: "prepared-plugin",
origin: "config",
configContracts: {
secretInputs: {
paths: [{ path: "credentials.token", expected: "string" }],
},
},
}),
]);
expect(
resolvePluginConfigContractsById({
pluginIds: ["prepared-plugin"],
manifestRegistry,
fallbackToBundledMetadata: true,
fallbackToBundledMetadataForResolvedBundled: true,
fallbackBundledPluginIds: ["prepared-plugin"],
}),
).toEqual(
new Map([
[
"prepared-plugin",
{
origin: "config",
configContracts: {
secretInputs: {
paths: [{ path: "credentials.token", expected: "string" }],
},
},
},
],
]),
);
expect(mocks.loadPluginManifestRegistryForPluginRegistry).not.toHaveBeenCalled();
expect(mocks.discoverOpenClawPlugins).not.toHaveBeenCalled();
expect(mocks.loadBundledManifestRegistry).not.toHaveBeenCalled();
expect(mocks.findBundledPluginMetadataById).not.toHaveBeenCalled();
});
it("does not fall back to bundled registry when registry already resolved a plugin without config contracts", () => {
mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue(
createRegistry([
createPluginRecord({
id: "brave",
origin: "bundled",
}),
]),
);
expect(
resolvePluginConfigContractsById({
pluginIds: ["brave"],
}),
).toEqual(new Map());
expect(mocks.loadBundledManifestRegistry).not.toHaveBeenCalled();
});
it("hydrates supplied bundled registry records from explicit bundled discovery", () => {
mocks.loadBundledManifestRegistry.mockReturnValue(
createRegistry([
createPluginRecord({
id: "prepared-plugin",
origin: "bundled",
configContracts: {
secretInputs: {
paths: [{ path: "credentials.token", expected: "string" }],
},
},
}),
]),
);
expect(
resolvePluginConfigContractsById({
pluginIds: ["prepared-plugin"],
manifestRegistry: createRegistry([
createPluginRecord({ id: "prepared-plugin", origin: "bundled" }),
]),
fallbackToBundledMetadata: true,
fallbackToBundledMetadataForResolvedBundled: true,
fallbackBundledPluginIds: ["prepared-plugin"],
}),
).toEqual(
new Map([
[
"prepared-plugin",
{
origin: "bundled",
configContracts: {
secretInputs: {
paths: [{ path: "credentials.token", expected: "string" }],
},
},
},
],
]),
);
});
it("can hydrate missing contracts from bundled registry for resolved bundled plugins", () => {
mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue(
createRegistry([
createPluginRecord({
id: "voice-call",
origin: "bundled",
configContracts: {
compatibilityMigrationPaths: ["plugins.entries.voice-call.config"],
},
}),
]),
);
mocks.loadBundledManifestRegistry.mockReturnValue(
createRegistry([
createPluginRecord({
id: "voice-call",
origin: "bundled",
configContracts: {
secretInputs: {
paths: [{ path: "twilio.authToken", expected: "string" }],
},
},
}),
]),
);
expect(
resolvePluginConfigContractsById({
pluginIds: ["voice-call"],
fallbackToBundledMetadataForResolvedBundled: true,
}),
).toEqual(
new Map([
[
"voice-call",
{
origin: "bundled",
configContracts: {
compatibilityMigrationPaths: ["plugins.entries.voice-call.config"],
secretInputs: {
paths: [{ path: "twilio.authToken", expected: "string" }],
},
},
},
],
]),
);
expect(mocks.loadPluginManifestRegistryForPluginRegistry).toHaveBeenCalledTimes(1);
expect(mocks.loadBundledManifestRegistry).toHaveBeenCalledTimes(1);
});
it("refreshes stale bundled SecretInput contracts from bundled registry", () => {
mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue(
createRegistry([
createPluginRecord({
id: "voice-call",
origin: "bundled",
configContracts: {
compatibilityMigrationPaths: ["plugins.entries.voice-call.config"],
secretInputs: {
paths: [{ path: "twilio.authToken", expected: "string" }],
},
},
}),
]),
);
mocks.loadBundledManifestRegistry.mockReturnValue(
createRegistry([
createPluginRecord({
id: "voice-call",
origin: "bundled",
configContracts: {
secretInputs: {
paths: [
{ path: "twilio.authToken", expected: "string" },
{ path: "realtime.providers.*.apiKey", expected: "string" },
],
},
},
}),
]),
);
expect(
resolvePluginConfigContractsById({
pluginIds: ["voice-call"],
fallbackToBundledMetadataForResolvedBundled: true,
}),
).toEqual(
new Map([
[
"voice-call",
{
origin: "bundled",
configContracts: {
compatibilityMigrationPaths: ["plugins.entries.voice-call.config"],
secretInputs: {
paths: [
{ path: "twilio.authToken", expected: "string" },
{ path: "realtime.providers.*.apiKey", expected: "string" },
],
},
},
},
],
]),
);
});
it("can hydrate missing contracts for plugin ids known to be bundled by runtime discovery", () => {
mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue(
createRegistry([
createPluginRecord({
id: "voice-call",
origin: "config",
}),
]),
);
mocks.loadBundledManifestRegistry.mockReturnValue(
createRegistry([
createPluginRecord({
id: "voice-call",
origin: "bundled",
configContracts: {
secretInputs: {
paths: [{ path: "tts.providers.*.apiKey", expected: "string" }],
},
},
}),
]),
);
expect(
resolvePluginConfigContractsById({
pluginIds: ["voice-call"],
fallbackBundledPluginIds: ["voice-call"],
}),
).toEqual(
new Map([
[
"voice-call",
{
origin: "bundled",
configContracts: {
secretInputs: {
paths: [{ path: "tts.providers.*.apiKey", expected: "string" }],
},
},
},
],
]),
);
});
it("can skip bundled metadata fallback for registry-scoped callers", () => {
expect(
resolvePluginConfigContractsById({
pluginIds: ["missing"],
fallbackToBundledMetadata: false,
}),
).toEqual(new Map());
expect(mocks.loadBundledManifestRegistry).not.toHaveBeenCalled();
});
});
describe("collectPluginConfigContractMatches", () => {
it("only accepts canonical array index path segments", () => {
const root = { items: ["first", "second"] };
expect(
collectPluginConfigContractMatches({
root,
pathPattern: "items.1",
}),
).toEqual([{ path: "items[1]", value: "second" }]);
expect(
collectPluginConfigContractMatches({
root,
pathPattern: "items.1.5",
}),
).toEqual([]);
expect(
collectPluginConfigContractMatches({
root,
pathPattern: "items.01",
}),
).toEqual([]);
});
it("rejects array indexes outside canonical config path bounds", () => {
const items = Array<string>(100_002);
items[100_001] = "too far";
expect(
collectPluginConfigContractMatches({
root: { items },
pathPattern: "items.100001",
}),
).toEqual([]);
});
});