136 lines · 5.2 KB · TypeScript
RawDownload
// Refresh endpoint — covers `sources.refresh(id)` for an OpenAPI// source added from a URL. Stands up a local HTTP server that serves// one of two spec versions (swappable mid-test) so we can verify the// refresh path re-fetches from the stored origin and replaces the// operation set. Raw-text sources assert the no-op branch. import { describe, expect, it } from "@effect/vitest";import { Effect, Schema } from "effect";import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"; import { ScopeId } from "@executor-js/sdk";import {  makeOpenApiHttpApiTestSpecPayload,  serveMutableOpenApiSpecTestServer,} from "@executor-js/plugin-openapi/testing"; import { asOrg } from "./__test-harness__/api-harness"; const PingEndpoint = HttpApiEndpoint.get("ping", "/ping", { success: Schema.Unknown });const PongEndpoint = HttpApiEndpoint.get("pong", "/pong", { success: Schema.Unknown }); const RefreshGroupV1 = HttpApiGroup.make("default", { topLevel: true }).add(PingEndpoint);const RefreshGroupV2 = HttpApiGroup.make("default", { topLevel: true })  .add(PingEndpoint)  .add(PongEndpoint); const refreshApi = (version: "1.0.0" | "2.0.0") =>  HttpApi.make("refreshFixture")    .add(version === "1.0.0" ? RefreshGroupV1 : RefreshGroupV2)    .annotateMerge(OpenApi.annotations({ title: "Refresh Fixture", version })); const makeRefreshSpecText = () => makeOpenApiHttpApiTestSpecPayload(refreshApi("1.0.0")).spec; describe("sources.refresh (HTTP)", () => {  it.effect("addSpec from URL → canRefresh:true; refresh re-fetches and updates tools", () =>    Effect.scoped(      Effect.gen(function* () {        const server = yield* serveMutableOpenApiSpecTestServer({          initialApi: refreshApi("1.0.0"),        });        const org = `org_${crypto.randomUUID()}`;        const namespace = `ns_${crypto.randomUUID().replace(/-/g, "_")}`;         yield* asOrg(org, (client) =>          client.openapi.addSpec({            params: { scopeId: ScopeId.make(org) },            payload: {              spec: { kind: "url", url: server.specUrl },              name: namespace,              baseUrl: server.baseUrl,              namespace,            },          }),        );         const before = yield* asOrg(org, (client) =>          client.sources.list({ params: { scopeId: ScopeId.make(org) } }),        );        const beforeSource = before.find((s) => s.id === namespace);        expect(beforeSource?.canRefresh).toBe(true);         const fetchedBefore = yield* asOrg(org, (client) =>          client.openapi.getSource({            params: { scopeId: ScopeId.make(org), namespace },          }),        );        expect(fetchedBefore?.config.sourceUrl).toBe(server.specUrl);         const beforeTools = yield* asOrg(org, (client) =>          client.sources.tools({            params: { scopeId: ScopeId.make(org), sourceId: namespace },          }),        );        expect(beforeTools.length).toBe(1);        expect(beforeTools.some((t) => t.id.endsWith(".default.ping"))).toBe(true);        expect(beforeTools.some((t) => t.id.endsWith(".default.pong"))).toBe(false);         // Flip the remote to v2 (adds `pong`) and trigger refresh.        yield* server.setApi(refreshApi("2.0.0"));        const requestsBefore = yield* server.requestCount;         const refreshResult = yield* asOrg(org, (client) =>          client.sources.refresh({            params: { scopeId: ScopeId.make(org), sourceId: namespace },          }),        );        expect(refreshResult.refreshed).toBe(true);        expect(yield* server.requestCount).toBeGreaterThan(requestsBefore);         const afterTools = yield* asOrg(org, (client) =>          client.sources.tools({            params: { scopeId: ScopeId.make(org), sourceId: namespace },          }),        );        expect(afterTools.length).toBe(2);        expect(afterTools.some((t) => t.id.endsWith(".default.ping"))).toBe(true);        expect(afterTools.some((t) => t.id.endsWith(".default.pong"))).toBe(true);      }),    ),  );   it.effect("addSpec from raw text → canRefresh:false; refresh is a no-op", () =>    Effect.gen(function* () {      const org = `org_${crypto.randomUUID()}`;      const namespace = `ns_${crypto.randomUUID().replace(/-/g, "_")}`;       yield* asOrg(org, (client) =>        client.openapi.addSpec({          params: { scopeId: ScopeId.make(org) },          payload: {            spec: { kind: "blob", value: makeRefreshSpecText() },            name: namespace,            baseUrl: "https://api.example.test",            namespace,          },        }),      );       const sources = yield* asOrg(org, (client) =>        client.sources.list({ params: { scopeId: ScopeId.make(org) } }),      );      const row = sources.find((s) => s.id === namespace);      expect(row?.canRefresh).toBe(false);       // Raw-text sources reach the plugin with no stored URL and      // silently no-op — UI gates the action on canRefresh, but the      // server should not 500 if a caller slips through.      const result = yield* asOrg(org, (client) =>        client.sources.refresh({          params: { scopeId: ScopeId.make(org), sourceId: namespace },        }),      );      expect(result.refreshed).toBe(true);    }),  );});