feat: complete advanced Sudoku workbench

This commit is contained in:
2026-08-31 08:20:30 +02:00
parent 8ca9300ab3
commit 0a1bdc1a8c
99 changed files with 20793 additions and 923 deletions
+279
View File
@@ -0,0 +1,279 @@
import { fireEvent, render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import {
SetterQualityLab,
type SetterQualityLabProps,
} from "../../src/components/SetterQualityLab";
import type {
PuzzleQualityAnalysis,
QualityItemReference,
} from "../../src/solver/quality";
const given: QualityItemReference = { kind: "given", cell: 0, value: 1 };
const constraint: QualityItemReference = {
kind: "constraint",
index: 0,
constraintType: "diagonal",
};
const bounds = {
perCheck: { maxNodes: 2_000_000, timeoutMs: 10_000 },
aggregate: { maxChecks: 1_000, maxNodes: 20_000_000, timeoutMs: 30_000 },
} as const;
const multiple: PuzzleQualityAnalysis = {
analysisDepth: "full",
solutionStatus: "multiple",
ambiguityWitness: {
firstSolution: [1, 2, 3, 4],
secondSolution: [2, 1, 3, 4],
differences: [
{ cell: 0, first: 1, second: 2 },
{ cell: 1, first: 2, second: 1 },
],
},
contradiction: {
status: "not-applicable",
core: [],
necessary: [],
removable: [],
unknown: [],
},
redundancy: {
givens: [
{
item: given,
classification: "critical",
checkIndex: 1,
solutionStatus: "multiple",
},
],
constraints: [
{
item: constraint,
classification: "redundant",
checkIndex: 2,
solutionStatus: "unique",
},
],
},
criticalityHeatmap: [
{
cell: 0,
score: 1,
criticalWeight: 1,
redundantWeight: 0,
unknownWeight: 0,
},
{
cell: 1,
score: null,
criticalWeight: 0,
redundantWeight: 0,
unknownWeight: 1,
},
],
minimality: {
status: "not-minimal",
redundant: [constraint],
unknown: [],
},
checks: [
{
index: 0,
purpose: "baseline",
solutionStatus: "multiple",
solutionsFound: 2,
conclusive: true,
truncated: true,
limitReason: "solution-cap",
nodes: 10,
elapsedMs: 2,
},
],
bounds,
budget: {
checksPlanned: 3,
checksPerformed: 3,
nodes: 42,
elapsedMs: 12,
truncated: false,
unknownReasons: [],
},
};
function props(
overrides: Partial<SetterQualityLabProps> = {},
): SetterQualityLabProps {
return {
size: 4,
onRunQuick: vi.fn(),
onRunMinimality: vi.fn(),
onCancel: vi.fn(),
onFocusCells: vi.fn(),
onFocusItem: vi.fn(),
...overrides,
};
}
describe("setter quality lab", () => {
it("runs distinct quick and bounded-minimality profiles and supports cancel", async () => {
const user = userEvent.setup();
const onRunQuick = vi.fn();
const onRunMinimality = vi.fn();
const onCancel = vi.fn();
const base = props({ onRunQuick, onRunMinimality, onCancel });
const { rerender } = render(<SetterQualityLab {...base} />);
fireEvent.change(screen.getByLabelText("Nodes per check"), {
target: { value: "123456" },
});
await user.click(
screen.getByRole("button", { name: "Run quick quality check" }),
);
expect(onRunQuick).toHaveBeenCalledWith({
perCheckMaxNodes: 123_456,
perCheckTimeoutMs: 10_000,
aggregateMaxChecks: 1_000,
aggregateMaxNodes: 20_000_000,
aggregateTimeoutMs: 30_000,
analysisDepth: "baseline",
proveMinimality: false,
});
await user.click(
screen.getByRole("button", { name: "Run full bounded minimality" }),
);
expect(onRunMinimality).toHaveBeenCalledWith(
expect.objectContaining({
analysisDepth: "full",
proveMinimality: true,
}),
);
rerender(<SetterQualityLab {...base} running="minimality" />);
expect(
screen.getByText("Full bounded minimality analysis running locally…"),
).toHaveAttribute("role", "status");
expect(
screen.getByRole("button", { name: "Run quick quality check" }),
).toBeDisabled();
await user.click(screen.getByRole("button", { name: "Cancel analysis" }));
expect(onCancel).toHaveBeenCalledOnce();
});
it("renders an ambiguity witness, findings, heatmap and bounded metrics", async () => {
const user = userEvent.setup();
const onFocusCells = vi.fn();
const onFocusItem = vi.fn();
render(
<SetterQualityLab
{...props({ result: multiple, onFocusCells, onFocusItem })}
/>,
);
expect(
screen.getByRole("heading", { name: "Multiple completions found" }),
).toBeInTheDocument();
expect(screen.getByText("2 differing cells")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Focus differences" }));
expect(onFocusCells).toHaveBeenCalledWith([0, 1]);
await user.click(
screen.getByRole("button", {
name: /r1c1: critical; score 1\.00/u,
}),
);
expect(onFocusCells).toHaveBeenLastCalledWith([0]);
expect(
screen.getByRole("button", { name: /r1c2: unknown; score unknown/u }),
).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Given 1 at r1c1" }));
expect(onFocusItem).toHaveBeenCalledWith(given);
const metrics = screen
.getByRole("heading", { name: "Checks and limits" })
.closest("section")!;
expect(within(metrics).getByText("3 / 3")).toBeInTheDocument();
expect(
within(metrics).getByText("2,000,000 nodes / 10,000 ms"),
).toBeInTheDocument();
expect(
within(metrics).getByText("1,000 checks / 20,000,000 nodes / 30,000 ms"),
).toBeInTheDocument();
expect(
screen.getByRole("heading", { name: "Not minimal" }),
).toBeInTheDocument();
});
it("labels bounded unknowns and exposes contradiction suspects", async () => {
const user = userEvent.setup();
const onFocusItem = vi.fn();
const unknown: PuzzleQualityAnalysis = {
...multiple,
solutionStatus: "unknown",
ambiguityWitness: undefined,
contradiction: {
status: "incomplete",
core: [given, constraint],
necessary: [given],
removable: [],
unknown: [constraint],
reason: "The aggregate time budget ended localization.",
},
redundancy: {
givens: [
{
item: given,
classification: "unknown",
unknownReason: "aggregate-timeout",
},
],
constraints: [],
},
criticalityHeatmap: [
{
cell: 0,
score: null,
criticalWeight: 0,
redundantWeight: 0,
unknownWeight: 1,
},
],
minimality: {
status: "unknown",
redundant: [],
unknown: [constraint],
reason: "One or more clue checks were bounded.",
},
budget: {
...multiple.budget,
checksPerformed: 2,
truncated: true,
unknownReasons: ["aggregate-timeout"],
},
};
render(<SetterQualityLab {...props({ result: unknown, onFocusItem })} />);
expect(
screen.getByRole("heading", { name: "Solution status unknown" }),
).toBeInTheDocument();
expect(
screen.getAllByText(/incomplete \/ unknown/iu).length,
).toBeGreaterThan(1);
expect(
screen.getByText("The aggregate time budget ended localization."),
).toBeInTheDocument();
expect(screen.getByText("Aggregate timeout")).toBeInTheDocument();
const necessary = screen
.getByRole("heading", { name: "Proven necessary suspects" })
.closest("section")!;
await user.click(
within(necessary).getByRole("button", { name: "Given 1 at r1c1" }),
);
expect(onFocusItem).toHaveBeenCalledWith(given);
});
});