import { describe, expect, test } from "bun:test"; import type { CapabilityContext } from "../capabilities/context"; import { mapUnassignedWork, suggestProjectForGroup, } from "../capabilities/mapping"; import { mapUnassignedGroup } from "./classify-unassigned"; // A git event whose metadata satisfies isGitMetadata and carries a gitRemote. function gitEvent(gitRemote: string, id = "e1") { return { id, source: "git", deviceId: "d1", metadata: { message: "commit", author: "tester", filesChanged: ["a.ts"], gitRemote, }, }; } /** * DB double for mapUnassignedGroup. `selectQueue` feeds each awaited * select/where in call order; insert().returning() echoes the captured values * with a generated id; updates/deletes are captured. The chain tracks the last * verb so `where()` resolves rows for a SELECT and undefined for update/delete. */ function makeDb(selectQueue: unknown[][]) { let si = 0; let mode: "select" | "insert" | "update" | "delete" | null = null; let insVals: Record | null = null; const captured = { inserts: [] as Array>, updates: [] as Array>, deletes: 0, }; // biome-ignore lint/suspicious/noExplicitAny: minimal chainable test double const chain: any = { select: () => { mode = "select"; return chain; }, from: () => chain, insert: () => { mode = "insert"; return chain; }, values: (v: Record) => { insVals = v; captured.inserts.push(v); return chain; }, update: () => { mode = "update"; return chain; }, set: (v: Record) => { captured.updates.push(v); return chain; }, delete: () => { mode = "delete"; return chain; }, returning: () => Promise.resolve([{ id: "gen-id", ...(insVals ?? {}) }]), where: () => { if (mode === "select") return Promise.resolve(selectQueue[si++] ?? []); if (mode === "delete") captured.deletes++; return Promise.resolve(undefined); }, }; return { chain, captured }; } const db = (chain: unknown) => chain as CapabilityContext["db"]; describe("mapUnassignedGroup", () => { test("create: inserts a project + a project_root for the path and re-points matching events", async () => { const { chain, captured } = makeDb([[gitEvent("github.com/o/r")]]); const result = await mapUnassignedGroup(db(chain), { groupKey: "git:github.com/o/r", path: "github.com/o/r", action: "create", projectName: "My Repo", organizationId: "org1", deviceIds: ["d1"], manuallyAssigned: true, }); if ("error" in result) throw new Error(result.error); expect(result.eventsUpdated).toBe(1); // project insert (name from newProjectName, scoped to org) + root insert. const projectInsert = captured.inserts.find((v) => v.name === "My Repo"); const rootInsert = captured.inserts.find( (v) => v.path === "github.com/o/r", ); expect(projectInsert?.organizationId).toBe("org1"); expect(rootInsert?.manuallyAssigned).toBe(true); // git group → remote stamped on the root so future git scans reconnect. expect(rootInsert?.gitRemote).toBe("github.com/o/r"); // root points at the new project, and the event points at the new root. expect(rootInsert?.projectId).toBe(projectInsert?.id); expect(captured.updates[0]?.projectRootId).toBe(rootInsert?.id); }); test("assign: verifies the project in-org, points an existing root, re-points events", async () => { const { chain, captured } = makeDb([ [gitEvent("github.com/o/r")], // unassigned events [{ id: "p1" }], // project lookup [{ id: "root1", manuallyAssigned: false }], // existing root for the path ]); const result = await mapUnassignedGroup(db(chain), { groupKey: "git:github.com/o/r", path: "github.com/o/r", action: "assign", projectId: "p1", organizationId: "org1", deviceIds: ["d1"], manuallyAssigned: true, }); if ("error" in result) throw new Error(result.error); expect(result.projectId).toBe("p1"); expect(result.projectRootId).toBe("root1"); // the existing root was updated to point at p1 + marked manual. const rootUpdate = captured.updates.find((u) => u.projectId === "p1"); expect(rootUpdate?.manuallyAssigned).toBe(true); // and the event re-pointed to root1. expect(captured.updates.some((u) => u.projectRootId === "root1")).toBe( true, ); }); test("delete: removes the group's matching events", async () => { const { chain, captured } = makeDb([[gitEvent("github.com/o/r")]]); const result = await mapUnassignedGroup(db(chain), { groupKey: "git:github.com/o/r", path: "github.com/o/r", action: "delete", deviceIds: ["d1"], }); if ("error" in result) throw new Error(result.error); expect(result.eventsDeleted).toBe(1); expect(captured.deletes).toBe(1); }); test("errors when no events match the group", async () => { const { chain } = makeDb([[gitEvent("github.com/other/repo")]]); const result = await mapUnassignedGroup(db(chain), { groupKey: "git:github.com/o/r", path: "github.com/o/r", action: "create", projectName: "X", deviceIds: ["d1"], }); expect(result).toMatchObject({ error: expect.stringContaining("No matching"), }); }); test("assign rejects a project outside the caller's org", async () => { const { chain } = makeDb([ [gitEvent("github.com/o/r")], [], // project lookup returns nothing (wrong org) ]); const result = await mapUnassignedGroup(db(chain), { groupKey: "git:github.com/o/r", path: "github.com/o/r", action: "assign", projectId: "foreign", organizationId: "org1", deviceIds: ["d1"], }); expect(result).toMatchObject({ error: expect.stringContaining("not found"), }); }); test("org member with no devices maps nothing", async () => { const { chain } = makeDb([]); const result = await mapUnassignedGroup(db(chain), { groupKey: "git:github.com/o/r", path: "github.com/o/r", action: "create", projectName: "X", deviceIds: [], }); expect(result).toMatchObject({ error: expect.stringContaining("No matching"), }); }); }); describe("mapUnassignedWork capability guards", () => { const ctx = { db: undefined as unknown as CapabilityContext["db"], userId: "u1", organizationId: "org1", deviceIds: ["d1"], }; test("requires exactly one of projectId / newProjectName", async () => { const neither = await mapUnassignedWork.execute(ctx, { groupKey: "git:x", path: "x", }); expect(neither).toMatchObject({ error: expect.stringContaining("either projectId"), }); const both = await mapUnassignedWork.execute(ctx, { groupKey: "git:x", path: "x", projectId: "p1", newProjectName: "New", }); expect(both).toMatchObject({ error: expect.stringContaining("only one") }); }); }); describe("suggestProjectForGroup", () => { const projects = [ { id: "p1", name: "bopen" }, { id: "p2", name: "scribe" }, { id: "p3", name: "1sat" }, ]; test("matches on exact normalized name", () => { expect(suggestProjectForGroup("scribe", projects)?.id).toBe("p2"); }); test("matches when the group name extends the project name", () => { // "bopen-ai" normalizes to "bopenai", project "bopen" is a prefix expect(suggestProjectForGroup("bopen-ai", projects)?.id).toBe("p1"); // punctuation/case ignored: "1Sat Ordinals"? -> "1sat..." starts with "1sat" expect(suggestProjectForGroup("1sat-sdk", projects)?.id).toBe("p3"); }); test("returns null when nothing overlaps", () => { expect(suggestProjectForGroup("minerva", projects)).toBeNull(); expect(suggestProjectForGroup("", projects)).toBeNull(); }); });