import { describe, expect, test } from "bun:test"; import { computeCloudProjectMappings, computeProjectReconciliation, type GroupLike, getCloudBackedGroupKeys, getResidualMappingIds, isResidualMatchType, type ProjectSuggestion, type RootLike, summarizeCloudMatchBasis, type UnassignedRootMapping, } from "./mapping"; function root(partial: Partial & { id: string }): RootLike { return { path: `/Users/dev/code/${partial.id}`, detectedName: null, gitRemote: null, ...partial, }; } describe("computeProjectReconciliation orgOnly", () => { test("flags cloud projects with no local presence as org-only", () => { const suggestions: ProjectSuggestion[] = [ { name: "1sat", gitRemotes: ["github.com/b-open-io/1sat-website"] }, ]; const { orgOnly } = computeProjectReconciliation([], [], suggestions); expect(orgOnly.map((p) => p.name)).toEqual(["1sat"]); expect(orgOnly[0].gitRemoteCount).toBe(1); }); test("does NOT flag a cloud project matched by an ungrouped root's git remote", () => { // Reproduces OPL-2582: user has 0 groups but an ungrouped root whose // remote is a member of the cloud umbrella project's repos. const suggestions: ProjectSuggestion[] = [ { name: "1sat", gitRemotes: [ "https://github.com/b-open-io/1sat-website", "https://github.com/bitcoinschema/js-1sat-ord", ], }, ]; const ungrouped: RootLike[] = [ root({ id: "1sat-website", path: "/Users/dev/code/1sat-website", gitRemote: "git@github.com:b-open-io/1sat-website.git", }), ]; const { orgOnly } = computeProjectReconciliation( [], ungrouped, suggestions, ); expect(orgOnly).toHaveLength(0); }); test("does NOT flag a cloud project matched by an ungrouped root's name", () => { const suggestions: ProjectSuggestion[] = [{ name: "bmap", gitRemotes: [] }]; const ungrouped: RootLike[] = [ root({ id: "bmap", path: "/Users/dev/code/bmap" }), ]; const { orgOnly } = computeProjectReconciliation( [], ungrouped, suggestions, ); expect(orgOnly).toHaveLength(0); }); test("still flags a cloud project when no ungrouped root matches", () => { const suggestions: ProjectSuggestion[] = [ { name: "ordfs", gitRemotes: ["github.com/b-open-io/ordfs"] }, ]; const ungrouped: RootLike[] = [ root({ id: "unrelated", path: "/Users/dev/code/unrelated", gitRemote: "git@github.com:someone/unrelated.git", }), ]; const { orgOnly } = computeProjectReconciliation( [], ungrouped, suggestions, ); expect(orgOnly.map((p) => p.name)).toEqual(["ordfs"]); }); test("existing group matches still suppress org-only", () => { const suggestions: ProjectSuggestion[] = [ { name: "bopen", gitRemotes: ["github.com/b-open-io/bopen"] }, ]; const groups: GroupLike[] = [ { id: "g1", name: "bopen", roots: [root({ id: "bopen", path: "/Users/dev/code/bopen" })], }, ]; const { orgOnly } = computeProjectReconciliation( groups, [] as RootLike[], suggestions, ); expect(orgOnly).toHaveLength(0); }); }); describe("getCloudBackedGroupKeys", () => { test("keeps groups matched to cloud projects by name or normalized git remote", () => { const groups: GroupLike[] = [ { id: "name-match", name: "Cloud Name", roots: [root({ id: "name-root" })], }, { id: "remote-match", name: "Renamed Locally", roots: [ root({ id: "remote-root", gitRemote: "git@github.com:org/cloud-repo.git", }), ], }, { id: "local-only", name: "Local Only", roots: [root({ id: "local-root" })], }, ]; const suggestions: ProjectSuggestion[] = [ { name: "cloud name", gitRemotes: [] }, { name: "Remote Cloud Project", gitRemotes: ["https://github.com/org/cloud-repo"], }, ]; expect([...getCloudBackedGroupKeys(groups, suggestions)]).toEqual([ "Cloud Name", "Renamed Locally", ]); }); }); describe("getResidualMappingIds", () => { function mapping( id: string, matchType: UnassignedRootMapping["matchType"], targetId: string | null, ): UnassignedRootMapping { return { root: root({ id }), targetGroup: { id: targetId, name: "Target" }, matchType, }; } test("residual match types are exactly the new-group tiers", () => { expect(isResidualMatchType("local-new-git")).toBe(true); expect(isResidualMatchType("local-new-name")).toBe(true); expect(isResidualMatchType("local-git")).toBe(false); expect(isResidualMatchType("org-git")).toBe(false); expect(isResidualMatchType("local-name")).toBe(false); expect(isResidualMatchType("org-name")).toBe(false); }); test("scopes AI to only the roots with no confident existing-group target", () => { const mappings: UnassignedRootMapping[] = [ mapping("git-exact", "local-git", "grp-1"), mapping("name-existing", "local-name", "grp-2"), mapping("org-git", "org-git", "grp-3"), mapping("novel-git", "local-new-git", null), mapping("novel-name", "local-new-name", null), ]; expect([...getResidualMappingIds(mappings)].sort()).toEqual([ "novel-git", "novel-name", ]); }); test("returns an empty set when every root has a deterministic target", () => { const mappings: UnassignedRootMapping[] = [ mapping("a", "local-git", "grp-1"), mapping("b", "org-name", "grp-2"), ]; expect(getResidualMappingIds(mappings).size).toBe(0); }); }); describe("computeCloudProjectMappings", () => { test("classifies a cloud project matched by git remote (not name) as mappable", () => { const suggestions: ProjectSuggestion[] = [ { name: "1sat", gitRemotes: ["https://github.com/b-open-io/1sat-website"], }, ]; const ungrouped: RootLike[] = [ root({ id: "r1", // Folder name differs from the cloud project name — only the git // remote ties them together. path: "/Users/dev/code/website-local", gitRemote: "git@github.com:b-open-io/1sat-website.git", }), ]; const { unassigned, orgOnly } = computeProjectReconciliation( [], ungrouped, suggestions, ); const mappings = computeCloudProjectMappings( suggestions, unassigned, orgOnly, ); expect(mappings).toHaveLength(1); expect(mappings[0].status).toBe("mappable"); expect(mappings[0].rootIds).toEqual(["r1"]); // Only the git remote ties them together — folder name differs. expect(mappings[0].matchBasis).toBe("git-remote"); }); test("classifies a cloud project matched by name as mappable", () => { const suggestions: ProjectSuggestion[] = [{ name: "bmap", gitRemotes: [] }]; const ungrouped: RootLike[] = [ root({ id: "r1", path: "/Users/dev/code/bmap" }), ]; const { unassigned, orgOnly } = computeProjectReconciliation( [], ungrouped, suggestions, ); const mappings = computeCloudProjectMappings( suggestions, unassigned, orgOnly, ); expect(mappings[0].status).toBe("mappable"); expect(mappings[0].rootIds).toEqual(["r1"]); // Folder name equals the cloud project name — present locally by path. expect(mappings[0].matchBasis).toBe("path"); }); test("classifies a cloud project with no local presence as unmatched", () => { const suggestions: ProjectSuggestion[] = [ { name: "ghost", gitRemotes: ["github.com/x/ghost"] }, ]; const { unassigned, orgOnly } = computeProjectReconciliation( [], [], suggestions, ); const mappings = computeCloudProjectMappings( suggestions, unassigned, orgOnly, ); expect(mappings[0].status).toBe("unmatched"); expect(mappings[0].rootIds).toEqual([]); expect(mappings[0].matchBasis).toBe("none"); }); test("classifies a cloud project already covered by a group as linked", () => { const suggestions: ProjectSuggestion[] = [ { name: "bopen", gitRemotes: ["github.com/b-open-io/bopen"] }, ]; const groups: GroupLike[] = [ { id: "g1", name: "bopen", roots: [root({ id: "bopen", path: "/Users/dev/code/bopen" })], }, ]; const { unassigned, orgOnly } = computeProjectReconciliation( groups, [] as RootLike[], suggestions, ); const mappings = computeCloudProjectMappings( suggestions, unassigned, orgOnly, ); expect(mappings[0].status).toBe("linked"); expect(mappings[0].rootIds).toEqual([]); // Group name equals the cloud project name — present locally by path. expect(mappings[0].matchBasis).toBe("path"); }); test("classifies a linked-by-git-remote-only project (group name differs) with git-remote basis", () => { // The local group's name differs from the cloud project, but one of its // roots shares the cloud project's git remote — present locally, but only // the remote ties them together, so the basis is git-remote (not path). const suggestions: ProjectSuggestion[] = [ { name: "1sat-website", gitRemotes: ["github.com/b-open-io/1sat-website"], }, ]; const groups: GroupLike[] = [ { id: "g1", name: "website", roots: [ root({ id: "r1", path: "/Users/dev/code/website", gitRemote: "git@github.com:b-open-io/1sat-website.git", }), ], }, ]; const { unassigned, orgOnly } = computeProjectReconciliation( groups, [] as RootLike[], suggestions, ); const mappings = computeCloudProjectMappings( suggestions, unassigned, orgOnly, groups, ); expect(mappings[0].status).toBe("linked"); expect(mappings[0].matchBasis).toBe("git-remote"); }); test("covered project stays linked even when a stray ungrouped sibling reconciles to it", () => { // Regression: a local group already covers the cloud project, but the same // repo was discovered at a second path and is still ungrouped. The stray // sibling must NOT downgrade the linked project to "mappable" (the chain // icon) — the project is mapped; the leftover root maps from its own picker. const suggestions: ProjectSuggestion[] = [ { name: "clawnet", gitRemotes: ["github.com/b-open-io/clawnet"] }, ]; const groups: GroupLike[] = [ { id: "g1", name: "clawnet", roots: [ root({ id: "clawnet-main", path: "/Users/dev/code/clawnet", gitRemote: "github.com/b-open-io/clawnet", }), ], }, ]; const ungrouped: RootLike[] = [ root({ id: "clawnet-wt", path: "/Users/dev/code/clawnet-worktree", gitRemote: "github.com/b-open-io/clawnet", }), ]; const { unassigned, orgOnly } = computeProjectReconciliation( groups, ungrouped, suggestions, ); const mappings = computeCloudProjectMappings( suggestions, unassigned, orgOnly, groups, ); expect(mappings[0].status).toBe("linked"); expect(mappings[0].rootIds).toEqual([]); }); test("does NOT offer a git-remote (org-git) root under a same-named-but-different cloud project", () => { // Regression for OPL-2618 MEDIUM-1: the root's folder name is "foo" (which a // name-only heuristic would match to cloud project "foo"), but its git remote // is owned by cloud project "bar". The engine assigns the root to "bar", so // the per-row "Map" must offer it ONLY under "bar" — never under "foo". const suggestions: ProjectSuggestion[] = [ { name: "foo", gitRemotes: [] }, { name: "bar", gitRemotes: ["https://github.com/org/bar-repo"] }, ]; const ungrouped: RootLike[] = [ root({ id: "r1", path: "/Users/dev/code/foo", gitRemote: "git@github.com:org/bar-repo.git", }), ]; const { unassigned, orgOnly } = computeProjectReconciliation( [], ungrouped, suggestions, ); const mappings = computeCloudProjectMappings( suggestions, unassigned, orgOnly, ); const foo = mappings.find((m) => m.name === "foo"); const bar = mappings.find((m) => m.name === "bar"); expect(bar?.status).toBe("mappable"); expect(bar?.rootIds).toEqual(["r1"]); // The root must NOT be offered under "foo". expect(foo?.rootIds).toEqual([]); expect(foo?.status).not.toBe("mappable"); }); }); describe("summarizeCloudMatchBasis", () => { test("counts cloud projects by match basis", () => { const suggestions: ProjectSuggestion[] = [ // Present locally by name/path -> byPath. { name: "bmap", gitRemotes: [] }, // Same git remote, folder name differs -> byGitRemote. { name: "1sat", gitRemotes: ["https://github.com/b-open-io/1sat-website"], }, // No local presence -> unmatched. { name: "ghost", gitRemotes: ["github.com/x/ghost"] }, ]; const ungrouped: RootLike[] = [ root({ id: "r1", path: "/Users/dev/code/bmap" }), root({ id: "r2", path: "/Users/dev/code/website-local", gitRemote: "git@github.com:b-open-io/1sat-website.git", }), ]; const { unassigned, orgOnly } = computeProjectReconciliation( [], ungrouped, suggestions, ); const mappings = computeCloudProjectMappings( suggestions, unassigned, orgOnly, ); expect(summarizeCloudMatchBasis(mappings)).toEqual({ byPath: 1, byGitRemote: 1, unmatched: 1, }); }); test("returns all zeros for an empty list", () => { expect(summarizeCloudMatchBasis([])).toEqual({ byPath: 0, byGitRemote: 0, unmatched: 0, }); }); });