import { describe, expect, it } from "vitest"; import { createRememberedGrant, createRunGuard, isMigratable, resolveKnotOptions, taskStatusPatches, type Draft } from "#lib/components/import/gitHubImportState.svelte.ts"; import type { MigrationTask } from "#lib/api/migrator.ts"; import { createJobMatcher } from "#lib/components/import/jobMatcher.ts"; describe("createJobMatcher", () => { const repos = [ { id: 1, name: "original-name" }, { id: 2, name: "another-repo" } ]; it("matches renamed repository by draft name when target name differs from github name", () => { const drafts = { 1: { name: "renamed-tangled-repo", rkey: "renamed-tangled-repo" }, 2: { name: "another-repo", rkey: "another-repo" } }; const matched = createJobMatcher(repos, drafts)({ name: "renamed-tangled-repo" }); expect(matched).toEqual({ id: 1, name: "original-name" }); }); it("matches renamed repository case-insensitively by draft rkey", () => { const drafts = { 1: { name: "CamelCaseRepo", rkey: "camelcaserepo" } }; const matched = createJobMatcher(repos, drafts)({ name: "camelcaserepo" }); expect(matched).toEqual({ id: 1, name: "original-name" }); }); it("matches repository by job ID when migration has succeeded", () => { const drafts = { 1: { name: "custom-name", migration: { status: "success", jobId: "job-123" } } }; const matched = createJobMatcher(repos, drafts)({ id: "job-123", name: "unexpected-name" }); expect(matched).toEqual({ id: 1, name: "original-name" }); }); it("falls back to original github repository name when draft name does not match", () => { const drafts = { 2: { name: "another-repo" } }; const matched = createJobMatcher(repos, drafts)({ name: "another-repo" }); expect(matched).toEqual({ id: 2, name: "another-repo" }); }); it("returns undefined when no repository or draft matches", () => { const matched = createJobMatcher(repos, {})({ name: "unknown-repo" }); expect(matched).toBeUndefined(); }); }); describe("resolveKnotOptions", () => { it("resolves array synchronously", async () => { const result = await resolveKnotOptions(["knot1.test", "knot2.test"]); expect(result).toEqual(["knot1.test", "knot2.test"]); }); it("resolves promise asynchronously before returning", async () => { const promise = Promise.resolve(["async-knot.test"]); const result = await resolveKnotOptions(promise); expect(result).toEqual(["async-knot.test"]); }); }); describe("isMigratable", () => { it.each([ ["no attempt yet", undefined, true], ["idle", { status: "idle" }, true], ["failed attempt", { status: "error" }, true], ["queued", { status: "queued" }, false], ["cloning", { status: "cloning" }, false], ["pushing", { status: "pushing" }, false], ["submitting", { status: "submitting" }, false], ["landed", { status: "success", jobStatus: "completed" }, false], ["landed but failed", { status: "success", jobStatus: "failed" }, true], [ "landed but needs authorization", { status: "success", jobStatus: "authorization_required" }, true ] ])("treats a row with %s as migratable: %s", (_name, migration, expected) => { expect(isMigratable(migration as never)).toBe(expected); }); }); describe("createRunGuard", () => { it("lets the newest run own the busy flag", () => { const guard = createRunGuard(); const first = guard.begin(); expect(guard.owns(first)).toBe(true); const second = guard.begin(); expect(guard.owns(first)).toBe(false); expect(guard.owns(second)).toBe(true); }); it("keeps ownership when nothing newer started", () => { const guard = createRunGuard(); const only = guard.begin(); expect(guard.owns(only)).toBe(true); expect(guard.owns(only)).toBe(true); }); }); describe("createRememberedGrant", () => { const fakeStorage = () => { const map = new Map(); return { map, getItem: (k: string) => map.get(k) ?? null, setItem: (k: string, v: string) => void map.set(k, v), removeItem: (k: string) => void map.delete(k) }; }; it("remembers a confirmed grant across loads", () => { const storage = fakeStorage(); const latch = createRememberedGrant(storage, "k"); expect(latch.restore()).toBe(false); latch.confirm(); expect(storage.map.get("k")).toBe("true"); expect(createRememberedGrant(storage, "k").restore()).toBe(true); }); it("forgets the grant when the owner disconnects", () => { const storage = fakeStorage(); const latch = createRememberedGrant(storage, "k"); latch.confirm(); latch.clear(); expect(latch.restore()).toBe(false); }); it("survives a storage that refuses to write", () => { const hostile = { getItem: () => null, setItem: () => { throw new Error("private mode"); }, removeItem: () => { throw new Error("private mode"); } }; const latch = createRememberedGrant(hostile, "k"); expect(() => latch.confirm()).not.toThrow(); expect(() => latch.clear()).not.toThrow(); expect(createRememberedGrant(null, "k").restore()).toBe(false); }); }); describe("taskStatusPatches", () => { const repos = [ { id: 1, name: "alpha" }, { id: 2, name: "beta" } ]; const job = (id: string, name: string, status: string) => ({ id, name, status, attempts: 1, knotDid: "did:web:knot.example" as const, private: false, sourceUrl: `https://github.com/acme/${name}` as const }); const task = (id: string, jobs: ReturnType[]): MigrationTask => ({ id, createdAt: "2026-10-01T00:00:00.000Z", ownerDid: "did:plc:owner", jobs }); const draft = (name: string, taskId: string, jobId: string, jobStatus: string): Draft => ({ selected: false, name, description: "", knot: "knot.example", rkey: name, migration: { status: "success", task: { taskId, name }, jobId, jobStatus } }); it("updates the repos the task is running", () => { const drafts = { 1: draft("alpha", "t1", "a", "queued"), 2: draft("beta", "t1", "b", "queued") }; const patches = taskStatusPatches( task("t1", [job("a", "alpha", "cloning"), job("b", "beta", "failed")]), repos, drafts ); expect(patches.map(([id, m]) => [id, m.status === "success" && m.jobStatus])).toEqual([ [1, "cloning"], [2, "failed"] ]); }); it("leaves a retried repo to its new task when the old batch reports in", () => { const drafts = { 1: draft("alpha", "t2", "a2", "cloning"), 2: draft("beta", "t1", "b", "importing") }; const patches = taskStatusPatches( task("t1", [job("a", "alpha", "failed"), job("b", "beta", "completed")]), repos, drafts ); expect(patches.map(([id]) => id)).toEqual([2]); }); it("tracks the retried repo from its new task", () => { const drafts = { 1: draft("alpha", "t2", "a2", "queued") }; const [[id, migration]] = taskStatusPatches( task("t2", [job("a2", "alpha", "importing")]), repos, drafts ); expect(id).toBe(1); expect(migration).toMatchObject({ jobId: "a2", jobStatus: "importing" }); }); });