Skip to content

Commit cb415eb

Browse files
authored
Merge pull request #167 from utof/fast-hashing/v0.6.x
feat(hash): three-tier identity + Tier-0 cache + on-demand canonical hash (closes #151 #155, advances #157)
2 parents 9cc0271 + 5a00a38 commit cb415eb

126 files changed

Lines changed: 10513 additions & 523 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/mutants.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88
# merge during slice 1 (continue-on-error swallows non-zero exit).
99

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

1919
permissions:

Cargo.lock

Lines changed: 39 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
6464
tokio = { version = "1", features = ["full"] }
6565
tokio-util = { version = "0.7", features = ["rt"] }
6666
uuid = { version = "1", features = ["v4", "v7", "serde"] }
67-
blake3 = "1"
67+
blake3 = { version = "1", features = ["mmap", "rayon"] }
6868
rusqlite = { version = "0.39", features = ["bundled"] }
6969
refinery = { version = "0.9", features = ["rusqlite"] }
7070
flume = "0.12"

apps/desktop/bun.lock

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/desktop/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"dependencies": {
1919
"@tanstack/react-query": "^5",
2020
"@tanstack/react-router": "^1",
21+
"@tanstack/react-virtual": "^3.13.24",
2122
"@tauri-apps/api": "^2",
2223
"@tauri-apps/plugin-dialog": "^2",
2324
"neverthrow": "^8",
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/**
2+
* CollisionPill — color-state pill per spec §4.6.1.
3+
*
4+
* Branches under test:
5+
* - 0 groups: gray, "no candidate duplicates"
6+
* - N groups, 0 verified: blue, "N candidate group(s)"
7+
* - 1 group, 0 verified: blue, "1 candidate group" (singular)
8+
* - N groups, 0 less than M less than N verified: blue, "N candidate (M ✓)"
9+
* - all verified: green, "all verified ✓"
10+
*
11+
* WHY mock `@tanstack/react-router`: CollisionPill renders a Link to="/dedup"
12+
* which requires a router context. Unit tests for a leaf pill component do not
13+
* need full router routing; mocking Link as a plain anchor keeps the test
14+
* focused on color-state rendering without router scaffolding.
15+
*/
16+
import { describe, it, expect, vi } from "vitest";
17+
import { screen } from "@testing-library/react";
18+
import CollisionPill from "../components/CollisionPill";
19+
import type { CollisionGroup } from "../bindings";
20+
import { renderWithProviders } from "./test-utils";
21+
22+
// WHY mock Link: avoids RouterProvider scaffolding for a pure leaf component.
23+
// The href is set to `to` so tests can assert navigation target if needed.
24+
vi.mock("@tanstack/react-router", () => ({
25+
Link: ({
26+
to,
27+
className,
28+
children,
29+
title,
30+
}: {
31+
to: string;
32+
className?: string;
33+
children: React.ReactNode;
34+
title?: string;
35+
}) => (
36+
<a href={to} className={className} title={title}>
37+
{children}
38+
</a>
39+
),
40+
}));
41+
42+
// ── Helpers ──────────────────────────────────────────────────────────────────
43+
44+
function makeGroup(
45+
quickHash: string,
46+
verifiedState: CollisionGroup["verified_state"],
47+
): CollisionGroup {
48+
return {
49+
quick_hash: quickHash,
50+
files: [],
51+
verified_state: verifiedState,
52+
};
53+
}
54+
55+
// ── Tests ─────────────────────────────────────────────────────────────────────
56+
57+
describe("CollisionPill", () => {
58+
it("renders gray 'no candidate duplicates' when groups is empty", () => {
59+
renderWithProviders(<CollisionPill groups={[]} />);
60+
const el = screen.getByText(/no candidate duplicates/i);
61+
expect(el).toBeInTheDocument();
62+
// WHY check class presence: gray state = text-gray-500, not a link.
63+
expect(el.tagName).toBe("SPAN");
64+
expect(el.className).toContain("text-gray-500");
65+
});
66+
67+
it("renders blue pill for N unverified groups (plural)", () => {
68+
const groups = [
69+
makeGroup("aaa", "Unverified"),
70+
makeGroup("bbb", "Unverified"),
71+
makeGroup("ccc", "Unverified"),
72+
];
73+
renderWithProviders(<CollisionPill groups={groups} />);
74+
const link = screen.getByRole("link");
75+
expect(link).toBeInTheDocument();
76+
expect(link.textContent).toMatch(/3 candidate groups/i);
77+
expect(link.className).toContain("text-blue-400");
78+
});
79+
80+
it("renders blue pill with singular 'group' label for 1 unverified group", () => {
81+
renderWithProviders(<CollisionPill groups={[makeGroup("aaa", "Unverified")]} />);
82+
const link = screen.getByRole("link");
83+
expect(link.textContent).toMatch(/1 candidate group$/i);
84+
// Must NOT be plural.
85+
expect(link.textContent).not.toMatch(/1 candidate groups/i);
86+
expect(link.className).toContain("text-blue-400");
87+
});
88+
89+
it("renders blue pill with verified count when 0 < M < N verified", () => {
90+
const groups = [
91+
makeGroup("aaa", "VerifiedDuplicate"),
92+
makeGroup("bbb", "Unverified"),
93+
makeGroup("ccc", "VerifiedDistinct"),
94+
];
95+
renderWithProviders(<CollisionPill groups={groups} />);
96+
const link = screen.getByRole("link");
97+
// 3 total, 2 verified (VerifiedDuplicate + VerifiedDistinct both count).
98+
expect(link.textContent).toMatch(/3 candidates \(2 \)/i);
99+
expect(link.className).toContain("text-blue-400");
100+
});
101+
102+
it("renders green pill when all groups are verified", () => {
103+
const groups = [
104+
makeGroup("aaa", "VerifiedDuplicate"),
105+
makeGroup("bbb", "VerifiedDistinct"),
106+
];
107+
renderWithProviders(<CollisionPill groups={groups} />);
108+
const link = screen.getByRole("link");
109+
expect(link.textContent).toMatch(/all verified /i);
110+
expect(link.className).toContain("text-green-400");
111+
});
112+
113+
it("link points to /dedup", () => {
114+
renderWithProviders(<CollisionPill groups={[makeGroup("aaa", "Unverified")]} />);
115+
const link = screen.getByRole("link");
116+
expect(link).toHaveAttribute("href", "/dedup");
117+
});
118+
119+
it("Mixed state counts as unverified (not fully verified)", () => {
120+
const groups = [makeGroup("aaa", "Mixed")];
121+
renderWithProviders(<CollisionPill groups={groups} />);
122+
const link = screen.getByRole("link");
123+
// Mixed is not VerifiedDuplicate or VerifiedDistinct → not in verified count.
124+
// With 0 verified, label = "1 candidate group".
125+
expect(link.textContent).toMatch(/1 candidate group$/i);
126+
expect(link.className).toContain("text-blue-400");
127+
});
128+
});

apps/desktop/src/__tests__/FileGrid.test.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ import type { FileWithTagsPayload } from "../bindings";
1010
*/
1111
function makeFile(overrides: Partial<FileWithTagsPayload>): FileWithTagsPayload {
1212
return {
13+
// WHY (Task 11): `file_uuid` is the React key. Default to a placeholder
14+
// and let `overrides` set it per-test; the FileGrid test that supplies
15+
// multiple files must override or the keys collide.
16+
file_uuid: "00000000-0000-0000-0000-000000000000",
1317
hash: "0".repeat(64),
18+
quick_hash: null,
1419
size: 1024,
1520
volume_id: "00000000-0000-0000-0000-000000000000",
1621
relative_path: "photos/example.jpg",
@@ -36,18 +41,21 @@ describe("FileGrid", () => {
3641
it("renders an <img> for ready tiles and placeholders for others", () => {
3742
const files: FileWithTagsPayload[] = [
3843
makeFile({
44+
file_uuid: "00000000-0000-0000-0000-00000000000a",
3945
hash: "a".repeat(64),
4046
relative_path: "photos/ready.jpg",
4147
thumbnail_path: "/var/data/perima/thumbnails/aa/ready.webp",
4248
thumbnail_status: "ready",
4349
}),
4450
makeFile({
51+
file_uuid: "00000000-0000-0000-0000-00000000000b",
4552
hash: "b".repeat(64),
4653
relative_path: "photos/pending.jpg",
4754
thumbnail_path: null,
4855
thumbnail_status: "pending",
4956
}),
5057
makeFile({
58+
file_uuid: "00000000-0000-0000-0000-00000000000c",
5159
hash: "c".repeat(64),
5260
relative_path: "photos/failed.jpg",
5361
thumbnail_path: null,

0 commit comments

Comments
 (0)