Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
8367e92
fix(desktop): scaffold tauri build (main.rs + [[bin]] + dialog plugin…
utof Apr 25, 2026
5fa7726
feat(desktop): tags UI MVP + auto-prefix search + faster debounce + r…
utof Apr 25, 2026
8c7a082
fix(app): unify CLI + desktop data-dir resolution (closes #154)
utof Apr 25, 2026
4e3058e
feat(db): V011 — file_uuid + quick_hash columns + file_identity_cache…
utof Apr 25, 2026
50c1a85
fix(db): V011 test uses open_with_flags(READ_ONLY) per disallowed-met…
utof Apr 25, 2026
e59e4d3
feat(db): FTS5 trigger pivot blake3_hash → file_uuid join key (refs #…
utof Apr 25, 2026
ddb8a8a
docs(db): fix stale '16 trigger entries' doc comment after Task 3 piv…
utof Apr 25, 2026
90981f4
fix(db): V011 backfill emits hyphenated UUIDv7 + clarify file_uuid su…
utof Apr 25, 2026
69cee58
feat(core): FileUuid + dedup types + FullHashUnavailable error + Veri…
utof Apr 25, 2026
e0683fe
test(core): wire-shape pins for Task 4 types + coreErrorMessage handl…
utof Apr 25, 2026
4003f8c
fix(core): replace .unwrap() with .expect() in Task 4 wire-shape test…
utof Apr 25, 2026
fd2f873
feat(hash): quick_hash_prefix_suffix + full_hash_dispatched override …
utof Apr 25, 2026
d4cabed
feat(db): IdentityCacheRepository port + SQLite adapter + writer cmds
utof Apr 25, 2026
1095517
fix(core): CacheKey.fs_file_id: u64 → i64 to match spec/plan (refs Ta…
utof Apr 25, 2026
fd903c6
feat(app): ScanUseCase consults Tier-0 cache + computes quick_hash on…
utof Apr 25, 2026
d7161f0
fix(db): populate files.quick_hash eagerly at scan time per spec §4.1…
utof Apr 26, 2026
2aef990
fix(core,app): clippy doc_markdown + cache-hit files.quick_hash asser…
utof Apr 26, 2026
1fa2c7e
feat(app): online quick_hash backfill worker for pre-V011 rows (refs …
utof Apr 26, 2026
715649f
feat(app): ComputeFullHashUseCase + DedupUseCase + 5 IPC commands (re…
utof Apr 26, 2026
a588e11
feat(app,desktop): VerifyProgress per-file dispatch end-to-end (refs …
utof Apr 26, 2026
84d38af
feat(core,desktop): IPC payloads gain file_uuid; hash fields nullable…
utof Apr 26, 2026
08b0672
fix(desktop): docstring on attach_tag_by_uuid matches code (refs #161)
utof Apr 26, 2026
26ce43c
feat(desktop): StatusBar collision pill driven by useCollisions (refs…
utof Apr 26, 2026
fbab18d
feat(desktop): /dedup route with per-group + Verify all + progress + …
utof Apr 26, 2026
351f498
feat(desktop): file-detail sidebar with Compute canonical hash (refs …
utof Apr 26, 2026
efc04de
fix(desktop): payload exposes quick_hash so Compute button placeholde…
utof Apr 26, 2026
628f747
feat(cli): perima hash + perima dedup subcommands (refs #157)
utof Apr 26, 2026
78e4bf2
feat(db): criterion bench — cache-hit re-scan throughput (closes #151…
utof Apr 26, 2026
6f06921
fix(desktop): allow clippy::print_stderr at terminal error path in main
utof Apr 30, 2026
b5a4bb2
ci(mutants): drop pull_request trigger, manual-dispatch only
utof Apr 30, 2026
5a00a38
ci(desktop): gate bindings_compile test off Windows (closes-PR-block …
utof Apr 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/mutants.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
# merge during slice 1 (continue-on-error swallows non-zero exit).

name: mutants
# WHY workflow_dispatch added (2026-04-25): allows on-demand "real deal"
# workspace-wide mutation runs (no --in-diff filter) — much longer than
# per-PR mode (HOURS, not 1-2 minutes). The per-PR `pull_request:` trigger
# stays as-is; the run-step branches on github.event_name to choose mode.
# WHY workflow_dispatch only (2026-04-30): the `pull_request:` trigger was
# removed at user request. Mutation testing is observability-only and was
# adding noise to per-PR check rollups without blocking merges (already
# `continue-on-error`). Manual dispatch retains the workspace-wide capability
# for periodic baseline-collection runs.
on:
pull_request:
workflow_dispatch:

permissions:
Expand Down
39 changes: 39 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
tokio = { version = "1", features = ["full"] }
tokio-util = { version = "0.7", features = ["rt"] }
uuid = { version = "1", features = ["v4", "v7", "serde"] }
blake3 = "1"
blake3 = { version = "1", features = ["mmap", "rayon"] }
rusqlite = { version = "0.39", features = ["bundled"] }
refinery = { version = "0.9", features = ["rusqlite"] }
flume = "0.12"
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"dependencies": {
"@tanstack/react-query": "^5",
"@tanstack/react-router": "^1",
"@tanstack/react-virtual": "^3.13.24",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "^2",
"neverthrow": "^8",
Expand Down
128 changes: 128 additions & 0 deletions apps/desktop/src/__tests__/CollisionPill.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/**
* CollisionPill — color-state pill per spec §4.6.1.
*
* Branches under test:
* - 0 groups: gray, "no candidate duplicates"
* - N groups, 0 verified: blue, "N candidate group(s)"
* - 1 group, 0 verified: blue, "1 candidate group" (singular)
* - N groups, 0 less than M less than N verified: blue, "N candidate (M ✓)"
* - all verified: green, "all verified ✓"
*
* WHY mock `@tanstack/react-router`: CollisionPill renders a Link to="/dedup"
* which requires a router context. Unit tests for a leaf pill component do not
* need full router routing; mocking Link as a plain anchor keeps the test
* focused on color-state rendering without router scaffolding.
*/
import { describe, it, expect, vi } from "vitest";
import { screen } from "@testing-library/react";
import CollisionPill from "../components/CollisionPill";
import type { CollisionGroup } from "../bindings";
import { renderWithProviders } from "./test-utils";

// WHY mock Link: avoids RouterProvider scaffolding for a pure leaf component.
// The href is set to `to` so tests can assert navigation target if needed.
vi.mock("@tanstack/react-router", () => ({
Link: ({
to,
className,
children,
title,
}: {
to: string;
className?: string;
children: React.ReactNode;
title?: string;
}) => (
<a href={to} className={className} title={title}>
{children}
</a>
),
}));

// ── Helpers ──────────────────────────────────────────────────────────────────

function makeGroup(
quickHash: string,
verifiedState: CollisionGroup["verified_state"],
): CollisionGroup {
return {
quick_hash: quickHash,
files: [],
verified_state: verifiedState,
};
}

// ── Tests ─────────────────────────────────────────────────────────────────────

describe("CollisionPill", () => {
it("renders gray 'no candidate duplicates' when groups is empty", () => {
renderWithProviders(<CollisionPill groups={[]} />);
const el = screen.getByText(/no candidate duplicates/i);
expect(el).toBeInTheDocument();
// WHY check class presence: gray state = text-gray-500, not a link.
expect(el.tagName).toBe("SPAN");
expect(el.className).toContain("text-gray-500");
});

it("renders blue pill for N unverified groups (plural)", () => {
const groups = [
makeGroup("aaa", "Unverified"),
makeGroup("bbb", "Unverified"),
makeGroup("ccc", "Unverified"),
];
renderWithProviders(<CollisionPill groups={groups} />);
const link = screen.getByRole("link");
expect(link).toBeInTheDocument();
expect(link.textContent).toMatch(/3 candidate groups/i);
expect(link.className).toContain("text-blue-400");
});

it("renders blue pill with singular 'group' label for 1 unverified group", () => {
renderWithProviders(<CollisionPill groups={[makeGroup("aaa", "Unverified")]} />);
const link = screen.getByRole("link");
expect(link.textContent).toMatch(/1 candidate group$/i);
// Must NOT be plural.
expect(link.textContent).not.toMatch(/1 candidate groups/i);
expect(link.className).toContain("text-blue-400");
});

it("renders blue pill with verified count when 0 < M < N verified", () => {
const groups = [
makeGroup("aaa", "VerifiedDuplicate"),
makeGroup("bbb", "Unverified"),
makeGroup("ccc", "VerifiedDistinct"),
];
renderWithProviders(<CollisionPill groups={groups} />);
const link = screen.getByRole("link");
// 3 total, 2 verified (VerifiedDuplicate + VerifiedDistinct both count).
expect(link.textContent).toMatch(/3 candidates \(2 ✓\)/i);
expect(link.className).toContain("text-blue-400");
});

it("renders green pill when all groups are verified", () => {
const groups = [
makeGroup("aaa", "VerifiedDuplicate"),
makeGroup("bbb", "VerifiedDistinct"),
];
renderWithProviders(<CollisionPill groups={groups} />);
const link = screen.getByRole("link");
expect(link.textContent).toMatch(/all verified ✓/i);
expect(link.className).toContain("text-green-400");
});

it("link points to /dedup", () => {
renderWithProviders(<CollisionPill groups={[makeGroup("aaa", "Unverified")]} />);
const link = screen.getByRole("link");
expect(link).toHaveAttribute("href", "/dedup");
});

it("Mixed state counts as unverified (not fully verified)", () => {
const groups = [makeGroup("aaa", "Mixed")];
renderWithProviders(<CollisionPill groups={groups} />);
const link = screen.getByRole("link");
// Mixed is not VerifiedDuplicate or VerifiedDistinct → not in verified count.
// With 0 verified, label = "1 candidate group".
expect(link.textContent).toMatch(/1 candidate group$/i);
expect(link.className).toContain("text-blue-400");
});
});
8 changes: 8 additions & 0 deletions apps/desktop/src/__tests__/FileGrid.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import type { FileWithTagsPayload } from "../bindings";
*/
function makeFile(overrides: Partial<FileWithTagsPayload>): FileWithTagsPayload {
return {
// WHY (Task 11): `file_uuid` is the React key. Default to a placeholder
// and let `overrides` set it per-test; the FileGrid test that supplies
// multiple files must override or the keys collide.
file_uuid: "00000000-0000-0000-0000-000000000000",
hash: "0".repeat(64),
quick_hash: null,
size: 1024,
volume_id: "00000000-0000-0000-0000-000000000000",
relative_path: "photos/example.jpg",
Expand All @@ -36,18 +41,21 @@ describe("FileGrid", () => {
it("renders an <img> for ready tiles and placeholders for others", () => {
const files: FileWithTagsPayload[] = [
makeFile({
file_uuid: "00000000-0000-0000-0000-00000000000a",
hash: "a".repeat(64),
relative_path: "photos/ready.jpg",
thumbnail_path: "/var/data/perima/thumbnails/aa/ready.webp",
thumbnail_status: "ready",
}),
makeFile({
file_uuid: "00000000-0000-0000-0000-00000000000b",
hash: "b".repeat(64),
relative_path: "photos/pending.jpg",
thumbnail_path: null,
thumbnail_status: "pending",
}),
makeFile({
file_uuid: "00000000-0000-0000-0000-00000000000c",
hash: "c".repeat(64),
relative_path: "photos/failed.jpg",
thumbnail_path: null,
Expand Down
Loading
Loading