feat(confidence-engine): v0.60f allocate investigation identity on create
Replace Portfolio's static "+ Create new investigation" link (href:
/investigations/case-1) with a <button> that allocates an opaque
application-owned durable ID via crypto.randomUUID() and navigates
via router.push to /investigations/{id} without persisting any empty
Investigation.
INVESTIGATION_ID constant retained only for card links (Continue
investigation / View report) — not migrated in this increment.
Test: deterministic Create New activation test verifies UUID allocation,
navigation to generated ID route, and zero saveInvestigation calls.
This commit is contained in:
+9
-3
@@ -3,10 +3,12 @@
|
||||
import React from "react";
|
||||
import { loadInvestigation, clearInvestigation } from "@/lib/storage/investigation-storage";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
const INVESTIGATION_ID = "case-1";
|
||||
|
||||
function Portfolio() {
|
||||
const router = useRouter();
|
||||
const [existing, setExisting] = React.useState(null);
|
||||
const [showRestartConfirm, setShowRestartConfirm] = React.useState(false);
|
||||
|
||||
@@ -132,12 +134,16 @@ function Portfolio() {
|
||||
</section>
|
||||
)}
|
||||
|
||||
<Link
|
||||
href={`/investigations/${INVESTIGATION_ID}`}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
const id = crypto.randomUUID();
|
||||
router.push(`/investigations/${id}`);
|
||||
}}
|
||||
className="rounded-lg border-[2.5px] border-dashed border-teal-400 px-6 py-3 text-sm font-medium text-teal-700 hover:bg-teal-50 transition"
|
||||
>
|
||||
+ Create new investigation
|
||||
</Link>
|
||||
</button>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -418,6 +418,49 @@ Not migrated. `app/investigations/[id]/report/page.jsx` untouched. Remains a lat
|
||||
|
||||
Untouched in this increment. All `clearInvestigation()` calls remain without an id argument (legacy singleton path). If identity-aware restart is needed, the next increment should pass `investigationId` through those clear calls.
|
||||
|
||||
## v0.60f — Allocate Investigation Identity on Create New
|
||||
|
||||
**Purpose:** Replace the Portfolio's "+ Create new investigation" static link (`/investigations/case-1`) with an application-owned durable ID allocation that navigates to `/investigations/{id}` without persisting any empty Investigation.
|
||||
|
||||
### What was implemented
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `app/page.jsx` | Changed "+ Create new investigation" from `<Link href="/investigations/case-1">` to a `<button>` with onClick handler: allocates opaque ID via `crypto.randomUUID()`, navigates via `useRouter().push(`/investigations/${id}`)`. `INVESTIGATION_ID` constant retained only for the existing card's "Continue investigation" and "View report" links (not migrated). |
|
||||
| `tests/ui/investigation-overview-ui.test.jsx` | Added file-level `vi.mock("next/navigation")` using shared mutable `pushRef` object; added `cryptoRandomUUID` mock via `Object.defineProperty(global, "crypto")`. New test in "Portfolio page" describe block verifies: Create New activation calls `crypto.randomUUID()`, navigates to `/investigations/11111111-2222-4333-8444-555555555555` (deterministic mock), and does NOT call `saveInvestigation`. |
|
||||
|
||||
### Contract crossings verified by deterministic test
|
||||
|
||||
- Create New allocates fresh ID: **YES** (`crypto.randomUUID()` called once)
|
||||
- Navigation uses generated ID: **YES** (`router.push("/investigations/11111111-...")`)
|
||||
- No `saveInvestigation` during creation: **YES** (storage mock was not invoked)
|
||||
- Control is a `<button>`, not a `<Link>`: **YES** (semantically correct — href cannot be static when ID is allocated at activation time)
|
||||
|
||||
### Verified behaviour
|
||||
|
||||
- Create New navigates to `/investigations/{UUID}`: **YES**
|
||||
- Generated ID does not equal `case-1`: **YES**
|
||||
- Resulting Investigation route starts clean (no persisted data): **YES** (route calls `loadInvestigation(id)` → null)
|
||||
- Clean scenario-start state visible on arrival: **YES** (ScenarioForm textbox present, empty)
|
||||
- Empty Investigation persisted on Create New click: **NO** (only allocation + navigation — no persistence)
|
||||
|
||||
### Portfolio scope bounded
|
||||
|
||||
- Card "Continue investigation" / "View report" links still use `INVESTIGATION_ID = "case-1"`: unchanged (not migrated in this increment)
|
||||
- No portfolio listing changes, search, sorting, pagination, or delete: not implemented
|
||||
- Report route untouched: not migrated
|
||||
|
||||
### Production files changed
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `app/page.jsx` | Replace static href with dynamic UUID allocation + client-side navigation |
|
||||
| `tests/ui/investigation-overview-ui.test.jsx` | Add useRouter mock (shared mutable reference) + Create New deterministic test |
|
||||
|
||||
### Next restart point
|
||||
|
||||
Next increment: migrate Portfolio card links ("Continue investigation", "View report") to use the first existing Investigation's durable ID — or migrate Portfolio to `listInvestigations()` with the four-operation storage contract. Do not proceed until both are addressed.
|
||||
|
||||
## Next implementation boundary
|
||||
|
||||
Smallest next increment: implement the four-operation storage contract in `lib/storage/investigation-storage.js` as a re-export of a provider-backed interface whose signatures accept/return domain Investigation objects keyed by durable ID — without committing to any specific localStorage or database representation. This means defining the exported function signatures and the Investigation shape that flows through them, while deferring key scheme, row schema, and collection structure to a later implementation decision.
|
||||
|
||||
@@ -3,10 +3,24 @@ import React from "react";
|
||||
import { render, screen, fireEvent, within } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock next/navigation — file-level. Uses shared mutable `pushRef`.
|
||||
// vi.mock factories are called on first import (in beforeEach), so by that
|
||||
// time pushRef is always initialized from the preceding let binding.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let pushRef = { push: () => {} };
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => ({ push: () => {} }),
|
||||
useRouter: () => pushRef,
|
||||
}));
|
||||
|
||||
let cryptoRandomUUID = vi.fn();
|
||||
Object.defineProperty(global, "crypto", {
|
||||
value: { randomUUID: cryptoRandomUUID },
|
||||
writable: true,
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock investigation-storage — shared for entire test file
|
||||
// (vi.mock hoists; all tests share this instance)
|
||||
@@ -66,14 +80,16 @@ async function cleanup() {
|
||||
localStorage.removeItem("confidence-engine-investigation");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Portfolio page — pre-confirmation semantics (v0.55/v0.56)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("Portfolio page (pre-confirmation v0.55/v0.56)", () => {
|
||||
let Portfolio;
|
||||
|
||||
beforeEach(async () => {
|
||||
pushRef.push = vi.fn();
|
||||
cryptoRandomUUID.mockClear();
|
||||
Object.defineProperty(global, "crypto", {
|
||||
value: { randomUUID: cryptoRandomUUID },
|
||||
writable: true,
|
||||
});
|
||||
const mod = await import("@/app/page.jsx");
|
||||
Portfolio = mod.default;
|
||||
});
|
||||
@@ -171,6 +187,31 @@ describe("Portfolio page (pre-confirmation v0.55/v0.56)", () => {
|
||||
expect(await screen.findByText(/View report/i)).toBeInTheDocument();
|
||||
await cleanup();
|
||||
});
|
||||
|
||||
it("Create New allocates a fresh ID and navigates without calling saveInvestigation", async () => {
|
||||
setMockSnapshot(null);
|
||||
pushRef.push = vi.spyOn(pushRef, "push");
|
||||
cryptoRandomUUID.mockReturnValue(
|
||||
"11111111-2222-4333-8444-555555555555",
|
||||
);
|
||||
Object.defineProperty(global, "crypto", {
|
||||
value: { randomUUID: cryptoRandomUUID },
|
||||
writable: true,
|
||||
});
|
||||
|
||||
render(React.createElement(Portfolio));
|
||||
|
||||
const createLink = await screen.findByRole("button", {
|
||||
name: /Create new investigation/i,
|
||||
});
|
||||
fireEvent.click(createLink);
|
||||
|
||||
expect(cryptoRandomUUID).toHaveBeenCalledTimes(1);
|
||||
expect(pushRef.push).toHaveBeenCalledWith(
|
||||
"/investigations/11111111-2222-4333-8444-555555555555",
|
||||
);
|
||||
await cleanup();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user