diff --git a/app/page.jsx b/app/page.jsx
index 5d55f07..125bb98 100644
--- a/app/page.jsx
+++ b/app/page.jsx
@@ -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() {
)}
- {
+ 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
-
+
);
}
diff --git a/docs/current-handoff.md b/docs/current-handoff.md
index 792c9da..e516463 100644
--- a/docs/current-handoff.md
+++ b/docs/current-handoff.md
@@ -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 ` ` to a `` 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 ``, not a ` `: **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.
diff --git a/tests/ui/investigation-overview-ui.test.jsx b/tests/ui/investigation-overview-ui.test.jsx
index e6cfd2d..5352628 100644
--- a/tests/ui/investigation-overview-ui.test.jsx
+++ b/tests/ui/investigation-overview-ui.test.jsx
@@ -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();
+ });
});
// ---------------------------------------------------------------------------