diff --git a/plugins/mangabaka/src/map.ts b/plugins/mangabaka/src/map.ts index 1041eff..ea03e0e 100644 --- a/plugins/mangabaka/src/map.ts +++ b/plugins/mangabaka/src/map.ts @@ -13,7 +13,7 @@ export const STATE_MAP: Record = { }; export function mapState(s: string | null | undefined): Status { - return (s != null && STATE_MAP[s]) || "none"; + return s != null && Object.hasOwn(STATE_MAP, s) ? STATE_MAP[s]! : "none"; } function hint(e: ApiEntry): WorkHint { diff --git a/plugins/mangabaka/src/pull.ts b/plugins/mangabaka/src/pull.ts index 0f588ae..e62b4ad 100644 --- a/plugins/mangabaka/src/pull.ts +++ b/plugins/mangabaka/src/pull.ts @@ -18,8 +18,9 @@ export async function* pullChanges(ctx: PluginContext): AsyncIterable(`${BASE_URL}/v2/my/library/changes`, { headers, query: { cursor }, signal: ctx.signal }); for (const c of res.data) { - if (c.type === "delete" || !c.entry) yield* mapDeletion(c.series_id); - else yield* mapEntry(c.entry); + if (c.type === "delete") yield* mapDeletion(c.series_id); + else if (c.entry) yield* mapEntry(c.entry); + else ctx.log(`mangabaka: upsert without entry for series ${c.series_id}, skipped`); } const next = res.next_cursor; if (next) { diff --git a/plugins/mangabaka/test/mangabaka.test.ts b/plugins/mangabaka/test/mangabaka.test.ts index 18446ea..4e438fa 100644 --- a/plugins/mangabaka/test/mangabaka.test.ts +++ b/plugins/mangabaka/test/mangabaka.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"; import { collect, createFixtureHttp, createTestContext } from "@sttracker/plugin-sdk/testing"; import type { Status } from "@sttracker/plugin-sdk/types"; import plugin from "../index.ts"; -import { mapState } from "../src/map.ts"; +import { mapDeletion, mapState } from "../src/map.ts"; import page1 from "./fixtures/library-page1.json"; import page2 from "./fixtures/library-page2.json"; import changes from "./fixtures/changes.json"; @@ -66,6 +66,25 @@ describe("mangabaka", () => { expect(new URL(h.calls[0]!.url).searchParams.get("cursor")).toBe("cur-1"); }); + test("upsert without entry is skipped and logged, never deleted", async () => { + const logs: string[] = []; + const h = createFixtureHttp({ [`GET ${B}/v2/my/library/changes`]: { data: [{ type: "upsert", series_id: 4 }], next_cursor: "c" } }); + const ctx = createTestContext({ fetch: h.fetch, config: { token: "tok" }, log: (m) => logs.push(m) }); + const recs = await collect(plugin.pullChanges!(ctx)); + expect(recs).toEqual([]); + expect(logs).toEqual(["mangabaka: upsert without entry for series 4, skipped"]); + }); + + test("inherited keys are not states", () => expect(mapState("constructor")).toBe("none")); + + test("delete keys are a subset of keys a full pull produced", async () => { + const { ctx } = ctxFor({ [`GET ${B}/v1/my/library?page=1`]: page1, [`GET ${B}/v1/my/library?page=2`]: page2 }); + const pulled = (await collect(plugin.pull!(ctx))).map((r) => r.key); + const del = mapDeletion(1).map((r) => r.key); + expect(del).toEqual(["series:1", "progress:1"]); + for (const k of del) expect(pulled).toContain(k); + }); + test("lookup searches series", async () => { const { h, ctx } = ctxFor({ [`GET ${B}/v1/series/search`]: search }); const r = await plugin.lookup!(ctx, { title: "Delta", mediaType: "manga" });