| @@ -3,12 +3,29 @@ import { randomBytes } from "node:crypto"; · 2 unmodified lines |
| 3 | 3 | import { expect } from "@effect/vitest"; |
| 4 | 4 | import { Effect } from "effect"; |
| 5 | 5 | import { composePluginApi } from "@executor-js/api/server"; |
| 6 | +import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api"; |
| 6 | 7 | import { toolkitsPlugin } from "@executor-js/plugin-toolkits/server"; |
| 8 | +import { AuthTemplateSlug, ConnectionName, IntegrationSlug } from "@executor-js/sdk/shared"; |
| 7 | 9 | |
| 8 | 10 | import { scenario } from "../src/scenario"; |
| 9 | 11 | import { Api, Browser, Target } from "../src/services"; |
| 10 | 12 | |
| 11 | | −const api = composePluginApi([toolkitsPlugin()] as const); |
| 13 | +const api = composePluginApi([toolkitsPlugin(), openApiHttpPlugin()] as const); |
| 14 | + |
| 15 | +const hiddenPersonalSpec = (baseUrl: string): string => |
| 16 | + JSON.stringify({ |
| 17 | + openapi: "3.0.3", |
| 18 | + info: { title: "Personal Hidden API", version: "1.0.0" }, |
| 19 | + servers: [{ url: baseUrl }], |
| 20 | + paths: { |
| 21 | + "/personal-only": { |
| 22 | + get: { |
| 23 | + operationId: "personalOnly", |
| 24 | + responses: { "200": { description: "Personal-only response" } }, |
| 25 | + }, |
| 26 | + }, |
| 27 | + }, |
| 28 | + }); |
| 12 | 29 | |
| 13 | 30 | scenario( |
| 14 | 31 | "Toolkits · self-host UI creates a toolkit and configures tools", |
| @@ -24,6 +41,8 @@ scenario( · 9 unmodified lines |
| 24 | 41 | const prefix = `toolkits-ui-${suffix}`; |
| 25 | 42 | const name = `${prefix}-created`; |
| 26 | 43 | const slug = name; |
| 44 | + const hiddenPersonalIntegration = `${prefix}-personal-api`; |
| 45 | + const hiddenPersonalConnection = "mine"; |
| 27 | 46 | const seededToolkits = [ |
| 28 | 47 | { owner: "org" as const, name: `${prefix}-workspace-a` }, |
| 29 | 48 | { owner: "org" as const, name: `${prefix}-workspace-b` }, |
| @@ -35,6 +54,18 @@ scenario( · 5 unmodified lines |
| 35 | 54 | const blockPattern = "executor.coreTools.policies.list"; |
| 36 | 55 | |
| 37 | 56 | const cleanup = Effect.gen(function* () { |
| 57 | + yield* client.connections |
| 58 | + .remove({ |
| 59 | + params: { |
| 60 | + owner: "user", |
| 61 | + integration: IntegrationSlug.make(hiddenPersonalIntegration), |
| 62 | + name: ConnectionName.make(hiddenPersonalConnection), |
| 63 | + }, |
| 64 | + }) |
| 65 | + .pipe(Effect.ignore); |
| 66 | + yield* client.openapi |
| 67 | + .removeSpec({ params: { slug: hiddenPersonalIntegration } }) |
| 68 | + .pipe(Effect.ignore); |
| 38 | 69 | const listed = yield* client.toolkits.list(); |
| 39 | 70 | yield* Effect.forEach( |
| 40 | 71 | listed.toolkits.filter((row) => row.slug.startsWith(prefix)), |
| @@ -49,6 +80,29 @@ scenario( · 8 unmodified lines |
| 49 | 80 | (toolkit) => client.toolkits.create({ payload: toolkit }), |
| 50 | 81 | { discard: true }, |
| 51 | 82 | ); |
| 83 | + yield* client.openapi.addSpec({ |
| 84 | + payload: { |
| 85 | + spec: { kind: "blob", value: hiddenPersonalSpec("http://127.0.0.1:59999") }, |
| 86 | + slug: IntegrationSlug.make(hiddenPersonalIntegration), |
| 87 | + baseUrl: "http://127.0.0.1:59999", |
| 88 | + authenticationTemplate: [ |
| 89 | + { |
| 90 | + slug: "apiKey", |
| 91 | + type: "apiKey", |
| 92 | + headers: { "x-api-key": [{ type: "variable", name: "token" }] }, |
| 93 | + }, |
| 94 | + ], |
| 95 | + }, |
| 96 | + }); |
| 97 | + yield* client.connections.create({ |
| 98 | + payload: { |
| 99 | + owner: "user", |
| 100 | + name: ConnectionName.make(hiddenPersonalConnection), |
| 101 | + integration: IntegrationSlug.make(hiddenPersonalIntegration), |
| 102 | + template: AuthTemplateSlug.make("apiKey"), |
| 103 | + value: "unused-token", |
| 104 | + }, |
| 105 | + }); |
| 52 | 106 | |
| 53 | 107 | yield* browser.session(identity, async ({ page, step }) => { |
| 54 | 108 | await step("Open the Toolkits plugin page", async () => { |
| @@ -143,8 +197,18 @@ scenario( · 88 unmodified lines |
| 143 | 197 | await page.getByRole("heading", { name }).waitFor(); |
| 144 | 198 | }); |
| 145 | 199 | |
| 146 | | − await step("Add a connection to the toolkit", async () => { |
| 200 | + await step("The connection picker explains hidden personal connections", async () => { |
| 147 | 201 | await page.getByRole("button", { name: "Manage toolkit connections" }).click(); |
| 202 | + const dialog = page.getByRole("dialog", { name: "Manage connections" }); |
| 203 | + await dialog.waitFor(); |
| 204 | + await dialog |
| 205 | + .getByText( |
| 206 | + "You have 1 personal connection that is not shown because this is a shared toolkit.", |
| 207 | + ) |
| 208 | + .waitFor(); |
| 209 | + }); |
| 210 | + |
| 211 | + await step("Add a connection to the toolkit", async () => { |
| 148 | 212 | const dialog = page.getByRole("dialog", { name: "Manage connections" }); |
| 149 | 213 | await dialog.waitFor(); |
| 150 | 214 | await dialog.getByLabel("Search connections and tools").fill("policies.list"); |