feat(adapters): external adapter plugin system with dynamic UI parser

- Plugin loader: install/reload/remove/reinstall external adapters
  from npm packages or local directories
- Plugin store persisted at ~/.paperclip/adapter-plugins.json
- Self-healing UI parser resolution with version caching
- UI: Adapter Manager page, dynamic loader, display registry
  with humanized names for unknown adapter types
- Dev watch: exclude adapter-plugins dir from tsx watcher
  to prevent mid-request server restarts during reinstall
- All consumer fallbacks use getAdapterLabel() for consistent display
- AdapterTypeDropdown uses controlled open state for proper close behavior
- Remove hermes-local from built-in UI (externalized to plugin)
- Add docs for external adapters and UI parser contract
This commit is contained in:
HenkDz 2026-03-31 20:21:13 +01:00
parent f8452a4520
commit 14d59da316
72 changed files with 4102 additions and 585 deletions

View file

@ -1,5 +1,8 @@
import { describe, expect, it } from "vitest";
import { summarizeHeartbeatRunResultJson } from "../services/heartbeat-run-summary.js";
import {
summarizeHeartbeatRunResultJson,
buildHeartbeatRunIssueComment,
} from "../services/heartbeat-run-summary.js";
describe("summarizeHeartbeatRunResultJson", () => {
it("truncates text fields and preserves cost aliases", () => {
@ -31,3 +34,24 @@ describe("summarizeHeartbeatRunResultJson", () => {
expect(summarizeHeartbeatRunResultJson({ nested: { only: "ignored" } })).toBeNull();
});
});
describe("buildHeartbeatRunIssueComment", () => {
it("uses the final summary text for issue comments on successful runs", () => {
const comment = buildHeartbeatRunIssueComment({
summary: "## Summary\n\n- fixed deploy config\n- posted issue update",
});
expect(comment).toContain("## Summary");
expect(comment).toContain("- fixed deploy config");
expect(comment).not.toContain("Run summary");
});
it("falls back to result or message when summary is missing", () => {
expect(buildHeartbeatRunIssueComment({ result: "done" })).toBe("done");
expect(buildHeartbeatRunIssueComment({ message: "completed" })).toBe("completed");
});
it("returns null when there is no usable final text", () => {
expect(buildHeartbeatRunIssueComment({ costUsd: 1.2 })).toBeNull();
});
});