History

Fix apps inner failures and descriptor blobs

Rhys Sullivan committed 4 months ago

Commit f5d9c971 parent 1aa1ab410 files · +296 −70Browse files

Unified · Split

10 files
  1. packages/plugins/apps/src/executor/app-tool-executor.ts +13 −2
  2. packages/plugins/apps/src/pipeline/descriptor.ts +0 −1
  3. packages/plugins/apps/src/pipeline/publish.test.ts +5 −4
  4. packages/plugins/apps/src/pipeline/publish.ts +20 −32
  5. packages/plugins/apps/src/plugin/apps-plugin.test.ts +132 −1
  6. packages/plugins/apps/src/plugin/apps-plugin.ts +61 −10
  7. packages/plugins/apps/src/plugin/bindings.ts +10 −1
  8. packages/plugins/apps/src/plugin/resolver.ts +44 −13
  9. packages/plugins/apps/src/plugin/store.test.ts +7 −3
  10. packages/plugins/apps/src/plugin/store.ts +4 −3
packages/plugins/apps/src/executor/app-tool-executor.ts+13−2
@@ -1,6 +1,6 @@
1/* oxlint-disable executor/no-try-catch-or-throw, executor/no-double-cast, executor/no-promise-reject, executor/no-instanceof-tagged-error -- boundary: in-process dynamic-import backing converts JavaScript exceptions into typed AppExecutorError */1/* oxlint-disable executor/no-try-catch-or-throw, executor/no-double-cast, executor/no-promise-reject, executor/no-instanceof-tagged-error -- boundary: in-process dynamic-import backing converts JavaScript exceptions into typed AppExecutorError */
2import { Buffer } from "node:buffer";2import { Buffer } from "node:buffer";
3import { Data, Effect } from "effect";3import { Data, Effect, Predicate } from "effect";
4import { z } from "zod";4import { z } from "zod";
5 5
6import { validToolKey } from "../pipeline/discover";6import { validToolKey } from "../pipeline/discover";
@@ -126,6 +126,13 @@ const jsonSchemaFor = (schema: unknown): unknown => { · 119 unmodified lines
126 return schema;126 return schema;
127};127};
128 128
129const innerToolFailureMessage = (cause: unknown): string | null => {
130 if (!Predicate.isTagged("AppInnerToolError")(cause)) return null;
131 const error = cause as { readonly address?: unknown; readonly innerMessage?: unknown };
132 if (typeof error.address !== "string" || typeof error.innerMessage !== "string") return null;
133 return `Inner tool ${error.address} failed: "${error.innerMessage}"`;
134};
135
129type CollectedIntegrationDecl = CollectedTool["integrations"][string];136type CollectedIntegrationDecl = CollectedTool["integrations"][string];
130 137
131const integrationDeclaration = (value: unknown): CollectedIntegrationDecl | null => {138const integrationDeclaration = (value: unknown): CollectedIntegrationDecl | null => {
@@ -452,6 +459,10 @@ export const makeInProcessAppToolExecutor = (): AppToolExecutor => ({ · 320 unmodified lines
452 catch: (cause) =>459 catch: (cause) =>
453 cause instanceof AppExecutorError460 cause instanceof AppExecutorError
454 ? cause461 ? cause
455 : new AppExecutorError({ kind: "invoke", message: "app tool invocation failed", cause }),462 : new AppExecutorError({
463 kind: "invoke",
464 message: innerToolFailureMessage(cause) ?? "app tool invocation failed",
465 cause,
466 }),
456 }),467 }),
457});468});
packages/plugins/apps/src/pipeline/descriptor.ts+0−1
@@ -48,7 +48,6 @@ export interface AppDescriptor { · 47 unmodified lines
48 readonly version: typeof DESCRIPTOR_VERSION;48 readonly version: typeof DESCRIPTOR_VERSION;
49 readonly app: string;49 readonly app: string;
50 readonly sourceRef: string;50 readonly sourceRef: string;
51 readonly descriptorKey: string;
52 readonly publishedAt: number;51 readonly publishedAt: number;
53 readonly toolchain: ToolchainRef;52 readonly toolchain: ToolchainRef;
54 readonly tools: readonly ToolDescriptor[];53 readonly tools: readonly ToolDescriptor[];
packages/plugins/apps/src/pipeline/publish.test.ts+5−4
@@ -28,6 +28,7 @@ const makeMemoryStore = (): AppsStore & { · 27 unmodified lines
28 const blobs = new Map<string, string>();28 const blobs = new Map<string, string>();
29 const rows = new Map<string, unknown>();29 const rows = new Map<string, unknown>();
30 let descriptor: AppDescriptor | null = null;30 let descriptor: AppDescriptor | null = null;
31 let descriptorKey: string | null = null;
31 return {32 return {
32 rows,33 rows,
33 blobs,34 blobs,
@@ -40,11 +41,9 @@ const makeMemoryStore = (): AppsStore & { · 6 unmodified lines
40 getBlob: (key) => Effect.sync(() => blobs.get(key) ?? null),41 getBlob: (key) => Effect.sync(() => blobs.get(key) ?? null),
41 getDescriptorRecord: () =>42 getDescriptorRecord: () =>
42 Effect.sync(() =>43 Effect.sync(() =>
43 descriptor44 descriptor ? { sourceRef: descriptor.sourceRef, descriptorKey: descriptorKey ?? "" } : null,
44 ? { sourceRef: descriptor.sourceRef, descriptorKey: descriptor.descriptorKey }
45 : null,
46 ),45 ),
47 putPublished: (next, _owner, expectedSourceRef) =>46 putPublished: (next, nextDescriptorKey, _owner, expectedSourceRef) =>
48 Effect.gen(function* () {47 Effect.gen(function* () {
49 const actualSourceRef = descriptor?.sourceRef ?? null;48 const actualSourceRef = descriptor?.sourceRef ?? null;
50 if (actualSourceRef !== expectedSourceRef) {49 if (actualSourceRef !== expectedSourceRef) {
@@ -55,6 +54,7 @@ const makeMemoryStore = (): AppsStore & { · 4 unmodified lines
55 });54 });
56 }55 }
57 descriptor = next;56 descriptor = next;
57 descriptorKey = nextDescriptorKey;
58 for (const key of rows.keys()) rows.delete(key);58 for (const key of rows.keys()) rows.delete(key);
59 for (const item of next.tools) rows.set(item.name, item);59 for (const item of next.tools) rows.set(item.name, item);
60 }),60 }),
@@ -132,6 +132,7 @@ describe("publish", () => { · 71 unmodified lines
132 expect(result.publishedTools).toEqual(["sync"]);132 expect(result.publishedTools).toEqual(["sync"]);
133 expect(result.skipped).toEqual([{ path: "workflows/later.ts", reason: "not supported yet" }]);133 expect(result.skipped).toEqual([{ path: "workflows/later.ts", reason: "not supported yet" }]);
134 expect(result.descriptor.toolchain.executor.name).toBe("in-process-data-url");134 expect(result.descriptor.toolchain.executor.name).toBe("in-process-data-url");
135 expect(store.blobs.size).toBe(2);
135 }),136 }),
136 );137 );
137 138
packages/plugins/apps/src/pipeline/publish.ts+20−32
@@ -180,11 +180,10 @@ export const publish = ( · 179 unmodified lines
180 }180 }
181 }181 }
182 182
183 const descriptorSeed = {183 const descriptor: AppDescriptor = {
184 version: DESCRIPTOR_VERSION,184 version: DESCRIPTOR_VERSION,
185 app: input.app,185 app: input.app,
186 sourceRef: input.sourceRef,186 sourceRef: input.sourceRef,
187 descriptorKey: "",
188 publishedAt: deps.now?.() ?? Date.now(),187 publishedAt: deps.now?.() ?? Date.now(),
189 toolchain: toolchainRef(),188 toolchain: toolchainRef(),
190 tools,189 tools,
@@ -193,49 +192,38 @@ export const publish = ( · 2 unmodified lines
193 skills: discovered.skipped.filter((item) => item.path.startsWith("skills/")),192 skills: discovered.skipped.filter((item) => item.path.startsWith("skills/")),
194 skipped: discovered.skipped,193 skipped: discovered.skipped,
195 };194 };
196 const descriptorBodyWithoutKey = stableStringify(descriptorSeed);
197 const descriptorKey = yield* deps.store.putBlob(descriptorBodyWithoutKey, owner).pipe(
198 Effect.mapError(
199 () =>
200 new PublishError({
201 stage: "project",
202 message: "failed to write descriptor blob",
203 diagnostics: [],
204 }),
205 ),
206 );
207 const descriptor: AppDescriptor = { ...descriptorSeed, descriptorKey };
208 const descriptorBody = stableStringify(descriptor);195 const descriptorBody = stableStringify(descriptor);
209 const finalDescriptorKey = yield* deps.store.putBlob(descriptorBody, owner).pipe(196 const descriptorKey = yield* deps.store.putBlob(descriptorBody, owner).pipe(
210 Effect.mapError(197 Effect.mapError(
211 () =>198 () =>
212 new PublishError({199 new PublishError({
213 stage: "project",200 stage: "project",
214 message: "failed to write final descriptor blob",201 message: "failed to write descriptor blob",
215 diagnostics: [],202 diagnostics: [],
216 }),203 }),
217 ),204 ),
218 );205 );
219 const finalDescriptor: AppDescriptor = { ...descriptor, descriptorKey: finalDescriptorKey };206 yield* deps.store
220 yield* deps.store.putPublished(finalDescriptor, owner, existing?.sourceRef ?? null).pipe(207 .putPublished(descriptor, descriptorKey, owner, existing?.sourceRef ?? null)
221 Effect.mapError((cause) => {208 .pipe(
222 if (Predicate.isTagged("AppPublishConflictError")(cause)) {209 Effect.mapError((cause) => {
210 if (Predicate.isTagged("AppPublishConflictError")(cause)) {
211 return new PublishError({
212 stage: "project",
213 message: `app "${input.app}" changed during publish`,
214 diagnostics: [{ path: "", message: "publish sourceRef changed during publish" }],
215 });
216 }
223 return new PublishError({217 return new PublishError({
224 stage: "project",218 stage: "project",
225 message: `app "${input.app}" changed during publish`,219 message: "failed to persist app publication",
226 diagnostics: [{ path: "", message: "publish sourceRef changed during publish" }],220 diagnostics: [],
227 });221 });
228 }222 }),
229 return new PublishError({223 );
230 stage: "project",
231 message: "failed to persist app publication",
232 diagnostics: [],
233 });
234 }),
235 );
236 return {224 return {
237 descriptor: finalDescriptor,225 descriptor,
238 publishedTools: finalDescriptor.tools.map((tool) => tool.name),226 publishedTools: descriptor.tools.map((tool) => tool.name),
239 skipped: discovered.skipped,227 skipped: discovered.skipped,
240 noop: false,228 noop: false,
241 };229 };
packages/plugins/apps/src/plugin/apps-plugin.test.ts+132−1
@@ -1,9 +1,60 @@
1import { describe, expect, it } from "@effect/vitest";1import { describe, expect, it } from "@effect/vitest";
2import { Effect } from "effect";2import { Effect } from "effect";
3import { ToolResult } from "@executor-js/sdk";
3 4
4import { projectAppsToolSchema } from "./apps-plugin";5import { bundleEntry } from "../pipeline/bundle";
6import { makeInProcessAppToolExecutor } from "../executor/app-tool-executor";
7import type { AppDescriptor } from "../pipeline/descriptor";
8import { makeAppsPlugin, projectAppsToolSchema } from "./apps-plugin";
5import type { AppsStore } from "./store";9import type { AppsStore } from "./store";
6 10
11const bundle = (source: string) =>
12 bundleEntry({
13 files: new Map([["tools/sync.ts", source]]),
14 entry: "tools/sync.ts",
15 });
16
17const makeInvokeStore = (input: {
18 readonly bundle: string;
19 readonly integrations: AppDescriptor["tools"][number]["integrations"];
20}): AppsStore => ({
21 putBlob: () => Effect.succeed("bundle"),
22 getBlob: () => Effect.succeed(input.bundle),
23 getDescriptorRecord: () => Effect.succeed(null),
24 putPublished: () => Effect.void,
25 listActiveTools: () => Effect.succeed([]),
26 getTool: () =>
27 Effect.succeed({
28 app: "crm",
29 name: "sync",
30 bundleKey: "bundle",
31 description: "Sync",
32 integrations: input.integrations,
33 }),
34});
35
36const invokeCtx = (input: {
37 readonly bundle: string;
38 readonly execute: (address: string, args: unknown) => Effect.Effect<unknown, unknown>;
39}) =>
40 ({
41 storage: makeInvokeStore({
42 bundle: input.bundle,
43 integrations: { crm: { slug: "dealcloud", mode: "one" } },
44 }),
45 connections: {
46 list: () => Effect.succeed([]),
47 get: () =>
48 Effect.succeed({
49 address: "tools.dealcloud.org.main",
50 integration: "dealcloud",
51 owner: "org",
52 name: "main",
53 }),
54 },
55 execute: (address: string, args: unknown) => input.execute(String(address), args),
56 }) as never;
57
7describe("apps plugin schema projection", () => {58describe("apps plugin schema projection", () => {
8 it.effect("narrows synthesized integration fields to connection address enums", () =>59 it.effect("narrows synthesized integration fields to connection address enums", () =>
9 Effect.gen(function* () {60 Effect.gen(function* () {
@@ -63,3 +114,83 @@ describe("apps plugin schema projection", () => { · 53 unmodified lines
63 }),114 }),
64 );115 );
65});116});
117
118describe("apps plugin invocation", () => {
119 it.effect("surfaces uncaught inner tool failures without binding_error", () =>
120 Effect.gen(function* () {
121 const bundled = yield* bundle(`
122 import { z } from "zod";
123 import { defineTool, integration } from "executor:app";
124 export default defineTool({
125 description: "Sync",
126 integrations: { crm: integration("dealcloud") },
127 input: z.object({}),
128 async handler(_input, { crm }) {
129 await crm.deals.list({});
130 return { ok: true };
131 },
132 });
133 `);
134 const plugin = makeAppsPlugin({ executor: makeInProcessAppToolExecutor() });
135 const result = yield* plugin.invokeTool!({
136 ctx: invokeCtx({
137 bundle: bundled.code,
138 execute: () =>
139 Effect.succeed(
140 ToolResult.fail({ code: "upstream_failed", message: "CRM unavailable" }),
141 ),
142 }),
143 toolRow: { name: "sync" },
144 args: { crm: "tools.dealcloud.org.main" },
145 } as never);
146 expect(result).toMatchObject({
147 ok: false,
148 error: {
149 code: "upstream_failed",
150 message: expect.stringContaining("tools.dealcloud.org.main.deals.list"),
151 },
152 });
153 expect(result).toMatchObject({
154 error: { message: expect.stringContaining('"CRM unavailable"') },
155 });
156 expect((result as { readonly error: { readonly code: string } }).error.code).not.toBe(
157 "binding_error",
158 );
159 }),
160 );
161
162 it.effect("lets handlers catch inner tool failures and return a fallback", () =>
163 Effect.gen(function* () {
164 const bundled = yield* bundle(`
165 import { z } from "zod";
166 import { defineTool, integration } from "executor:app";
167 export default defineTool({
168 description: "Sync",
169 integrations: { crm: integration("dealcloud") },
170 input: z.object({}),
171 async handler(_input, { crm }) {
172 try {
173 await crm.deals.list({});
174 return { fallback: false };
175 } catch {
176 return { fallback: true };
177 }
178 },
179 });
180 `);
181 const plugin = makeAppsPlugin({ executor: makeInProcessAppToolExecutor() });
182 const result = yield* plugin.invokeTool!({
183 ctx: invokeCtx({
184 bundle: bundled.code,
185 execute: () =>
186 Effect.succeed(
187 ToolResult.fail({ code: "upstream_failed", message: "CRM unavailable" }),
188 ),
189 }),
190 toolRow: { name: "sync" },
191 args: { crm: "tools.dealcloud.org.main" },
192 } as never);
193 expect(result).toEqual({ fallback: true });
194 }),
195 );
196});
packages/plugins/apps/src/plugin/apps-plugin.ts+61−10
@@ -1,5 +1,5 @@
1import { Data, Effect } from "effect";1import { Data, Effect, Predicate } from "effect";
2import { ToolName, definePlugin, type PluginCtx, type ToolDef } from "@executor-js/sdk";2import { ToolName, ToolResult, definePlugin, type PluginCtx, type ToolDef } from "@executor-js/sdk";
3 3
4import { makeInProcessAppToolExecutor, type AppToolExecutor } from "../executor/app-tool-executor";4import { makeInProcessAppToolExecutor, type AppToolExecutor } from "../executor/app-tool-executor";
5import { publish } from "../pipeline/publish";5import { publish } from "../pipeline/publish";
@@ -19,6 +19,43 @@ interface ProjectedToolSchema { · 13 unmodified lines
19 readonly outputSchema?: unknown;19 readonly outputSchema?: unknown;
20}20}
21 21
22const innerToolError = (
23 cause: unknown,
24): { readonly address: string; readonly innerMessage: string; readonly code?: string } | null => {
25 const direct =
26 Predicate.isTagged("AppInnerToolError")(cause) && typeof cause === "object" && cause !== null
27 ? (cause as {
28 readonly address?: unknown;
29 readonly innerMessage?: unknown;
30 readonly code?: unknown;
31 })
32 : null;
33 const nested =
34 direct === null &&
35 cause !== null &&
36 typeof cause === "object" &&
37 "cause" in cause &&
38 Predicate.isTagged("AppInnerToolError")(cause.cause)
39 ? (cause.cause as {
40 readonly address?: unknown;
41 readonly innerMessage?: unknown;
42 readonly code?: unknown;
43 })
44 : direct;
45 if (
46 nested === null ||
47 typeof nested.address !== "string" ||
48 typeof nested.innerMessage !== "string"
49 ) {
50 return null;
51 }
52 return {
53 address: nested.address,
54 innerMessage: nested.innerMessage,
55 ...(typeof nested.code === "string" ? { code: nested.code } : {}),
56 };
57};
58
22const makeAppsExtension = (ctx: PluginCtx<AppsStore>, executor?: AppToolExecutor) => ({59const makeAppsExtension = (ctx: PluginCtx<AppsStore>, executor?: AppToolExecutor) => ({
23 publish: (input: Parameters<typeof publish>[1]) =>60 publish: (input: Parameters<typeof publish>[1]) =>
24 publish({ store: ctx.storage, executor: executor ?? makeInProcessAppToolExecutor() }, input),61 publish({ store: ctx.storage, executor: executor ?? makeInProcessAppToolExecutor() }, input),
@@ -92,14 +129,28 @@ export const makeAppsPlugin = (options?: { readonly executor?: AppToolExecutor } · 67 unmodified lines
92 resolver,129 resolver,
93 invokeOptions,130 invokeOptions,
94 });131 });
95 const result = yield* (options?.executor ?? makeInProcessAppToolExecutor()).invoke(132 const result = yield* (options?.executor ?? makeInProcessAppToolExecutor())
96 bundle,133 .invoke(
97 { toolName: tool.name },134 bundle,
98 { ...bindings.input, ...bindings.bindings },135 { toolName: tool.name },
99 bridge,136 { ...bindings.input, ...bindings.bindings },
100 { timeoutMs: 30_000 },137 bridge,
101 );138 { timeoutMs: 30_000 },
102 return result.output;139 )
140 .pipe(
141 Effect.catch((cause: unknown) => {
142 const inner = innerToolError(cause);
143 return inner
144 ? Effect.succeed(
145 ToolResult.fail({
146 code: inner.code ?? "inner_tool_error",
147 message: `Inner tool ${inner.address} failed: "${inner.innerMessage}"`,
148 }),
149 )
150 : Effect.fail(cause);
151 }),
152 );
153 return "output" in result ? result.output : result;
103 }),154 }),
104 }))();155 }))();
105 156
packages/plugins/apps/src/plugin/bindings.ts+10−1
@@ -11,6 +11,15 @@ export class BindingError extends Data.TaggedError("BindingError")<{ · 10 unmodified lines
11 readonly requestedConnection?: string;11 readonly requestedConnection?: string;
12}> {}12}> {}
13 13
14export class AppInnerToolError extends Data.TaggedError("AppInnerToolError")<{
15 readonly address: string;
16 readonly innerMessage: string;
17 readonly code?: string;
18}> {}
19
20export const appInnerToolFailureMessage = (error: AppInnerToolError): string =>
21 `Inner tool ${error.address} failed: "${error.innerMessage}"`;
22
14export interface ConnectionCandidate {23export interface ConnectionCandidate {
15 readonly address: string;24 readonly address: string;
16 readonly integration: string;25 readonly integration: string;
@@ -29,7 +38,7 @@ export interface ClientResolver { · 12 unmodified lines
29 readonly path: readonly string[];38 readonly path: readonly string[];
30 readonly args: unknown;39 readonly args: unknown;
31 readonly invokeOptions?: InvokeOptions;40 readonly invokeOptions?: InvokeOptions;
32 }) => Effect.Effect<unknown, BindingError>;41 }) => Effect.Effect<unknown, BindingError | AppInnerToolError>;
33}42}
34 43
35export interface ResolvedBindings {44export interface ResolvedBindings {
packages/plugins/apps/src/plugin/resolver.ts+44−13
@@ -1,15 +1,21 @@
1import { Effect } from "effect";1import { Effect, Predicate } from "effect";
2import {2import {
3 ConnectionName,3 ConnectionName,
4 IntegrationSlug,4 IntegrationSlug,
5 ToolAddress,5 ToolAddress,
6 isToolResult,6 isToolResult,
7 type ExecuteError,
7 type ConnectionRef,8 type ConnectionRef,
8 type Owner,9 type Owner,
9 type PluginCtx,10 type PluginCtx,
10} from "@executor-js/sdk";11} from "@executor-js/sdk";
11 12
12import { BindingError, type ClientResolver, type ConnectionCandidate } from "./bindings";13import {
14 AppInnerToolError,
15 BindingError,
16 type ClientResolver,
17 type ConnectionCandidate,
18} from "./bindings";
13 19
14export interface AppsResolverPluginCtx {20export interface AppsResolverPluginCtx {
15 readonly connections: Pick<PluginCtx["connections"], "list" | "get">;21 readonly connections: Pick<PluginCtx["connections"], "list" | "get">;
@@ -44,6 +50,34 @@ const toCandidate = (connection: { · 28 unmodified lines
44 integration: String(connection.integration),50 integration: String(connection.integration),
45});51});
46 52
53const innerMessageFromCause = (cause: ExecuteError): string => {
54 if (
55 Predicate.isTagged("ToolInvocationError")(cause) ||
56 Predicate.isTagged("CredentialResolutionError")(cause) ||
57 Predicate.isTagged("StorageError")(cause)
58 ) {
59 // oxlint-disable-next-line executor/no-unknown-error-message -- typed ExecuteError variants expose message as caller-facing failure text
60 return cause.message;
61 }
62 if (Predicate.isTagged("ToolNotFoundError")(cause)) return `Tool not found: ${cause.address}`;
63 if (Predicate.isTagged("ToolBlockedError")(cause)) return `Tool blocked: ${cause.address}`;
64 if (Predicate.isTagged("PluginNotLoadedError")(cause)) {
65 return `Plugin not loaded for tool: ${cause.address}`;
66 }
67 if (Predicate.isTagged("NoHandlerError")(cause)) return `No handler for tool: ${cause.address}`;
68 if (Predicate.isTagged("ConnectionNotFoundError")(cause)) {
69 return `Connection not found: ${cause.integration}.${cause.owner}.${cause.name}`;
70 }
71 if (Predicate.isTagged("CredentialProviderNotRegisteredError")(cause)) {
72 return `Credential provider not registered: ${cause.provider}`;
73 }
74 if (Predicate.isTagged("ElicitationDeclinedError")(cause)) {
75 return `Tool approval ${cause.action === "cancel" ? "cancelled" : "declined"}: ${cause.address}`;
76 }
77 if (Predicate.isTagged("UniqueViolationError")(cause)) return "storage unique violation";
78 return "inner tool failed";
79};
80
47export const makePluginCtxAppsResolver = (input: {81export const makePluginCtxAppsResolver = (input: {
48 readonly ctx: AppsResolverPluginCtx;82 readonly ctx: AppsResolverPluginCtx;
49}): ClientResolver => ({83}): ClientResolver => ({
@@ -98,22 +132,19 @@ export const makePluginCtxAppsResolver = (input: { · 48 unmodified lines
98 const address = ToolAddress.make(`${connection}.${path.join(".")}`);132 const address = ToolAddress.make(`${connection}.${path.join(".")}`);
99 const result = yield* input.ctx.execute(address, args, invokeOptions).pipe(133 const result = yield* input.ctx.execute(address, args, invokeOptions).pipe(
100 Effect.mapError(134 Effect.mapError(
101 () =>135 (cause) =>
102 new BindingError({136 new AppInnerToolError({
103 role: integration,137 address,
104 integration,138 innerMessage: innerMessageFromCause(cause),
105 requestedConnection: connection,
106 message: `failed to execute ${address}`,
107 }),139 }),
108 ),140 ),
109 );141 );
110 if (isToolResult(result)) {142 if (isToolResult(result)) {
111 if (result.ok) return result.data;143 if (result.ok) return result.data;
112 return yield* new BindingError({144 return yield* new AppInnerToolError({
113 role: integration,145 address,
114 integration,146 code: result.error.code,
115 requestedConnection: connection,147 innerMessage: result.error.message,
116 message: result.error.message,
117 });148 });
118 }149 }
119 return result;150 return result;
packages/plugins/apps/src/plugin/store.test.ts+7−3
@@ -139,7 +139,6 @@ const descriptor = (tools: readonly AppDescriptor["tools"][number][]): AppDescri · 138 unmodified lines
139 version: 6,139 version: 6,
140 app: "crm",140 app: "crm",
141 sourceRef: "sha-1",141 sourceRef: "sha-1",
142 descriptorKey: "descriptor",
143 publishedAt: 1,142 publishedAt: 1,
144 toolchain: {143 toolchain: {
145 bundler: { name: "esbuild", version: "0" },144 bundler: { name: "esbuild", version: "0" },
@@ -169,8 +168,13 @@ describe("apps store", () => { · 23 unmodified lines
169 blobs: pluginBlobStore(makeInMemoryBlobStore(), { org: "tenant", user: null }, "apps"),168 blobs: pluginBlobStore(makeInMemoryBlobStore(), { org: "tenant", user: null }, "apps"),
170 pluginStorage: makeMemoryPluginStorage(),169 pluginStorage: makeMemoryPluginStorage(),
171 });170 });
172 yield* store.putPublished(descriptor([tool("first"), tool("second")]), "org", null);171 yield* store.putPublished(
173 yield* store.putPublished(descriptor([tool("first")]), "org", "sha-1");172 descriptor([tool("first"), tool("second")]),
173 "descriptor",
174 "org",
175 null,
176 );
177 yield* store.putPublished(descriptor([tool("first")]), "descriptor-2", "org", "sha-1");
174 const active = yield* store.listActiveTools();178 const active = yield* store.listActiveTools();
175 expect(active.map((item) => item.name)).toEqual(["first"]);179 expect(active.map((item) => item.name)).toEqual(["first"]);
176 }),180 }),
packages/plugins/apps/src/plugin/store.ts+4−3
@@ -59,6 +59,7 @@ export interface AppsStore { · 58 unmodified lines
59 >;59 >;
60 readonly putPublished: (60 readonly putPublished: (
61 descriptor: AppDescriptor,61 descriptor: AppDescriptor,
62 descriptorKey: string,
62 owner: Owner,63 owner: Owner,
63 expectedSourceRef: string | null,64 expectedSourceRef: string | null,
64 ) => Effect.Effect<void, StorageFailure | AppPublishConflictError>;65 ) => Effect.Effect<void, StorageFailure | AppPublishConflictError>;
@@ -113,7 +114,7 @@ export const makeAppsStore = (input: { · 48 unmodified lines
113 : null,114 : null,
114 ),115 ),
115 ),116 ),
116 putPublished: (descriptor, owner, expectedSourceRef) =>117 putPublished: (descriptor, descriptorKey, owner, expectedSourceRef) =>
117 Effect.gen(function* () {118 Effect.gen(function* () {
118 const current = yield* descriptors.get({ key: descriptor.app });119 const current = yield* descriptors.get({ key: descriptor.app });
119 const actualSourceRef = current?.data.sourceRef ?? null;120 const actualSourceRef = current?.data.sourceRef ?? null;
@@ -134,7 +135,7 @@ export const makeAppsStore = (input: { · 14 unmodified lines
134 app: descriptor.app,135 app: descriptor.app,
135 name: tool.name,136 name: tool.name,
136 sourceRef: descriptor.sourceRef,137 sourceRef: descriptor.sourceRef,
137 descriptorKey: descriptor.descriptorKey,138 descriptorKey,
138 bundleKey: tool.bundleKey,139 bundleKey: tool.bundleKey,
139 description: tool.description,140 description: tool.description,
140 inputSchema: tool.inputSchema,141 inputSchema: tool.inputSchema,
@@ -161,7 +162,7 @@ export const makeAppsStore = (input: { · 20 unmodified lines
161 data: {162 data: {
162 app: descriptor.app,163 app: descriptor.app,
163 sourceRef: descriptor.sourceRef,164 sourceRef: descriptor.sourceRef,
164 descriptorKey: descriptor.descriptorKey,165 descriptorKey,
165 publishedAt: descriptor.publishedAt,166 publishedAt: descriptor.publishedAt,
166 toolchain: descriptor.toolchain,167 toolchain: descriptor.toolchain,
167 },168 },

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t