import { describe, expect, it } from "@effect/vitest"; import { Effect } from "effect"; import { makeInMemoryBlobStore, pluginBlobStore, type PluginStorageCollectionDefinition, type PluginStorageEntry, type PluginStorageFacade, } from "@executor-js/sdk"; import type { AppDescriptor } from "../pipeline/descriptor"; import { descriptorCollection, makeAppsStore, toolCollection } from "./store"; const entry = (input: { owner: "org" | "user"; collection: string; key: string; data: T; }): PluginStorageEntry => ({ id: `${input.collection}:${input.key}`, owner: input.owner, pluginId: "apps", collection: input.collection, key: input.key, data: input.data, createdAt: new Date(0), updatedAt: new Date(0), }); const makeMemoryPluginStorage = (): PluginStorageFacade => { const rows = new Map>(); const rowKey = (owner: string, collection: string, key: string) => `${owner}:${collection}:${key}`; const put = (input: { readonly owner: "org" | "user"; readonly collection: string; readonly key: string; readonly data: object; }) => Effect.sync(() => { const next = entry({ owner: input.owner, collection: input.collection, key: input.key, data: input.data, }); rows.set(rowKey(input.owner, input.collection, input.key), next); return next; }); return { collection: ( definition: TDefinition, ) => ({ get: ({ key }) => Effect.sync(() => { for (const row of rows.values()) { if (row.collection === definition.name && row.key === key) return row as never; } return null; }), getForOwner: ({ owner, key }) => Effect.sync( () => (rows.get(rowKey(owner, definition.name, key)) as never | undefined) ?? null, ), list: ({ keyPrefix } = {}) => Effect.sync( () => [...rows.values()].filter( (row) => row.collection === definition.name && (keyPrefix === undefined || row.key.startsWith(keyPrefix)), ) as never, ), put: (input) => put({ ...input, collection: definition.name }) as never, query: (input = {}) => Effect.sync(() => { let out = [...rows.values()].filter((row) => row.collection === definition.name); if (input.where) { out = out.filter((row) => Object.entries(input.where ?? {}).every(([field, expected]) => { const actual = (row.data as Record)[field]; return typeof expected === "object" && expected !== null && "eq" in expected ? actual === expected.eq : actual === expected; }), ); } return (input.limit ? out.slice(0, input.limit) : out) as never; }), count: () => Effect.sync(() => 0), remove: ({ owner, key }) => Effect.sync(() => { rows.delete(rowKey(owner, definition.name, key)); }), }), get: ({ collection, key }) => Effect.sync(() => { for (const row of rows.values()) { if (row.collection === collection && row.key === key) return row as never; } return null; }), getForOwner: ({ owner, collection, key }) => Effect.sync(() => (rows.get(rowKey(owner, collection, key)) as never | undefined) ?? null), list: ({ collection, keyPrefix }) => Effect.sync( () => [...rows.values()].filter( (row) => row.collection === collection && (keyPrefix === undefined || row.key.startsWith(keyPrefix)), ) as never, ), put: (input) => put(input as never) as never, putMany: ({ owner, entries }) => Effect.sync(() => { for (const item of entries) { const next = entry({ owner, collection: item.collection, key: item.key, data: item.data, }); rows.set(rowKey(owner, item.collection, item.key), next); } }), remove: ({ owner, collection, key }) => Effect.sync(() => { rows.delete(rowKey(owner, collection, key)); }), removeMany: ({ owner, entries }) => Effect.sync(() => { for (const item of entries) rows.delete(rowKey(owner, item.collection, item.key)); }), }; }; const descriptor = (tools: readonly AppDescriptor["tools"][number][]): AppDescriptor => ({ version: 6, app: "crm", sourceRef: "sha-1", publishedAt: 1, toolchain: { bundler: { name: "esbuild", version: "0" }, executor: { name: "test", version: "0" }, target: "es2022", }, tools, workflows: [], ui: [], skills: [], skipped: [], }); const tool = (name: string): AppDescriptor["tools"][number] => ({ name, sourcePath: `tools/${name}.ts`, bundleKey: `bundle:${name}`, source: { path: `tools/${name}.ts`, sourceHash: name }, description: name, integrations: {}, }); describe("apps store", () => { it.effect("tombstones removed tools", () => Effect.gen(function* () { const store = makeAppsStore({ blobs: pluginBlobStore(makeInMemoryBlobStore(), { org: "tenant", user: null }, "apps"), pluginStorage: makeMemoryPluginStorage(), }); yield* store.putPublished( descriptor([tool("first"), tool("second")]), "descriptor", "org", null, ); yield* store.putPublished(descriptor([tool("first")]), "descriptor-2", "org", "sha-1"); const active = yield* store.listActiveTools(); expect(active.map((item) => item.name)).toEqual(["first"]); expect(active.map((item) => item.app)).toEqual(["crm"]); }), ); it.effect("removes a published app catalog", () => Effect.gen(function* () { const store = makeAppsStore({ blobs: pluginBlobStore(makeInMemoryBlobStore(), { org: "tenant", user: null }, "apps"), pluginStorage: makeMemoryPluginStorage(), }); yield* store.putPublished( descriptor([tool("first"), tool("second")]), "descriptor", "org", null, ); yield* store.removePublished("crm", "org"); const active = yield* store.listActiveTools(); const descriptorRecord = yield* store.getDescriptorRecord("crm"); expect(active).toEqual([]); expect(descriptorRecord).toBeNull(); }), ); it.effect("keeps owner blob partitions isolated", () => Effect.gen(function* () { const blobStore = makeInMemoryBlobStore(); const orgStore = makeAppsStore({ blobs: pluginBlobStore(blobStore, { org: "tenant-a", user: null }, "apps"), pluginStorage: makeMemoryPluginStorage(), }); const otherStore = makeAppsStore({ blobs: pluginBlobStore(blobStore, { org: "tenant-b", user: null }, "apps"), pluginStorage: makeMemoryPluginStorage(), }); const key = yield* orgStore.putBlob("same app", "org"); expect(yield* orgStore.getBlob(key)).toBe("same app"); expect(yield* otherStore.getBlob(key)).toBeNull(); }), ); it("declares plugin storage collections", () => { expect(descriptorCollection.name).toBe("apps_descriptors"); expect(toolCollection.name).toBe("apps_tools"); }); });