| @@ -3,12 +3,29 @@ import { randomBytes } from "node:crypto"; · 2 unmodified lines |
| 3 | import { expect } from "@effect/vitest"; | 3 | import { expect } from "@effect/vitest"; |
| 4 | import { Effect } from "effect"; | 4 | import { Effect } from "effect"; |
| 5 | import { composePluginApi } from "@executor-js/api/server"; | 5 | import { composePluginApi } from "@executor-js/api/server"; |
| | 6 | import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api"; |
| 6 | import { toolkitsPlugin } from "@executor-js/plugin-toolkits/server"; | 7 | import { toolkitsPlugin } from "@executor-js/plugin-toolkits/server"; |
| | 8 | import { AuthTemplateSlug, ConnectionName, IntegrationSlug } from "@executor-js/sdk/shared"; |
| 7 | | 9 | |
| 8 | import { scenario } from "../src/scenario"; | 10 | import { scenario } from "../src/scenario"; |
| 9 | import { Api, Browser, Target } from "../src/services"; | 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 | scenario( | 30 | scenario( |
| 14 | "Toolkits · self-host UI creates a toolkit and configures tools", | 31 | "Toolkits · self-host UI creates a toolkit and configures tools", |
| @@ -24,6 +41,8 @@ scenario( · 9 unmodified lines |
| 24 | const prefix = `toolkits-ui-${suffix}`; | 41 | const prefix = `toolkits-ui-${suffix}`; |
| 25 | const name = `${prefix}-created`; | 42 | const name = `${prefix}-created`; |
| 26 | const slug = name; | 43 | const slug = name; |
| | 44 | const hiddenPersonalIntegration = `${prefix}-personal-api`; |
| | 45 | const hiddenPersonalConnection = "mine"; |
| 27 | const seededToolkits = [ | 46 | const seededToolkits = [ |
| 28 | { owner: "org" as const, name: `${prefix}-workspace-a` }, | 47 | { owner: "org" as const, name: `${prefix}-workspace-a` }, |
| 29 | { owner: "org" as const, name: `${prefix}-workspace-b` }, | 48 | { owner: "org" as const, name: `${prefix}-workspace-b` }, |
| @@ -35,6 +54,18 @@ scenario( · 5 unmodified lines |
| 35 | const blockPattern = "executor.coreTools.policies.list"; | 54 | const blockPattern = "executor.coreTools.policies.list"; |
| 36 | | 55 | |
| 37 | const cleanup = Effect.gen(function* () { | 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 | const listed = yield* client.toolkits.list(); | 69 | const listed = yield* client.toolkits.list(); |
| 39 | yield* Effect.forEach( | 70 | yield* Effect.forEach( |
| 40 | listed.toolkits.filter((row) => row.slug.startsWith(prefix)), | 71 | listed.toolkits.filter((row) => row.slug.startsWith(prefix)), |
| @@ -49,6 +80,29 @@ scenario( · 8 unmodified lines |
| 49 | (toolkit) => client.toolkits.create({ payload: toolkit }), | 80 | (toolkit) => client.toolkits.create({ payload: toolkit }), |
| 50 | { discard: true }, | 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 | yield* browser.session(identity, async ({ page, step }) => { | 107 | yield* browser.session(identity, async ({ page, step }) => { |
| 54 | await step("Open the Toolkits plugin page", async () => { | 108 | await step("Open the Toolkits plugin page", async () => { |
| @@ -143,8 +197,18 @@ scenario( · 88 unmodified lines |
| 143 | await page.getByRole("heading", { name }).waitFor(); | 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 | await page.getByRole("button", { name: "Manage toolkit connections" }).click(); | 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 | const dialog = page.getByRole("dialog", { name: "Manage connections" }); | 212 | const dialog = page.getByRole("dialog", { name: "Manage connections" }); |
| 149 | await dialog.waitFor(); | 213 | await dialog.waitFor(); |
| 150 | await dialog.getByLabel("Search connections and tools").fill("policies.list"); | 214 | await dialog.getByLabel("Search connections and tools").fill("policies.list"); |