fix(plugins): mangabaka never deletes on upsert without entry

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Lucas Winther
2026-10-30 15:50:32 +01:00
co-authored by Claude Opus 5.5
parent ddd938935e
commit f30c0db2b1
3 changed files with 24 additions and 4 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ export const STATE_MAP: Record<string, Status> = {
};
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 {
+3 -2
View File
@@ -18,8 +18,9 @@ export async function* pullChanges(ctx: PluginContext): AsyncIterable<SourceReco
for (;;) {
const res = await ctx.http.json<ChangesPage>(`${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) {
+20 -1
View File
@@ -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" });