This is the first commit of the project, at the state that runs on timelog.minhtrannhat.com. Changes in this state: - Remove redundant UI text from the sidebar, timer and insights pages. - Show the remaining time and tag in the browser tab title while a session runs. - Put the shared record type and the due and end helpers in src/shared/record.ts. - Remove the startup tag migration and the one-time Forest import script. The live data does not need them. - Remove unused CSS, Playwright ignore rules and duplicate helpers. - Do not track Forest CSV exports, because they contain private data. Co-Authored-By: Claude Opus 5.5 <[email protected]>
53 lines
2.2 KiB
TypeScript
53 lines
2.2 KiB
TypeScript
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<typeof Bun.spawn>;
|
|
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);
|
|
});
|