forked from farhoodlabs/paperclip
fix(adapters): honor paused overrides and isolate UI parser state
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
import { describe, expect, it, beforeEach, afterEach } from "vitest";
|
||||
import { describe, expect, it, beforeEach, afterEach, vi } from "vitest";
|
||||
import type { ServerAdapterModule } from "../adapters/index.js";
|
||||
import {
|
||||
detectAdapterModel,
|
||||
findActiveServerAdapter,
|
||||
findServerAdapter,
|
||||
listAdapterModels,
|
||||
registerServerAdapter,
|
||||
requireServerAdapter,
|
||||
unregisterServerAdapter,
|
||||
} from "../adapters/index.js";
|
||||
import { setOverridePaused } from "../adapters/registry.js";
|
||||
|
||||
const externalAdapter: ServerAdapterModule = {
|
||||
type: "external_test",
|
||||
@@ -28,10 +31,14 @@ const externalAdapter: ServerAdapterModule = {
|
||||
describe("server adapter registry", () => {
|
||||
beforeEach(() => {
|
||||
unregisterServerAdapter("external_test");
|
||||
unregisterServerAdapter("claude_local");
|
||||
setOverridePaused("claude_local", false);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
unregisterServerAdapter("external_test");
|
||||
unregisterServerAdapter("claude_local");
|
||||
setOverridePaused("claude_local", false);
|
||||
});
|
||||
|
||||
it("registers external adapters and exposes them through lookup helpers", async () => {
|
||||
@@ -87,4 +94,50 @@ describe("server adapter registry", () => {
|
||||
{ id: "plugin-model", label: "Plugin Override" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("switches active adapter behavior back to the builtin when an override is paused", async () => {
|
||||
const builtIn = findServerAdapter("claude_local");
|
||||
expect(builtIn).not.toBeNull();
|
||||
|
||||
const detectModel = vi.fn(async () => ({
|
||||
model: "plugin-model",
|
||||
provider: "plugin-provider",
|
||||
source: "plugin-source",
|
||||
}));
|
||||
const plugin: ServerAdapterModule = {
|
||||
type: "claude_local",
|
||||
execute: async () => ({
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
timedOut: false,
|
||||
}),
|
||||
testEnvironment: async () => ({
|
||||
adapterType: "claude_local",
|
||||
status: "pass",
|
||||
checks: [],
|
||||
testedAt: new Date(0).toISOString(),
|
||||
}),
|
||||
models: [{ id: "plugin-model", label: "Plugin Override" }],
|
||||
detectModel,
|
||||
supportsLocalAgentJwt: false,
|
||||
};
|
||||
|
||||
registerServerAdapter(plugin);
|
||||
|
||||
expect(findActiveServerAdapter("claude_local")).toBe(plugin);
|
||||
expect(await listAdapterModels("claude_local")).toEqual([
|
||||
{ id: "plugin-model", label: "Plugin Override" },
|
||||
]);
|
||||
expect(await detectAdapterModel("claude_local")).toMatchObject({
|
||||
model: "plugin-model",
|
||||
provider: "plugin-provider",
|
||||
});
|
||||
|
||||
expect(setOverridePaused("claude_local", true)).toBe(true);
|
||||
|
||||
expect(findActiveServerAdapter("claude_local")).not.toBe(plugin);
|
||||
expect(await listAdapterModels("claude_local")).toEqual(builtIn?.models ?? []);
|
||||
expect(await detectAdapterModel("claude_local")).toBeNull();
|
||||
expect(detectModel).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import express from "express";
|
||||
import request from "supertest";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import type { ServerAdapterModule } from "../adapters/index.js";
|
||||
import { registerServerAdapter, unregisterServerAdapter } from "../adapters/index.js";
|
||||
import { setOverridePaused } from "../adapters/registry.js";
|
||||
import { adapterRoutes } from "../routes/adapters.js";
|
||||
import { errorHandler } from "../middleware/index.js";
|
||||
|
||||
const overridingConfigSchemaAdapter: ServerAdapterModule = {
|
||||
type: "claude_local",
|
||||
execute: async () => ({ exitCode: 0, signal: null, timedOut: false }),
|
||||
testEnvironment: async () => ({
|
||||
adapterType: "claude_local",
|
||||
status: "pass",
|
||||
checks: [],
|
||||
testedAt: new Date(0).toISOString(),
|
||||
}),
|
||||
getConfigSchema: async () => ({
|
||||
version: 1,
|
||||
fields: [
|
||||
{
|
||||
key: "mode",
|
||||
type: "text",
|
||||
label: "Mode",
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
function createApp() {
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use((req, _res, next) => {
|
||||
(req as any).actor = {
|
||||
type: "board",
|
||||
userId: "local-board",
|
||||
companyIds: [],
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: false,
|
||||
};
|
||||
next();
|
||||
});
|
||||
app.use("/api", adapterRoutes());
|
||||
app.use(errorHandler);
|
||||
return app;
|
||||
}
|
||||
|
||||
describe("adapter routes", () => {
|
||||
beforeEach(() => {
|
||||
setOverridePaused("claude_local", false);
|
||||
registerServerAdapter(overridingConfigSchemaAdapter);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setOverridePaused("claude_local", false);
|
||||
unregisterServerAdapter("claude_local");
|
||||
});
|
||||
|
||||
it("uses the active adapter when resolving config schema for a paused builtin override", async () => {
|
||||
const app = createApp();
|
||||
|
||||
const active = await request(app).get("/api/adapters/claude_local/config-schema");
|
||||
expect(active.status, JSON.stringify(active.body)).toBe(200);
|
||||
expect(active.body).toMatchObject({
|
||||
fields: [{ key: "mode" }],
|
||||
});
|
||||
|
||||
const paused = await request(app)
|
||||
.patch("/api/adapters/claude_local/override")
|
||||
.send({ paused: true });
|
||||
expect(paused.status, JSON.stringify(paused.body)).toBe(200);
|
||||
|
||||
const builtin = await request(app).get("/api/adapters/claude_local/config-schema");
|
||||
expect(builtin.status, JSON.stringify(builtin.body)).toBe(404);
|
||||
expect(String(builtin.body.error ?? "")).toContain("does not provide a config schema");
|
||||
});
|
||||
});
|
||||
@@ -61,6 +61,7 @@ const mockAdapter = vi.hoisted(() => ({
|
||||
|
||||
vi.mock("@paperclipai/shared/telemetry", () => ({
|
||||
trackAgentCreated: mockTrackAgentCreated,
|
||||
trackErrorHandlerCrash: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../telemetry.js", () => ({
|
||||
@@ -85,6 +86,7 @@ vi.mock("../services/index.js", () => ({
|
||||
|
||||
vi.mock("../adapters/index.js", () => ({
|
||||
findServerAdapter: vi.fn(() => mockAdapter),
|
||||
findActiveServerAdapter: vi.fn(() => mockAdapter),
|
||||
listAdapterModels: vi.fn(),
|
||||
detectAdapterModel: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -4,14 +4,14 @@ import { notifyHireApproved } from "../services/hire-hook.js";
|
||||
|
||||
// Mock the registry so we control whether the adapter has onHireApproved and what it does.
|
||||
vi.mock("../adapters/registry.js", () => ({
|
||||
findServerAdapter: vi.fn(),
|
||||
findActiveServerAdapter: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../services/activity-log.js", () => ({
|
||||
logActivity: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const { findServerAdapter } = await import("../adapters/registry.js");
|
||||
const { findActiveServerAdapter } = await import("../adapters/registry.js");
|
||||
const { logActivity } = await import("../services/activity-log.js");
|
||||
|
||||
function mockDbWithAgent(agent: { id: string; companyId: string; name: string; adapterType: string; adapterConfig?: Record<string, unknown> }): Db {
|
||||
@@ -39,7 +39,7 @@ afterEach(() => {
|
||||
|
||||
describe("notifyHireApproved", () => {
|
||||
it("writes success activity when adapter hook returns ok", async () => {
|
||||
vi.mocked(findServerAdapter).mockReturnValue({
|
||||
vi.mocked(findActiveServerAdapter).mockReturnValue({
|
||||
type: "openclaw_gateway",
|
||||
onHireApproved: vi.fn().mockResolvedValue({ ok: true }),
|
||||
} as any);
|
||||
@@ -88,11 +88,11 @@ describe("notifyHireApproved", () => {
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(findServerAdapter).not.toHaveBeenCalled();
|
||||
expect(findActiveServerAdapter).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does nothing when adapter has no onHireApproved", async () => {
|
||||
vi.mocked(findServerAdapter).mockReturnValue({ type: "process" } as any);
|
||||
vi.mocked(findActiveServerAdapter).mockReturnValue({ type: "process" } as any);
|
||||
|
||||
const db = mockDbWithAgent({
|
||||
id: "a1",
|
||||
@@ -110,12 +110,12 @@ describe("notifyHireApproved", () => {
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(findServerAdapter).toHaveBeenCalledWith("process");
|
||||
expect(findActiveServerAdapter).toHaveBeenCalledWith("process");
|
||||
expect(logActivity).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("logs failed result when adapter onHireApproved returns ok=false", async () => {
|
||||
vi.mocked(findServerAdapter).mockReturnValue({
|
||||
vi.mocked(findActiveServerAdapter).mockReturnValue({
|
||||
type: "openclaw_gateway",
|
||||
onHireApproved: vi.fn().mockResolvedValue({ ok: false, error: "HTTP 500", detail: { status: 500 } }),
|
||||
} as any);
|
||||
@@ -147,7 +147,7 @@ describe("notifyHireApproved", () => {
|
||||
});
|
||||
|
||||
it("does not throw when adapter onHireApproved throws (non-fatal)", async () => {
|
||||
vi.mocked(findServerAdapter).mockReturnValue({
|
||||
vi.mocked(findActiveServerAdapter).mockReturnValue({
|
||||
type: "openclaw_gateway",
|
||||
onHireApproved: vi.fn().mockRejectedValue(new Error("Network error")),
|
||||
} as any);
|
||||
|
||||
Reference in New Issue
Block a user