Tighten managed skill reconcile behavior

This commit is contained in:
Dotta committed 2026-05-09 23:40:41 -05:00
1 parent ce42fb3c97
commit 4de53ef96f
4 files changed
+65

No files matched your search

+1
View File
@@ -1275,6 +1275,7 @@ export function createTestHarness(options: TestHarnessOptions): TestHarness {
} satisfies PluginManagedSkillResolution;
},
async reset(skillKey, companyId) {
requireCapability(manifest, capabilitySet, "skills.managed");
const existing = await this.get(skillKey, companyId);
const declaration = manifest.skills?.find((skill) => skill.skillKey === skillKey);
if (!declaration) return existing;
@@ -174,6 +174,25 @@ describeEmbeddedPostgres("plugin-managed skills", () => {
expect(reset.skill?.markdown).toContain("key: \"plugin/paperclip-managed-skills-test/wiki-maintainer\"");
});
it("does not rewrite managed skill bindings when defaults are unchanged", async () => {
const { companyId, services } = await seedCompanyAndPlugin();
const created = await services.skills.managedReconcile({ companyId, skillKey: "wiki-maintainer" });
expect(created.skillId).toBeTruthy();
const [binding] = await db.select().from(pluginManagedResources);
const oldUpdatedAt = new Date("2026-01-01T00:00:00.000Z");
await db
.update(pluginManagedResources)
.set({ updatedAt: oldUpdatedAt })
.where(eq(pluginManagedResources.id, binding.id));
const reconciled = await services.skills.managedReconcile({ companyId, skillKey: "wiki-maintainer" });
const [bindingAfter] = await db.select().from(pluginManagedResources);
expect(reconciled.status).toBe("resolved");
expect(bindingAfter.updatedAt.toISOString()).toBe(oldUpdatedAt.toISOString());
});
it("relinks an existing canonical skill without overwriting operator edits", async () => {
const { companyId, services } = await seedCompanyAndPlugin();
const created = await services.skills.managedReconcile({ companyId, skillKey: "wiki-maintainer" });
@@ -0,0 +1,28 @@
import { describe, expect, it } from "vitest";
import type { PaperclipPluginManifestV1 } from "@paperclipai/shared";
import { createTestHarness } from "@paperclipai/plugin-sdk/testing";
describe("plugin SDK test harness", () => {
it("requires skills.managed capability before resetting a missing declaration", async () => {
const manifest: PaperclipPluginManifestV1 = {
id: "paperclip.test-missing-managed-skill-capability",
apiVersion: 1,
version: "0.1.0",
displayName: "Missing Managed Skill Capability",
description: "Test plugin",
author: "Paperclip",
categories: ["automation"],
capabilities: [],
entrypoints: { worker: "./dist/worker.js" },
skills: [{
skillKey: "wiki-maintainer",
displayName: "Wiki Maintainer",
}],
};
const harness = createTestHarness({ manifest });
await expect(harness.ctx.skills.managed.reset("unknown-skill", "company-1")).rejects.toThrow(
"missing required capability 'skills.managed'",
);
});
});
@@ -122,6 +122,17 @@ function buildSkillDefaults(
};
}
function stableJson(value: unknown): string {
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
if (value && typeof value === "object") {
return `{${Object.entries(value as Record<string, unknown>)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`)
.join(",")}}`;
}
return JSON.stringify(value);
}
function resolution(
pluginKey: string,
companyId: string,
@@ -177,6 +188,12 @@ export function pluginManagedSkillService(
const defaultsJson = buildSkillDefaults(options.pluginKey, declaration);
const existing = await getBinding(companyId, declaration.skillKey);
if (existing) {
if (
existing.resourceId === skillId &&
stableJson(existing.defaultsJson) === stableJson(defaultsJson)
) {
return existing;
}
return db
.update(pluginManagedResources)
.set({