- Log swallowed errors in file watcher and window operations (lib.rs, watcher.rs) - Propagate recovery errors from bridge::recover_pending to SyncResult.errors so the frontend can display them instead of silently dropping failures - Fix useTauriEvent/useTauriEvents race condition where cleanup fires before async listen() resolves, leaking the listener (cancelled flag pattern) - Guard computeStaleness against invalid date strings (NaN -> 'normal' instead of incorrectly returning 'urgent') - Strengthen isMcError type guard to check field types, not just presence - Log warning when data directory resolution falls back to '.' (state.rs, bridge.rs) - Add test for computeStaleness with invalid date inputs
330 lines
8.1 KiB
TypeScript
330 lines
8.1 KiB
TypeScript
/**
|
|
* Tests for useActions hook.
|
|
*
|
|
* TDD: These tests define the expected behavior before implementation.
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import { renderHook, act } from "@testing-library/react";
|
|
import { useActions } from "@/hooks/useActions";
|
|
|
|
// Mock Tauri shell plugin
|
|
const mockOpen = vi.fn();
|
|
vi.mock("@tauri-apps/plugin-shell", () => ({
|
|
open: (...args: unknown[]) => mockOpen(...args),
|
|
}));
|
|
|
|
// Mock Tauri invoke
|
|
const mockInvoke = vi.fn();
|
|
vi.mock("@tauri-apps/api/core", () => ({
|
|
invoke: (...args: unknown[]) => mockInvoke(...args),
|
|
}));
|
|
|
|
// Mock the focus store
|
|
const mockLogDecision = vi.fn();
|
|
const mockUpdateItem = vi.fn();
|
|
const mockAct = vi.fn();
|
|
vi.mock("@/stores/focus-store", () => ({
|
|
useFocusStore: () => ({
|
|
logDecision: mockLogDecision,
|
|
updateItem: mockUpdateItem,
|
|
act: mockAct,
|
|
}),
|
|
}));
|
|
|
|
describe("useActions", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mockInvoke.mockResolvedValue(undefined);
|
|
mockOpen.mockResolvedValue(undefined);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
describe("start", () => {
|
|
it("opens URL in browser", async () => {
|
|
const { result } = renderHook(() => useActions());
|
|
|
|
await act(async () => {
|
|
await result.current.start({
|
|
id: "br-x7f",
|
|
url: "https://gitlab.com/platform/core/-/merge_requests/847",
|
|
title: "Test MR",
|
|
});
|
|
});
|
|
|
|
expect(mockOpen).toHaveBeenCalledWith(
|
|
"https://gitlab.com/platform/core/-/merge_requests/847"
|
|
);
|
|
});
|
|
|
|
it("does not open URL if none provided", async () => {
|
|
const { result } = renderHook(() => useActions());
|
|
|
|
await act(async () => {
|
|
await result.current.start({
|
|
id: "br-x7f",
|
|
title: "Manual task",
|
|
});
|
|
});
|
|
|
|
expect(mockOpen).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("logs decision via Tauri", async () => {
|
|
const { result } = renderHook(() => useActions());
|
|
|
|
await act(async () => {
|
|
await result.current.start({
|
|
id: "br-x7f",
|
|
title: "Test",
|
|
});
|
|
});
|
|
|
|
expect(mockInvoke).toHaveBeenCalledWith(
|
|
"log_decision",
|
|
expect.objectContaining({
|
|
entry: expect.objectContaining({
|
|
action: "start",
|
|
bead_id: "br-x7f",
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("defer", () => {
|
|
it("calculates correct snooze time for 1h", async () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date("2026-02-25T10:00:00Z"));
|
|
|
|
const { result } = renderHook(() => useActions());
|
|
|
|
await act(async () => {
|
|
await result.current.defer(
|
|
{ id: "br-x7f", title: "Test" },
|
|
"1h",
|
|
"Need more time"
|
|
);
|
|
});
|
|
|
|
expect(mockInvoke).toHaveBeenCalledWith(
|
|
"update_item",
|
|
expect.objectContaining({
|
|
id: "br-x7f",
|
|
updates: expect.objectContaining({
|
|
snoozed_until: "2026-02-25T11:00:00.000Z",
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
|
|
it("calculates correct snooze time for 3h", async () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date("2026-02-25T10:00:00Z"));
|
|
|
|
const { result } = renderHook(() => useActions());
|
|
|
|
await act(async () => {
|
|
await result.current.defer(
|
|
{ id: "br-x7f", title: "Test" },
|
|
"3h",
|
|
null
|
|
);
|
|
});
|
|
|
|
expect(mockInvoke).toHaveBeenCalledWith(
|
|
"update_item",
|
|
expect.objectContaining({
|
|
id: "br-x7f",
|
|
updates: expect.objectContaining({
|
|
snoozed_until: "2026-02-25T13:00:00.000Z",
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
|
|
it("defer tomorrow uses 9am next day", async () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date("2026-02-25T22:00:00Z")); // 10pm
|
|
|
|
const { result } = renderHook(() => useActions());
|
|
|
|
await act(async () => {
|
|
await result.current.defer(
|
|
{ id: "br-x7f", title: "Test" },
|
|
"tomorrow",
|
|
null
|
|
);
|
|
});
|
|
|
|
// Should be 9am on Feb 26
|
|
expect(mockInvoke).toHaveBeenCalledWith(
|
|
"update_item",
|
|
expect.objectContaining({
|
|
id: "br-x7f",
|
|
updates: expect.objectContaining({
|
|
snoozed_until: expect.stringContaining("2026-02-26T09:00:00"),
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
|
|
it("logs decision with reason", async () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date("2026-02-25T10:00:00Z"));
|
|
|
|
const { result } = renderHook(() => useActions());
|
|
|
|
await act(async () => {
|
|
await result.current.defer(
|
|
{ id: "br-x7f", title: "Test" },
|
|
"1h",
|
|
"In a meeting"
|
|
);
|
|
});
|
|
|
|
expect(mockInvoke).toHaveBeenCalledWith(
|
|
"log_decision",
|
|
expect.objectContaining({
|
|
entry: expect.objectContaining({
|
|
action: "defer",
|
|
bead_id: "br-x7f",
|
|
reason: "In a meeting",
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
|
|
it("advances to next item in queue", async () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date("2026-02-25T10:00:00Z"));
|
|
|
|
const { result } = renderHook(() => useActions());
|
|
|
|
await act(async () => {
|
|
await result.current.defer(
|
|
{ id: "br-x7f", title: "Test" },
|
|
"1h",
|
|
null
|
|
);
|
|
});
|
|
|
|
expect(mockAct).toHaveBeenCalledWith("defer_1h", undefined);
|
|
});
|
|
});
|
|
|
|
describe("skip", () => {
|
|
it("marks item as skipped for today", async () => {
|
|
const { result } = renderHook(() => useActions());
|
|
|
|
await act(async () => {
|
|
await result.current.skip(
|
|
{ id: "br-x7f", title: "Test" },
|
|
"Not urgent"
|
|
);
|
|
});
|
|
|
|
expect(mockInvoke).toHaveBeenCalledWith(
|
|
"update_item",
|
|
expect.objectContaining({
|
|
id: "br-x7f",
|
|
updates: expect.objectContaining({
|
|
skipped_today: true,
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
|
|
it("logs decision with reason", async () => {
|
|
const { result } = renderHook(() => useActions());
|
|
|
|
await act(async () => {
|
|
await result.current.skip(
|
|
{ id: "br-x7f", title: "Test" },
|
|
"Low priority"
|
|
);
|
|
});
|
|
|
|
expect(mockInvoke).toHaveBeenCalledWith(
|
|
"log_decision",
|
|
expect.objectContaining({
|
|
entry: expect.objectContaining({
|
|
action: "skip",
|
|
bead_id: "br-x7f",
|
|
reason: "Low priority",
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
|
|
it("advances to next item in queue", async () => {
|
|
const { result } = renderHook(() => useActions());
|
|
|
|
await act(async () => {
|
|
await result.current.skip({ id: "br-x7f", title: "Test" }, null);
|
|
});
|
|
|
|
expect(mockAct).toHaveBeenCalledWith("skip", undefined);
|
|
});
|
|
});
|
|
|
|
describe("complete", () => {
|
|
it("closes bead via Tauri", async () => {
|
|
const { result } = renderHook(() => useActions());
|
|
|
|
await act(async () => {
|
|
await result.current.complete(
|
|
{ id: "br-x7f", title: "Test" },
|
|
"Fixed the bug"
|
|
);
|
|
});
|
|
|
|
expect(mockInvoke).toHaveBeenCalledWith(
|
|
"close_bead",
|
|
expect.objectContaining({
|
|
bead_id: "br-x7f",
|
|
reason: "Fixed the bug",
|
|
})
|
|
);
|
|
});
|
|
|
|
it("logs decision", async () => {
|
|
const { result } = renderHook(() => useActions());
|
|
|
|
await act(async () => {
|
|
await result.current.complete(
|
|
{ id: "br-x7f", title: "Test" },
|
|
"Done"
|
|
);
|
|
});
|
|
|
|
expect(mockInvoke).toHaveBeenCalledWith(
|
|
"log_decision",
|
|
expect.objectContaining({
|
|
entry: expect.objectContaining({
|
|
action: "complete",
|
|
bead_id: "br-x7f",
|
|
reason: "Done",
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
|
|
it("advances to next item in queue", async () => {
|
|
const { result } = renderHook(() => useActions());
|
|
|
|
await act(async () => {
|
|
await result.current.complete(
|
|
{ id: "br-x7f", title: "Test" },
|
|
null
|
|
);
|
|
});
|
|
|
|
expect(mockAct).toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|