import { afterAll, beforeAll, expect, test } from "bun:test"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import type { TimeRecord } from "../src/shared/record.ts"; let processHandle: ReturnType; let folder: string; const port = 33000 + Math.floor(Math.random() * 10000); const base = `http://127.0.0.1:${port}`; beforeAll(async () => { folder = await mkdtemp(join(tmpdir(), "timelog-api-test-")); processHandle = Bun.spawn([process.execPath, "src/api/server.ts"], { cwd: process.cwd(), env: { ...process.env, API_PORT: String(port), DB_PATH: join(folder, "test.sqlite"), PUBLIC_ORIGIN: "http://localhost:3000", DEV_AUTH: "1" }, stdout: "ignore", stderr: "pipe", }); for (let attempt = 0; attempt < 40; attempt++) { try { if ((await fetch(`${base}/api/health`)).ok) return; } catch {} await Bun.sleep(50); } throw new Error("API did not start"); }); afterAll(async () => { processHandle?.kill(); await processHandle?.exited; if (folder) await rm(folder, { recursive: true, force: true }); }); test("sync is idempotent and completes a due session at its target", async () => { const startMs = Date.now() - 10_000; const record: TimeRecord = { id: "session_integration_123", startMs, endMs: null, targetSeconds: 5, tag: "job hunting", note: "", treeType: "", isSuccess: null, source: "timer", rawStart: null, rawEnd: null, deleted: false, }; const request = { operations: [{ id: "op_integration_123", record }] }; const options = { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(request) }; expect((await fetch(`${base}/api/operations`, options)).status).toBe(200); expect((await fetch(`${base}/api/operations`, options)).status).toBe(200); const state = await (await fetch(`${base}/api/state?since=0`)).json() as { records: TimeRecord[]; cursor: number; }; expect(state.records).toHaveLength(1); expect(state.records[0]!.endMs).toBe(startMs + 5000); expect(state.records[0]!.isSuccess).toBe(true); expect(state.records[0]!.tag).toBe("Job Hunting"); expect(state.cursor).toBe(2); });