Files
kandev/apps/web/lib/plugins/registry-provider-lifecycle.test.ts
2026-09-15 19:05:40 +01:00

287 lines
10 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
import { pluginRegistry } from "./registry";
import type { RepositoryProviderRegistration } from "./types";
const PRIMARY_PLUGIN_ID = "plugin-a";
const SOURCE_CONTROL_PROVIDER_ID = "source-control";
const WORKSPACE_ID = "workspace-a";
function repositoryProvider(
overrides: Partial<RepositoryProviderRegistration> = {},
): RepositoryProviderRegistration {
return {
id: SOURCE_CONTROL_PROVIDER_ID,
label: SOURCE_CONTROL_PROVIDER_ID,
listRepositories: async () => [],
matchesURL: () => false,
listBranches: async () => [],
inspectURL: async () => null,
...overrides,
};
}
afterEach(() => {
pluginRegistry.unregisterPlugin(PRIMARY_PLUGIN_ID);
});
describe("pluginRegistry — repository provider result lifecycle", () => {
it("binds repository inspection identity to the registered provider", async () => {
const spoofed = {
providerId: "spoofed-provider",
providerHost: "code.example.test",
ownerOrProject: "team",
repositoryId: "team/app",
repositoryName: "app",
cloneUrl: "https://code.example.test/team/app.git",
};
pluginRegistry.forPlugin(PRIMARY_PLUGIN_ID).registerRepositoryProvider(
repositoryProvider({
listRepositories: async () => [spoofed],
inspectURL: async () => spoofed,
}),
);
const provider = pluginRegistry.getRepositoryProvider(SOURCE_CONTROL_PROVIDER_ID)!;
const signal = new AbortController().signal;
await expect(provider.listRepositories({ workspaceId: WORKSPACE_ID, signal })).resolves.toEqual(
[{ ...spoofed, providerId: SOURCE_CONTROL_PROVIDER_ID }],
);
await expect(
provider.inspectURL({ workspaceId: WORKSPACE_ID, url: spoofed.cloneUrl, signal }),
).resolves.toEqual({ ...spoofed, providerId: SOURCE_CONTROL_PROVIDER_ID });
});
it("aborts in-flight repository work when its owner unloads", async () => {
const aborted = vi.fn();
let markStarted: () => void;
const started = new Promise<void>((resolve) => {
markStarted = resolve;
});
const provider = repositoryProvider({
listRepositories: ({ signal }) =>
new Promise((_, reject) => {
markStarted();
signal.addEventListener(
"abort",
() => {
aborted();
reject(new Error("provider request aborted"));
},
{ once: true },
);
}),
});
pluginRegistry.forPlugin(PRIMARY_PLUGIN_ID).registerRepositoryProvider(provider);
const request = pluginRegistry
.getRepositoryProvider(SOURCE_CONTROL_PROVIDER_ID)
?.listRepositories({ workspaceId: WORKSPACE_ID, signal: new AbortController().signal });
await started;
pluginRegistry.unregisterPlugin(PRIMARY_PLUGIN_ID);
await expect(request).rejects.toMatchObject({ name: "AbortError" });
expect(aborted).toHaveBeenCalledOnce();
});
it("rejects stale results from a provider that ignores cancellation", async () => {
let resolveRequest!: (
value: Awaited<ReturnType<RepositoryProviderRegistration["listRepositories"]>>,
) => void;
const provider = repositoryProvider({
listRepositories: () => new Promise((resolve) => (resolveRequest = resolve)),
});
pluginRegistry.forPlugin(PRIMARY_PLUGIN_ID).registerRepositoryProvider(provider);
const request = pluginRegistry
.getRepositoryProvider(SOURCE_CONTROL_PROVIDER_ID)!
.listRepositories({ workspaceId: WORKSPACE_ID, signal: new AbortController().signal });
await Promise.resolve();
pluginRegistry.unregisterPlugin(PRIMARY_PLUGIN_ID);
resolveRequest([]);
await expect(request).rejects.toMatchObject({ name: "AbortError" });
});
it("does not start provider work for an already-aborted request", async () => {
const listRepositories = vi.fn(async () => []);
pluginRegistry
.forPlugin(PRIMARY_PLUGIN_ID)
.registerRepositoryProvider(repositoryProvider({ listRepositories }));
const controller = new AbortController();
controller.abort();
await expect(
pluginRegistry
.getRepositoryProvider(SOURCE_CONTROL_PROVIDER_ID)!
.listRepositories({ workspaceId: WORKSPACE_ID, signal: controller.signal }),
).rejects.toMatchObject({ name: "AbortError" });
expect(listRepositories).not.toHaveBeenCalled();
});
});
describe("pluginRegistry — atomic unload and work lifecycle", () => {
it("does not abort in-flight provider work when an atomic unload rolls back", async () => {
let release!: () => void;
const gate = new Promise<void>((resolve) => {
release = resolve;
});
const provider = repositoryProvider({
listRepositories: ({ signal }) =>
new Promise((resolve, reject) => {
signal.addEventListener(
"abort",
() => reject(new DOMException("aborted", "AbortError")),
{ once: true },
);
gate.then(() => resolve([]));
}),
});
pluginRegistry.forPlugin(PRIMARY_PLUGIN_ID).registerRepositoryProvider(provider);
const request = pluginRegistry
.getRepositoryProvider(SOURCE_CONTROL_PROVIDER_ID)!
.listRepositories({ workspaceId: WORKSPACE_ID, signal: new AbortController().signal });
await Promise.resolve();
expect(() =>
pluginRegistry.runAtomicMutation(() => {
pluginRegistry.unregisterPlugin(PRIMARY_PLUGIN_ID);
throw new Error("staged commit failed");
}),
).toThrow("staged commit failed");
release();
await expect(request).resolves.toEqual([]);
});
it("aborts in-flight provider work once an atomic unload commits", async () => {
const aborted = vi.fn();
const provider = repositoryProvider({
listRepositories: ({ signal }) =>
new Promise((_, reject) => {
signal.addEventListener(
"abort",
() => {
aborted();
reject(new DOMException("aborted", "AbortError"));
},
{ once: true },
);
}),
});
pluginRegistry.forPlugin(PRIMARY_PLUGIN_ID).registerRepositoryProvider(provider);
const request = pluginRegistry
.getRepositoryProvider(SOURCE_CONTROL_PROVIDER_ID)!
.listRepositories({ workspaceId: WORKSPACE_ID, signal: new AbortController().signal });
await Promise.resolve();
pluginRegistry.runAtomicMutation(() => {
pluginRegistry.unregisterPlugin(PRIMARY_PLUGIN_ID);
});
await expect(request).rejects.toMatchObject({ name: "AbortError" });
expect(aborted).toHaveBeenCalledOnce();
});
});
describe("pluginRegistry — repository provider reload lifecycle", () => {
it("keeps re-enabled provider work tracked after an older request settles", async () => {
let rejectFirst!: (error: Error) => void;
let markFirstStarted!: () => void;
const firstStarted = new Promise<void>((resolve) => {
markFirstStarted = resolve;
});
const first = repositoryProvider({
listRepositories: () =>
new Promise((_, reject) => {
rejectFirst = reject;
markFirstStarted();
}),
});
pluginRegistry.forPlugin(PRIMARY_PLUGIN_ID).registerRepositoryProvider(first);
const firstRequest = pluginRegistry
.getRepositoryProvider(SOURCE_CONTROL_PROVIDER_ID)!
.listRepositories({ workspaceId: WORKSPACE_ID, signal: new AbortController().signal });
await firstStarted;
pluginRegistry.unregisterPlugin(PRIMARY_PLUGIN_ID);
const secondAborted = vi.fn();
let markSecondStarted!: () => void;
const secondStarted = new Promise<void>((resolve) => {
markSecondStarted = resolve;
});
const second = repositoryProvider({
listRepositories: ({ signal }) =>
new Promise((_, reject) => {
markSecondStarted();
signal.addEventListener(
"abort",
() => {
secondAborted();
reject(new Error("second provider request aborted"));
},
{ once: true },
);
}),
});
pluginRegistry.forPlugin(PRIMARY_PLUGIN_ID).registerRepositoryProvider(second);
const secondRequest = pluginRegistry
.getRepositoryProvider(SOURCE_CONTROL_PROVIDER_ID)!
.listRepositories({ workspaceId: WORKSPACE_ID, signal: new AbortController().signal });
await secondStarted;
rejectFirst(new Error("first provider request stopped"));
await expect(firstRequest).rejects.toThrow();
pluginRegistry.unregisterPlugin(PRIMARY_PLUGIN_ID);
await expect(secondRequest).rejects.toMatchObject({ name: "AbortError" });
expect(secondAborted).toHaveBeenCalledOnce();
});
});
describe("pluginRegistry — repository change-request lifecycle", () => {
it("aborts provider change-request creation when its owner unloads", async () => {
let markStarted!: () => void;
const started = new Promise<void>((resolve) => (markStarted = resolve));
const aborted = vi.fn();
pluginRegistry.forPlugin(PRIMARY_PLUGIN_ID).registerRepositoryProvider(
repositoryProvider({
createChangeRequest: ({ signal }) =>
new Promise((_, reject) => {
markStarted();
signal.addEventListener(
"abort",
() => {
aborted();
reject(new Error("create aborted"));
},
{ once: true },
);
}),
}),
);
const provider = pluginRegistry.getRepositoryProvider(SOURCE_CONTROL_PROVIDER_ID)!;
const request = provider.createChangeRequest!({
workspaceId: WORKSPACE_ID,
taskId: "task-a",
sessionId: "session-a",
repositoryId: "repo-a",
repository: {
id: "repo-a",
workspace_id: WORKSPACE_ID,
name: "api",
provider: SOURCE_CONTROL_PROVIDER_ID,
},
title: "Title",
body: "",
draft: false,
signal: new AbortController().signal,
});
await started;
pluginRegistry.unregisterPlugin(PRIMARY_PLUGIN_ID);
await expect(request).rejects.toMatchObject({ name: "AbortError" });
expect(aborted).toHaveBeenCalledOnce();
});
});