Files
gacha-event-tracker/test/tasks-store.test.ts
T
Lucas WintherandClaude Opus 5.5 5a4c746a44 fix(tasks): keep a task whole through import and edit, and fix two counts
Review found that import and edit rebuilt a task from this build's parse.
That stripped any field it didn't know, and import dropped a newer
version's task entirely. Both now keep the record as stored and validate
only to display it.

An event whose goal is points can't be ticked, so it no longer counts as
waiting today: it counts with its week. The month-day field reset
itself to 1 when emptied; it now keeps a draft.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 13:54:29 +02:00

119 lines
5.2 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { mergeAmounts, readableAmounts } from "../src/client/state/useAmounts.ts";
import { editedTask, mergeTasks, visibleTasks } from "../src/client/state/useTasks.ts";
/**
* The two stores weekly and monthly tasks add. Both are the only copy of what
* they hold, so the rules are the repo's usual ones: nothing is dropped on
* load, and an import merges without ever removing or rolling back.
*/
const task = (id: string, title: string, updatedAt: string, game = "zzz") => ({
id, game, title,
period: { unit: "week", startsOn: 1 },
measure: { kind: "days", required: 1 },
at: "2026-10-01T00:00:00.000Z",
updatedAt,
});
const A = "mytask:aaaaaa1111";
const B = "mytask:bbbbbb2222";
// A shape no version of this build understands: a later version's task.
const FUTURE = { ...task("mytask:cccccc3333", "Later", "2026-10-02T00:00:00.000Z"), measure: { kind: "streak", n: 3 } };
describe("tasks", () => {
test("an unreadable stored task is hidden, never dropped", () => {
const raw = { [A]: task(A, "Weekly", "2026-10-02T00:00:00.000Z"), [FUTURE.id]: FUTURE };
expect(visibleTasks(raw).map((t) => t.id)).toEqual([A]);
const merged = mergeTasks(raw, {});
expect(merged[FUTURE.id]).toEqual(FUTURE);
});
test("import keeps both sides and the later edit wins, either direction", () => {
const device = { [A]: task(A, "New name", "2026-10-05T00:00:00.000Z") };
const file = {
[A]: task(A, "Old name", "2026-10-03T00:00:00.000Z"),
[B]: task(B, "Monthly", "2026-10-03T00:00:00.000Z"),
};
const merged = mergeTasks(device, file);
expect(Object.keys(merged).sort()).toEqual([A, B]);
expect((merged[A] as { title: string }).title).toBe("New name");
expect((mergeTasks(file, device)[A] as { title: string }).title).toBe("New name");
});
test("a malformed incoming task is skipped, not let in", () => {
const merged = mergeTasks({}, { [A]: { id: A, title: 7 }, junk: "x" });
expect(merged).toEqual({});
expect(mergeTasks({}, "not an object")).toEqual({});
});
test("visible tasks are ordered by game, then title", () => {
const raw = {
[A]: task(A, "b", "2026-10-02T00:00:00.000Z", "zzz"),
[B]: task(B, "a", "2026-10-02T00:00:00.000Z", "endfield"),
};
expect(visibleTasks(raw).map((t) => t.title)).toEqual(["a", "b"]);
});
});
describe("amounts", () => {
const value = (m: Record<string, Record<string, unknown>>, key: string, period: string) =>
readableAmounts(m[key])[period]?.value;
const T1 = "2026-10-14T00:00:00.000Z";
const T2 = "2026-10-15T00:00:00.000Z";
test("an older backup cannot roll back a newer week", () => {
const device = { [A]: { "2026-10-12": { value: 1900, at: T2 } } };
const file = { [A]: { "2026-10-12": { value: 800, at: T1 } } };
expect(value(mergeAmounts(device, file), A, "2026-10-12")).toBe(1900);
expect(value(mergeAmounts(file, device), A, "2026-10-12")).toBe(1900);
});
test("a period only one side has survives", () => {
const device = { [A]: { "2026-10-12": { value: 1900, at: T2 } } };
const file = { [A]: { "2026-10-05": { value: 2100, at: T1 } }, [B]: { "2026-10-01": { value: 3, at: T1 } } };
const merged = mergeAmounts(device, file);
expect(Object.keys(merged[A]!).sort()).toEqual(["2026-10-05", "2026-10-12"]);
expect(value(merged, B, "2026-10-01")).toBe(3);
});
test("a malformed stored entry is kept on disk and skipped when read", () => {
const stored = { [A]: { "2026-10-12": { value: "lots", at: T1 }, "2026-10-05": { value: 5, at: T1 } } };
const merged = mergeAmounts(stored, {});
expect(merged[A]!["2026-10-12"]).toEqual({ value: "lots", at: T1 });
expect(readableAmounts(merged[A])).toEqual({ "2026-10-05": { value: 5, at: T1 } });
});
test("non-numbers and bad shapes are skipped", () => {
const merged = mergeAmounts({}, {
[A]: { "2026-10-12": { value: "1900", at: T1 }, "2026-10-05": { value: Number.NaN, at: T1 } },
[B]: "nope",
});
expect(merged).toEqual({});
});
});
describe("review fixes: a task's data outlives this build", () => {
test("merge keeps fields this build does not know", () => {
const withExtra = { ...task(A, "Weekly", "2026-10-05T00:00:00.000Z"), notes: "keep me" };
expect(mergeTasks({}, { [A]: withExtra })[A]).toEqual(withExtra);
});
test("a newer version's task in a backup reaches a fresh device", () => {
expect(mergeTasks({}, { [FUTURE.id]: FUTURE })[FUTURE.id]).toEqual(FUTURE);
});
test("but junk without a task id or a timestamp still stays out", () => {
expect(mergeTasks({}, { x: { id: "myevent:abcdef1234", updatedAt: "2026" } })).toEqual({});
expect(mergeTasks({}, { x: { id: "mytask:abcdef1234" } })).toEqual({});
});
test("editing keeps fields this build does not know", () => {
const stored = { ...task(A, "Weekly", "2026-10-05T00:00:00.000Z"), notes: "keep me" };
const next = editedTask(stored, {
game: "zzz", title: "Renamed", period: { unit: "week", startsOn: 1 },
measure: { kind: "days", required: 1 },
}, "2026-10-06T00:00:00.000Z");
expect(next).toMatchObject({ title: "Renamed", notes: "keep me", updatedAt: "2026-10-06T00:00:00.000Z" });
});
});