import { describe, it } from "@effect/vitest"; import { expect } from "vitest"; import { beforeAll, afterAll, beforeEach } from "vitest"; import { Effect } from "effect"; import { PGlite } from "@electric-sql/pglite"; import { drizzle } from "drizzle-orm/pglite"; import { migrate } from "drizzle-orm/pglite/migrator"; import { sql } from "drizzle-orm"; import { fileURLToPath } from "node:url"; import { dirname, resolve } from "node:path"; import { createExecutor, ScopeId, ToolId, SecretId, ToolRegistration, scopeKv, type Executor, } from "@executor/sdk"; import { makePgConfig } from "./index"; import { makePgKv } from "./pg-kv"; import { makeUserStore } from "./user-store"; import * as schema from "./schema"; // --------------------------------------------------------------------------- // Test setup — in-memory Postgres via PGlite + Drizzle migrations // --------------------------------------------------------------------------- const TEST_TEAM_ID = "test-team-1"; const TEST_TEAM_NAME = "Test Team"; const TEST_ENCRYPTION_KEY = "test-encryption-key-for-unit-tests"; const __dirname = dirname(fileURLToPath(import.meta.url)); const migrationsFolder = resolve(__dirname, "../drizzle"); let client: PGlite; let db: ReturnType; beforeAll(async () => { client = new PGlite(); db = drizzle(client, { schema }); await migrate(db, { migrationsFolder }); }); beforeEach(async () => { await db.execute( sql`TRUNCATE plugin_kv, policies, secrets, tool_definitions, tools, sources, sessions, invitations, team_members, teams, users`, ); }); afterAll(async () => { await client.close(); }); // --------------------------------------------------------------------------- // Helper — create executor from PgConfig // --------------------------------------------------------------------------- const makeTestExecutor = () => { const config = makePgConfig(db, { teamId: TEST_TEAM_ID, teamName: TEST_TEAM_NAME, encryptionKey: TEST_ENCRYPTION_KEY, }); return createExecutor(config); }; const makeTestExecutorForTeam = (teamId: string, teamName: string) => { const config = makePgConfig(db, { teamId, teamName, encryptionKey: TEST_ENCRYPTION_KEY, }); return createExecutor(config); }; // --------------------------------------------------------------------------- // Executor via makePgConfig // --------------------------------------------------------------------------- describe("Executor with Postgres storage", () => { it.effect("scope reflects team", () => Effect.gen(function* () { const executor = yield* makeTestExecutor(); expect(executor.scope.id).toBe(TEST_TEAM_ID); expect(executor.scope.name).toBe(TEST_TEAM_NAME); }), ); // --- Tools --- it.effect("register and list tools", () => Effect.gen(function* () { const executor = yield* makeTestExecutor(); // Register tools via the underlying registry (plugins do this) const config = makePgConfig(db, { teamId: TEST_TEAM_ID, teamName: TEST_TEAM_NAME, encryptionKey: TEST_ENCRYPTION_KEY, }); yield* config.tools.register([ new ToolRegistration({ id: ToolId.make("t1"), pluginKey: "test", sourceId: "src-a", name: "tool-one", description: "First tool", }), new ToolRegistration({ id: ToolId.make("t2"), pluginKey: "test", sourceId: "src-b", name: "tool-two", }), ]); const all = yield* executor.tools.list(); expect(all).toHaveLength(2); const filtered = yield* executor.tools.list({ sourceId: "src-a" }); expect(filtered).toHaveLength(1); expect(filtered[0]!.name).toBe("tool-one"); }), ); it.effect("query filter on tools", () => Effect.gen(function* () { const executor = yield* makeTestExecutor(); const config = makePgConfig(db, { teamId: TEST_TEAM_ID, teamName: TEST_TEAM_NAME, encryptionKey: TEST_ENCRYPTION_KEY, }); yield* config.tools.register([ new ToolRegistration({ id: ToolId.make("a"), pluginKey: "test", sourceId: "test-src", name: "create-user", description: "Creates a user", }), new ToolRegistration({ id: ToolId.make("b"), pluginKey: "test", sourceId: "test-src", name: "delete-user", }), ]); const results = yield* executor.tools.list({ query: "creates" }); expect(results).toHaveLength(1); expect(results[0]!.name).toBe("create-user"); }), ); // --- Secrets --- it.effect("set and resolve secrets", () => Effect.gen(function* () { const executor = yield* makeTestExecutor(); yield* executor.secrets.set({ id: SecretId.make("api-key"), name: "API Key", value: "sk-12345", purpose: "auth", }); const resolved = yield* executor.secrets.resolve(SecretId.make("api-key")); expect(resolved).toBe("sk-12345"); }), ); it.effect("list and remove secrets", () => Effect.gen(function* () { const executor = yield* makeTestExecutor(); yield* executor.secrets.set({ id: SecretId.make("rm-me"), name: "Removable", value: "val", }); const listed = yield* executor.secrets.list(); expect(listed).toHaveLength(1); yield* executor.secrets.remove(SecretId.make("rm-me")); expect(yield* executor.secrets.list()).toHaveLength(0); }), ); it.effect("secret status check", () => Effect.gen(function* () { const executor = yield* makeTestExecutor(); const missing = yield* executor.secrets.status(SecretId.make("no-exist")); expect(missing).toBe("missing"); yield* executor.secrets.set({ id: SecretId.make("exists"), name: "Exists", value: "v", }); const resolved = yield* executor.secrets.status(SecretId.make("exists")); expect(resolved).toBe("resolved"); }), ); it.effect("encryption with wrong key fails to resolve", () => Effect.gen(function* () { const executor1 = yield* makeTestExecutorForTeam(TEST_TEAM_ID, TEST_TEAM_NAME); yield* executor1.secrets.set({ id: SecretId.make("enc-test"), name: "Encrypted", value: "secret-value", }); // Create executor with different encryption key const config2 = makePgConfig(db, { teamId: TEST_TEAM_ID, teamName: TEST_TEAM_NAME, encryptionKey: "wrong-key", }); const executor2 = yield* createExecutor(config2); const result = yield* executor2.secrets.resolve(SecretId.make("enc-test")).pipe( Effect.either, ); expect(result._tag).toBe("Left"); }), ); // --- Policies --- it.effect("add and list policies", () => Effect.gen(function* () { const executor = yield* makeTestExecutor(); const policy = yield* executor.policies.add({ scopeId: ScopeId.make(TEST_TEAM_ID), name: "allow-t1", action: "allow" as const, match: { toolPattern: "t1" }, priority: 0, }); expect(policy.id).toBeDefined(); const listed = yield* executor.policies.list(); expect(listed).toHaveLength(1); }), ); it.effect("remove policies", () => Effect.gen(function* () { const executor = yield* makeTestExecutor(); const policy = yield* executor.policies.add({ scopeId: ScopeId.make(TEST_TEAM_ID), name: "allow-t1", action: "allow" as const, match: { toolPattern: "t1" }, priority: 0, }); expect(yield* executor.policies.remove(policy.id)).toBe(true); expect(yield* executor.policies.list()).toHaveLength(0); }), ); // --- Team isolation --- it.effect("team isolation — tools", () => Effect.gen(function* () { const configA = makePgConfig(db, { teamId: "team-a", teamName: "Team A", encryptionKey: TEST_ENCRYPTION_KEY }); const configB = makePgConfig(db, { teamId: "team-b", teamName: "Team B", encryptionKey: TEST_ENCRYPTION_KEY }); yield* configA.tools.register([ new ToolRegistration({ id: ToolId.make("t1"), pluginKey: "test", sourceId: "src", name: "team-a-tool" }), ]); yield* configB.tools.register([ new ToolRegistration({ id: ToolId.make("t1"), pluginKey: "test", sourceId: "src", name: "team-b-tool" }), ]); const executorA = yield* createExecutor(configA); const executorB = yield* createExecutor(configB); const aTools = yield* executorA.tools.list(); expect(aTools).toHaveLength(1); expect(aTools[0]!.name).toBe("team-a-tool"); const bTools = yield* executorB.tools.list(); expect(bTools).toHaveLength(1); expect(bTools[0]!.name).toBe("team-b-tool"); }), ); it.effect("team isolation — secrets", () => Effect.gen(function* () { const executorA = yield* makeTestExecutorForTeam("team-a", "Team A"); const executorB = yield* makeTestExecutorForTeam("team-b", "Team B"); yield* executorA.secrets.set({ id: SecretId.make("shared-id"), name: "Team A Secret", value: "a-value", }); yield* executorB.secrets.set({ id: SecretId.make("shared-id"), name: "Team B Secret", value: "b-value", }); expect(yield* executorA.secrets.resolve(SecretId.make("shared-id"))).toBe("a-value"); expect(yield* executorB.secrets.resolve(SecretId.make("shared-id"))).toBe("b-value"); }), ); // --- Plugin KV (escape hatch) --- it.effect("plugin KV works via scopeKv", () => Effect.gen(function* () { const kv = makePgKv(db, TEST_TEAM_ID); const scoped = scopeKv(kv, "my-plugin"); yield* scoped.set("k1", "v1"); expect(yield* scoped.get("k1")).toBe("v1"); const items = yield* scoped.list(); expect(items).toHaveLength(1); yield* scoped.delete("k1"); expect(yield* scoped.get("k1")).toBeNull(); }), ); it.effect("plugin KV team isolation", () => Effect.gen(function* () { const kv1 = makePgKv(db, "team-a"); const kv2 = makePgKv(db, "team-b"); yield* kv1.set("ns", "key", "team-a-value"); yield* kv2.set("ns", "key", "team-b-value"); expect(yield* kv1.get("ns", "key")).toBe("team-a-value"); expect(yield* kv2.get("ns", "key")).toBe("team-b-value"); }), ); // --- Close --- it.effect("executor closes cleanly", () => Effect.gen(function* () { const executor = yield* makeTestExecutor(); yield* executor.close(); }), ); }); // --------------------------------------------------------------------------- // User Store (not part of Executor, tested directly) // --------------------------------------------------------------------------- describe("UserStore", () => { it("upsert and get user", async () => { const store = makeUserStore(db); const user = await store.upsertUser({ id: "user-1", email: "test@example.com", name: "Test User", }); expect(user.id).toBe("user-1"); const fetched = await store.getUser("user-1"); expect(fetched!.name).toBe("Test User"); await store.upsertUser({ id: "user-1", email: "test@example.com", name: "Updated" }); expect((await store.getUser("user-1"))!.name).toBe("Updated"); }); it("create team and manage members", async () => { const store = makeUserStore(db); await store.upsertUser({ id: "u1", email: "a@example.com", name: "A" }); await store.upsertUser({ id: "u2", email: "b@example.com", name: "B" }); const team = await store.createTeam("My Team"); await store.addMember(team.id, "u1", "owner"); await store.addMember(team.id, "u2", "member"); expect(await store.listMembers(team.id)).toHaveLength(2); const teams = await store.getTeamsForUser("u1"); expect(teams[0]!.teamName).toBe("My Team"); await store.removeMember(team.id, "u2"); expect(await store.listMembers(team.id)).toHaveLength(1); }); it("invitations workflow", async () => { const store = makeUserStore(db); await store.upsertUser({ id: "u1", email: "owner@example.com" }); const team = await store.createTeam("Team"); await store.addMember(team.id, "u1", "owner"); const invitation = await store.createInvitation(team.id, "new@example.com", "u1"); expect(invitation.status).toBe("pending"); expect(await store.getPendingInvitations("new@example.com")).toHaveLength(1); await store.acceptInvitation(invitation.id); expect(await store.getPendingInvitations("new@example.com")).toHaveLength(0); }); it("session management", async () => { const store = makeUserStore(db); await store.upsertUser({ id: "u1", email: "a@example.com" }); const team = await store.createTeam("Team"); const session = await store.createSession("u1", team.id, new Date(Date.now() + 3600_000)); expect((await store.getSession(session.id))!.userId).toBe("u1"); await store.deleteSession(session.id); expect(await store.getSession(session.id)).toBeNull(); }); it("expired sessions return null", async () => { const store = makeUserStore(db); await store.upsertUser({ id: "u1", email: "a@example.com" }); const team = await store.createTeam("Team"); const session = await store.createSession("u1", team.id, new Date(Date.now() - 1000)); expect(await store.getSession(session.id)).toBeNull(); }); });