// Cross-target: updating an OpenAPI spec IN PLACE — the "our API changed,// don't make me remove/re-add the integration" promise. A real 127.0.0.1// server serves the spec; after registration + connection the spec evolves// (one operation removed, one added) and `openapi.updateSpec` re-fetches it.// The tool catalog follows, the response reports the diff, and everything// user-curated (connection, description, auth template) survives.import { randomBytes, randomUUID } from "node:crypto";import { createServer } from "node:http"; import { expect } from "@effect/vitest";import { Effect } from "effect";import { composePluginApi } from "@executor-js/api/server";import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";import { AuthTemplateSlug, ConnectionName, IntegrationSlug, ProviderItemId,} from "@executor-js/sdk/shared"; import { scenario } from "../src/scenario";import { Api, Target } from "../src/services"; const api = composePluginApi([openApiHttpPlugin()] as const); const specV1 = JSON.stringify({ openapi: "3.0.3", info: { title: "Evolving API", version: "1.0.0" }, paths: { "/ping": { get: { operationId: "ping", summary: "Return a pong", responses: { "200": { description: "pong" } }, }, }, "/legacy": { get: { operationId: "legacyOp", summary: "Soon to be removed", responses: { "200": { description: "ok" } }, }, }, },}); const specV2 = JSON.stringify({ openapi: "3.0.3", info: { title: "Evolving API", version: "2.0.0" }, paths: { "/ping": { get: { operationId: "ping", summary: "Return a pong", responses: { "200": { description: "pong" } }, }, }, "/widgets": { get: { operationId: "listWidgets", summary: "List widgets", responses: { "200": { description: "widgets" } }, }, }, },}); /** A real 127.0.0.1 server whose served spec can be swapped mid-scenario. */const serveMutableSpec = (initial: string) => Effect.acquireRelease( Effect.callback<{ readonly url: string; readonly setBody: (body: string) => void; readonly close: () => void; }>((resume) => { let body = initial; const server = createServer((_request, response) => { response.writeHead(200, { "content-type": "application/json" }); response.end(body); }); server.listen(0, "127.0.0.1", () => { const address = server.address(); const port = typeof address === "object" && address ? address.port : 0; resume( Effect.succeed({ url: `http://127.0.0.1:${port}/spec.json`, setBody: (next: string) => { body = next; }, close: () => { server.close(); server.closeAllConnections(); }, }), ); }); }), (server) => Effect.sync(server.close), ); scenario( "OpenAPI · updating the spec rebuilds tools without re-adding the integration", {}, Effect.scoped( Effect.gen(function* () { const target = yield* Target; const { client } = yield* Api; const identity = yield* target.newIdentity(); const apiClient = yield* client(api, identity); const slug = `update-spec-${randomBytes(4).toString("hex")}`; const specServer = yield* serveMutableSpec(specV1); yield* Effect.ensuring( Effect.gen(function* () { const added = yield* apiClient.openapi.addSpec({ payload: { spec: { kind: "url", url: specServer.url }, slug, baseUrl: "http://127.0.0.1:59999", // tools are never invoked here description: "Curated description that must survive the update.", authenticationTemplate: [ { slug: "apiKey", type: "apiKey", headers: { "x-api-key": [{ type: "variable", name: "token" }] }, }, ], }, }); expect(added.toolCount, "v1 spec has two operations").toBe(2); const providers = yield* apiClient.providers.list(); yield* apiClient.connections.create({ payload: { owner: "org", name: ConnectionName.make("main"), integration: IntegrationSlug.make(slug), template: AuthTemplateSlug.make("apiKey"), from: { provider: providers[0]!, id: ProviderItemId.make(randomUUID()) }, }, }); const toolNames = (filter: { integration: string }) => Effect.map( apiClient.tools.list({ query: { integration: IntegrationSlug.make(filter.integration) }, }), (tools) => tools.map((tool) => tool.name).sort(), ); expect(yield* toolNames({ integration: slug }), "v1 tools are live").toEqual([ "legacy.legacyOp", "ping.getOperation", ]); // The upstream API ships v2: legacyOp is gone, listWidgets appears. specServer.setBody(specV2); const updated = yield* apiClient.openapi.updateSpec({ params: { slug }, payload: {}, }); expect(updated.addedTools, "the diff names the new tool").toEqual([ "widgets.listWidgets", ]); expect(updated.removedTools, "the diff names the removed tool").toEqual([ "legacy.legacyOp", ]); // The connection's catalog follows the new spec — same connection, // no re-add. expect(yield* toolNames({ integration: slug }), "v2 tools are live").toEqual([ "ping.getOperation", "widgets.listWidgets", ]); const connections = yield* apiClient.connections.list({ query: { integration: IntegrationSlug.make(slug) }, }); expect( connections.map((connection) => String(connection.name)), "the connection survived", ).toEqual(["main"]); const integration = yield* apiClient.integrations.get({ params: { slug: IntegrationSlug.make(slug) }, }); expect(integration.description, "the curated description survived").toBe( "Curated description that must survive the update.", ); }), Effect.gen(function* () { yield* apiClient.connections .remove({ params: { owner: "org", integration: IntegrationSlug.make(slug), name: ConnectionName.make("main"), }, }) .pipe(Effect.ignore); yield* apiClient.openapi.removeSpec({ params: { slug } }).pipe(Effect.ignore); }), ); }), ),);
// Cross-target: updating an OpenAPI spec IN PLACE — the "our API changed,
// don't make me remove/re-add the integration" promise. A real 127.0.0.1
// server serves the spec; after registration + connection the spec evolves
// (one operation removed, one added) and `openapi.updateSpec` re-fetches it.
// The tool catalog follows, the response reports the diff, and everything
// user-curated (connection, description, auth template) survives.
import { randomBytes, randomUUID } from "node:crypto";
import { createServer } from "node:http";
import { expect } from "@effect/vitest";
import { Effect } from "effect";
import { composePluginApi } from "@executor-js/api/server";
import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";
import {
AuthTemplateSlug,
ConnectionName,
IntegrationSlug,
ProviderItemId,
} from "@executor-js/sdk/shared";
import { scenario } from "../src/scenario";
import { Api, Target } from "../src/services";
const api = composePluginApi([openApiHttpPlugin()] as const);
const specV1 = JSON.stringify({
openapi: "3.0.3",
info: { title: "Evolving API", version: "1.0.0" },
paths: {
"/ping": {
get: {
operationId: "ping",
summary: "Return a pong",
responses: { "200": { description: "pong" } },
},
},
"/legacy": {
get: {
operationId: "legacyOp",
summary: "Soon to be removed",
responses: { "200": { description: "ok" } },
},
},
},
});
const specV2 = JSON.stringify({
openapi: "3.0.3",
info: { title: "Evolving API", version: "2.0.0" },
paths: {
"/ping": {
get: {
operationId: "ping",
summary: "Return a pong",
responses: { "200": { description: "pong" } },
},
},
"/widgets": {
get: {
operationId: "listWidgets",
summary: "List widgets",
responses: { "200": { description: "widgets" } },
},
},
},
});
/** A real 127.0.0.1 server whose served spec can be swapped mid-scenario. */
const serveMutableSpec = (initial: string) =>
Effect.acquireRelease(
Effect.callback<{
readonly url: string;
readonly setBody: (body: string) => void;
readonly close: () => void;
}>((resume) => {
let body = initial;
const server = createServer((_request, response) => {
response.writeHead(200, { "content-type": "application/json" });
response.end(body);
});
server.listen(0, "127.0.0.1", () => {
const address = server.address();
const port = typeof address === "object" && address ? address.port : 0;
resume(
Effect.succeed({
url: `http://127.0.0.1:${port}/spec.json`,
setBody: (next: string) => {
body = next;
},
close: () => {
server.close();
server.closeAllConnections();
},
}),
);
});
}),
(server) => Effect.sync(server.close),
);
scenario(
"OpenAPI · updating the spec rebuilds tools without re-adding the integration",
{},
Effect.scoped(
Effect.gen(function* () {
const target = yield* Target;
const { client } = yield* Api;
const identity = yield* target.newIdentity();
const apiClient = yield* client(api, identity);
const slug = `update-spec-${randomBytes(4).toString("hex")}`;
const specServer = yield* serveMutableSpec(specV1);
yield* Effect.ensuring(
Effect.gen(function* () {
const added = yield* apiClient.openapi.addSpec({
payload: {
spec: { kind: "url", url: specServer.url },
slug,
baseUrl: "http://127.0.0.1:59999", // tools are never invoked here
description: "Curated description that must survive the update.",
authenticationTemplate: [
{
slug: "apiKey",
type: "apiKey",
headers: { "x-api-key": [{ type: "variable", name: "token" }] },
},
],
},
});
expect(added.toolCount, "v1 spec has two operations").toBe(2);
const providers = yield* apiClient.providers.list();
yield* apiClient.connections.create({
payload: {
owner: "org",
name: ConnectionName.make("main"),
integration: IntegrationSlug.make(slug),
template: AuthTemplateSlug.make("apiKey"),
from: { provider: providers[0]!, id: ProviderItemId.make(randomUUID()) },
},
});
const toolNames = (filter: { integration: string }) =>
Effect.map(
apiClient.tools.list({
query: { integration: IntegrationSlug.make(filter.integration) },
}),
(tools) => tools.map((tool) => tool.name).sort(),
);
expect(yield* toolNames({ integration: slug }), "v1 tools are live").toEqual([
"legacy.legacyOp",
"ping.getOperation",
]);
// The upstream API ships v2: legacyOp is gone, listWidgets appears.
specServer.setBody(specV2);
const updated = yield* apiClient.openapi.updateSpec({
params: { slug },
payload: {},
});
expect(updated.addedTools, "the diff names the new tool").toEqual([
"widgets.listWidgets",
]);
expect(updated.removedTools, "the diff names the removed tool").toEqual([
"legacy.legacyOp",
]);
// The connection's catalog follows the new spec — same connection,
// no re-add.
expect(yield* toolNames({ integration: slug }), "v2 tools are live").toEqual([
"ping.getOperation",
"widgets.listWidgets",
]);
const connections = yield* apiClient.connections.list({
query: { integration: IntegrationSlug.make(slug) },
});
expect(
connections.map((connection) => String(connection.name)),
"the connection survived",
).toEqual(["main"]);
const integration = yield* apiClient.integrations.get({
params: { slug: IntegrationSlug.make(slug) },
});
expect(integration.description, "the curated description survived").toBe(
"Curated description that must survive the update.",
);
}),
Effect.gen(function* () {
yield* apiClient.connections
.remove({
params: {
owner: "org",
integration: IntegrationSlug.make(slug),
name: ConnectionName.make("main"),
},
})
.pipe(Effect.ignore);
yield* apiClient.openapi.removeSpec({ params: { slug } }).pipe(Effect.ignore);
}),
);
}),
),
);