feat(provider): add adapter contract routing

This commit is contained in:
2026-09-15 04:04:41 +08:00
parent 05493ead7d
commit ec998005ac
12 changed files with 266 additions and 20 deletions
+1
View File
@@ -44,6 +44,7 @@ test("all supported providers expose the adapter contract", () => {
assert.equal(typeof adapters[id].sendPrompt, "function");
assert.equal(typeof adapters[id].collectResponse, "function");
assert.equal(typeof adapters[id].newChat, "function");
assert.equal(typeof adapters[id].healthCheck, "function");
}
});
+94
View File
@@ -0,0 +1,94 @@
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const test = require("node:test");
const vm = require("node:vm");
const extensionRoot = path.join(__dirname, "..", "apps", "browser-extension");
function loadContract() {
const context = {};
context.globalThis = context;
vm.createContext(context);
vm.runInContext(
fs.readFileSync(path.join(extensionRoot, "shared", "provider-adapter-contract.js"), "utf8"),
context,
{ filename: "shared/provider-adapter-contract.js" }
);
vm.runInContext(
fs.readFileSync(path.join(extensionRoot, "shared", "provider-catalog.js"), "utf8"),
context,
{ filename: "shared/provider-catalog.js" }
);
return {
contract: context.AIParallelProviderAdapterContract,
catalog: context.AIParallelProviderCatalog
};
}
test("provider adapter contract exposes the required capability methods", () => {
const { contract } = loadContract();
assert.equal(contract.CONTRACT_VERSION, "provider-adapter-v1");
assert.deepEqual([...contract.REQUIRED_METHODS], [
"sendPrompt",
"collectResponse",
"newChat",
"healthCheck"
]);
});
test("catalog adapters route every operation through the shared transport", async () => {
const { contract, catalog } = loadContract();
const calls = [];
const transport = {
request(provider, operation, payload, context) {
calls.push({ providerId: provider.id, operation, payload, context });
return Promise.resolve({ ok: true, operation });
},
healthCheck(provider, context) {
calls.push({ providerId: provider.id, operation: "healthCheck", context });
return Promise.resolve({ ok: true, providerId: provider.id });
}
};
for (const provider of catalog) {
const adapter = contract.createProviderAdapter({ provider, transport });
assert.equal(adapter.id, provider.adapter);
assert.equal(adapter.providerId, provider.id);
assert.equal(contract.validateProviderAdapter(adapter), true);
await adapter.sendPrompt(" hello ", { attempt: 1 });
await adapter.collectResponse({ attempt: 1 });
await adapter.newChat({ attempt: 1 });
await adapter.healthCheck({ attempt: 1 });
}
assert.equal(calls.length, catalog.length * 4);
assert.equal(calls.filter(({ operation }) => operation === "AI_PARALLEL_SEND").length, catalog.length);
assert.equal(calls.filter(({ operation }) => operation === "AI_PARALLEL_COLLECT_RESPONSE").length, catalog.length);
assert.equal(calls.filter(({ operation }) => operation === "AI_PARALLEL_NEW_CHAT").length, catalog.length);
assert.equal(calls.filter(({ operation }) => operation === "healthCheck").length, catalog.length);
assert.equal(calls[0].payload.prompt, "hello");
});
test("provider adapter rejects an empty prompt without invoking transport", async () => {
const { contract, catalog } = loadContract();
let called = false;
const adapter = contract.createProviderAdapter({
provider: catalog[0],
transport: {
request() {
called = true;
return Promise.resolve({ ok: true });
},
healthCheck() {
return Promise.resolve({ ok: true });
}
}
});
const result = await adapter.sendPrompt(" \n ");
assert.equal(result.ok, false);
assert.equal(result.code, "INVALID_PROMPT");
assert.equal(called, false);
});
+4 -3
View File
@@ -40,8 +40,9 @@ test("shared provider catalog matches manifest host coverage", () => {
assert.ok(contentScriptMatches.has(match), `${provider.id} missing content-script match ${match}`);
}
assert.equal(new URL(provider.url).origin, provider.origins[0]);
assert.equal(provider.adapter.type, "dom");
assert.equal(provider.adapter.contract, "provider-adapter-v1");
assert.equal(provider.adapter, provider.id);
assert.equal(provider.adapterType, "dom");
assert.equal(provider.adapterContract, "provider-adapter-v1");
assert.equal(provider.capabilities.send, true);
assert.equal(provider.capabilities.collect, true);
assert.equal(provider.capabilities.newChat, true);
@@ -58,7 +59,6 @@ test("shared provider catalog protects metadata arrays from mutation", () => {
assert.equal(Object.isFrozen(catalog[0]), true);
assert.equal(Object.isFrozen(catalog[0].hosts), true);
assert.equal(Object.isFrozen(catalog[0].origins), true);
assert.equal(Object.isFrozen(catalog[0].adapter), true);
assert.equal(Object.isFrozen(catalog[0].capabilities), true);
});
@@ -69,6 +69,7 @@ test("extension entry points load the catalog before consuming it", () => {
assert.match(popupHtml, /shared\/provider-catalog\.js[\s\S]*popup\.js/);
assert.match(workspaceHtml, /\.\.\/shared\/provider-catalog\.js[\s\S]*workspace\.js/);
assert.match(workspaceHtml, /\.\.\/shared\/provider-adapter-contract\.js[\s\S]*workspace\.js/);
assert.match(workspaceHtml, /\.\.\/shared\/provider-task-runtime\.js[\s\S]*workspace\.js/);
assert.match(serviceWorker, /^importScripts\("shared\/provider-catalog\.js"\);/);
});