146 lines · 4.8 KB · TypeScript
RawDownload
import { Effect } from "effect"; import type { AppDescriptor } from "../pipeline/descriptor";import type { AppsStore } from "../plugin/store";import { BindingError, type ClientResolver, type ConnectionCandidate } from "../plugin/bindings";import {  ArtifactStoreError,  asSnapshotId,  type ArtifactStore,  type FileSet,  type ScopeArtifactStore,  type SnapshotId,} from "../seams/artifact-store";import { scopeAddressStorageKey } from "../seams/scope-address"; export * from "./daily-brief"; export const makeInMemoryAppsStore = (): AppsStore & {  readonly descriptors: Map<string, AppDescriptor>;} => {  const descriptors = new Map<string, AppDescriptor>();  const publishedAt = new Map<string, number>();  const keyFor = (tenant: string, key: string): string => `${tenant}:${key}`;  return {    descriptors,    putDescriptor: (tenant, _owner, descriptor) =>      Effect.sync(() => {        const key = keyFor(tenant, descriptor.scope);        descriptors.set(key, descriptor);        publishedAt.set(key, Date.now());      }),    getDescriptor: (tenant, scope) =>      Effect.sync(() => descriptors.get(keyFor(tenant, scope)) ?? null),    removeDescriptor: (tenant, scope) =>      Effect.sync(() => {        const key = keyFor(tenant, scope);        descriptors.delete(key);        publishedAt.delete(key);      }),    listDescriptors: (tenant) =>      Effect.sync(() =>        [...descriptors.entries()]          .filter(([key]) => key.startsWith(`${tenant}:`))          .map(([key, descriptor]) => ({            descriptor,            publishedAt: publishedAt.get(key) ?? 0,          })),      ),  };}; export const makeInMemoryArtifactStore = (): ArtifactStore => {  const scopes = new Map<string, Map<string, FileSet>>();  const order = new Map<string, string[]>();  let counter = 0;  const forScope = (key: string): ScopeArtifactStore => {    const snaps = scopes.get(key) ?? new Map<string, FileSet>();    scopes.set(key, snaps);    const seq = order.get(key) ?? [];    order.set(key, seq);    return {      commit: (files, message) =>        Effect.sync(() => {          const id = `mem${(++counter).toString(16).padStart(40, "0")}`;          snaps.set(id, new Map(files));          seq.push(id);          return { id: asSnapshotId(id), message, committedAt: Date.now() };        }),      read: (id) =>        snaps.has(id)          ? Effect.succeed(snaps.get(id)!)          : Effect.fail(new ArtifactStoreError({ message: `no snapshot ${id}` })),      readFile: (id, path) => Effect.succeed(snaps.get(id)?.get(path) ?? null),      list: (id) => Effect.succeed([...(snaps.get(id)?.keys() ?? [])]),      latest: () =>        Effect.sync(() => {          const last = seq.at(-1);          if (!last) return null;          return { id: asSnapshotId(last), message: "latest", committedAt: Date.now() };        }),      log: (limit) =>        Effect.sync(() =>          [...seq]            .reverse()            .slice(0, limit ?? seq.length)            .map((id) => ({ id: asSnapshotId(id), message: "", committedAt: Date.now() })),        ),    };  };  return {    forScope: (address) => Effect.succeed(forScope(scopeAddressStorageKey(address))),    removeScope: (address) =>      Effect.sync(() => {        const key = scopeAddressStorageKey(address);        scopes.delete(key);        order.delete(key);      }),  };}; export type { SnapshotId }; export const makeTestResolver = (  handlers: Record<string, Record<string, (args: readonly unknown[]) => unknown>>,  connections?: readonly ConnectionCandidate[],): ClientResolver & {  readonly calls: { integration: string; connection: string; method: string }[];} => {  const calls: { integration: string; connection: string; method: string }[] = [];  const knownConnections =    connections ??    Object.keys(handlers).map((integration) => ({      address: `tools.${integration}.user.${integration}`,      integration,      name: integration,      owner: "user",    }));  return {    calls,    listConnections: ({ integration }) =>      Effect.succeed(        knownConnections.filter((connection) => connection.integration === integration),      ),    resolveConnection: ({ connection }) =>      Effect.succeed(        knownConnections.find(          (candidate) => candidate.address === connection || candidate.name === connection,        ) ?? null,      ),    call: ({ integration, connection, path, args }) => {      const method = path.join(".");      calls.push({ integration, connection, method });      const handler = handlers[integration]?.[method];      if (!handler) {        return Effect.fail(          new BindingError({            message: `no test handler for ${integration}.${method}`,            role: integration,            integration,          }),        );      }      return Effect.sync(() => handler(args));    },  };};