Skip to content

Releases/v0.29#321

Merged
therealbrad merged 6 commits into
mainfrom
releases/v0.29
May 20, 2026
Merged

Releases/v0.29#321
therealbrad merged 6 commits into
mainfrom
releases/v0.29

Conversation

@therealbrad
Copy link
Copy Markdown
Contributor

Description

Release v0.29.0 — parameterized test cases plus everything that shipped in v0.28.0.

Parameterized Test Cases

A single test case can now be driven from a table of input rows. Each row becomes an iteration with its own status; results roll up to one case in the report.

  • Per-case parameters and local datasets — declare named parameters on a case, attach a per-case dataset, paste/import CSV, drag-reorder rows.
  • Project-scoped shared datasets — manage datasets under Project Settings → Test Case Parameters, assign them to cases, pin to a specific version (current / follow-latest / specific). Immutable version snapshots are captured at run-creation time.
  • Parameterized run model — iteration rows fan out from cases × configurations × dataset rows when a run is created. Synchronous below the async cap, queued via the new iteration-generation worker above it, hard-refused at the safety ceiling. Per-iteration result entry, with per-case rollup using worst-of status.
  • Parameter Iteration Matrix report — Report Builder preset showing cases × configurations × dataset rows with status rollup per cell. Available in the shared report viewer.
  • Outbound webhook surface — new opt-in iteration.result.recorded event, plus per-iteration redacted values appended to the existing test_run.completed payload. Sensitive parameter values are redacted server-side for non-privileged viewers.
  • LLM test-case generation — admin-gated Include parameters toggle in the generation wizard. When enabled, the assistant emits a parameter schema alongside the steps.
  • Configurable JUnit iteration-property names — project-level allowlist for the property/attribute/trait names the JUnit import path reads to assign an iteration index per result.

Included from v0.28.0

  • Turkish (tr-TR) and Russian (ru-RU) locale support.
  • Shared BullMQ defaultJobOptions presets across the queue layer.

Related Issue

Ships the v0.29.0 milestone.

Type of Change

  • New feature (non-breaking change which adds functionality)
  • Documentation update

Testing

  • Unit tests pass (pnpm test)
  • pnpm lint — 0 errors
  • pnpm type-check — clean
  • pnpm build — clean
  • E2E suite (E2E_PROD=on pnpm test:e2e) — passing on the merged tree

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

therealbrad and others added 2 commits May 19, 2026 13:47
* test(02-06): RED - failing tests for TipTapEditor parameters wiring

- conditional parameterMention extension mount on non-empty parameters
- conditional InsertParameterToolbarButton render
- readOnly + undefined + empty-array negative cases

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(02-06): GREEN - TipTapEditor mounts parameter extension + toolbar + warning

- Optional parameters: ParameterChipMeta[] prop; conditional on length > 0
- Conditional createParameterMentionExtension(parameters) added to extensions array
- Conditional InsertParameterToolbarButton rendered immediately after the link Popover
- UndeclaredParameterWarning rendered below EditorContent when parameters supplied
- Scoped chip CSS (UI-SPEC F.1 verbatim) injected once when parameters !== undefined
- CommentEditor.tsx UNTOUCHED (PARAM-07 invariant; uses useEditor directly, not TipTapEditor)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(02-06): plumb parameters from case-detail page through Steps editor

- StepsForm: optional parameters + onOpenParametersSheet props; forward through
  StepItem -> TipTapEditorWrapper -> TipTapEditor
- RenderField (AddCase form): forward parameters into Steps branch
- FieldValueRenderer (case-detail surface): forward parameters into Steps branch
- page.tsx: map TestCaseParameter rows -> ParameterChipMeta (defaultValue Json -> string|null);
  pass through both <FieldValueRenderer> usages with onOpenParametersSheet ->
  setIsParamSheetOpen(true)

CommentEditor.tsx unchanged (PARAM-07 invariant — uses useEditor directly).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(02-06): 5 E2E specs for the full Phase 2 parameter flow

- authoring-step-mentions: PARAM-04 — declare 2 params, autocomplete
  on @, chip via toolbar, undeclared warning ribbon
- parameter-rename-flow: PARAM-05 — Sheet inline-edit -> rename dialog
  (or zero-refs silent path) -> renamed row visible
- version-history-parameter-diff: PARAM-06 — adding a parameter creates
  a new RepositoryCaseVersion whose snapshot includes the parameters
  array (UI diff is Phase 6 polish per RESEARCH A1)
- dataset-csv-import-flow: DSET-05 — full 4-step wizard happy path +
  BOM-prefixed CSV auto-mapping (RESEARCH Pitfall 4)
- no-parameter-case-unchanged: PARAM-07 invariant — case with zero
  parameters has Configure button visible but NO toolbar parameter
  button, NO autocomplete on @, NO undeclared warning

All specs use data-testid selectors first, no native dialogs, deterministic
seeded data via api.createProject/createFolder/createTestCase. Manual UAT
runs the full suite at phase close-out.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(parameters): add missing parameters Json? column to RepositoryCaseVersions

testCaseVersionService.snapshotParametersForVersion writes a `parameters`
field on RepositoryCaseVersions, but Plan 02-01 added the helper code
without adding the column to the schema. Tests passed because all
integration tests mocked the Prisma transaction (typed `any`), masking
a runtime PrismaClientValidationError on every parameter mutation that
reached production code.

Adding the column to schema.zmodel + regenerating Prisma artifacts
unblocks PARAM-06 (version bump on parameter changes) and PARAM-05
(atomic rename rewrite, which transactionally bumps the version).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(parameters): place Configure parameters button before Steps section

The button was mounted at the top of the case-detail page above all
template-driven fields, which felt disconnected from the steps it
applies to (parameter substitution is steps-only per PARAM-04). Move
it inside the Steps `<li>` in the template field map so it appears
directly above whichever position the template assigns to the Steps
field. Same treatment for the orphaned-steps fallback (when a case
has steps but the current template doesn't include a Steps field).
When the template has no Steps field AND no orphaned steps, the
button doesn't render at all — there's no use case for `@paramName`
without steps.

Also tightens spacing on the button itself (drops `mb-2` and the
inner-icon `mr-2`; shadcn Button's `gap-2` handles icon-text spacing).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(tiptap): recreate editor when parameters prop changes

useEditor was called without a deps array, so the editor mounted once
and never re-evaluated the conditional spread of
parameterMentionExtension. When parameters arrived from
useFindManyTestCaseParameter after the editor was already constructed,
the extension never attached — typing `@` did nothing, and the toolbar
button inserted plain text instead of a chip.

Pass [parameters] as the useEditor deps so the editor recreates when
the parameter set changes. parameterChipMeta is memoized on the
case-detail page (deps: [caseParameters]) so the reference is stable
and the editor only recreates on actual parameter changes.

The existing TipTapEditor test suite missed this because every test
mounts the editor with parameters statically populated as a prop —
none simulated the async-load case where parameters arrive after the
editor is constructed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(parameters): render parameter chips in view-mode step display

View-mode StepsDisplay routes content through TextFromJson →
TipTapEditorWrapper → TipTapEditor (readOnly). None of those
forwarded the `parameters` prop, so the read-only editor mounted
without parameterMentionExtension. Tiptap's parser silently dropped
unknown parameterMention nodes from saved JSON, leaving step text
that contained chips appearing empty.

Thread `parameters` through:
  FieldValueRenderer → StepsDisplay → TextFromJson →
    TipTapEditorWrapper → TipTapEditor

Now the same chip rendering used in edit mode (parameterMention
schema understood) applies in view mode. The renderHTML in the
extension renders `@${label}` as a span regardless of whether
parameters is empty, so chips remain visible even before the
parameters query resolves.

Also forwards `parameters` to RenderSharedGroupItems so chips
render correctly in shared-step content invoked by the case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(parameters): correct ZenStack query-key prefix for cache invalidation

Every Phase 2 mutation (parameter add/update/delete/rename, dataset
attach/rows/import, paste-csv) called
`queryClient.invalidateQueries({ queryKey: ["TestCaseParameter"] })`
or similar. ZenStack's tanstack-query runtime generates keys as
`["zenstack", model, operation, args, options]`, so the model-name-only
prefix never matched and the React Query cache was never invalidated.
The user had to close + reopen the Sheet to see new parameters.

Fixed across 21 sites in 8 components by prepending "zenstack" to every
queryKey array. Affects:
  - ParameterAddForm
  - ParametersTab (reorder)
  - ParameterRenameDialog (rename + cancel paths)
  - ParameterDeleteDialog
  - ParameterRow (inline-edit toggle)
  - SelectSourceSwitchDialog
  - DatasetRowActions
  - DatasetImportWizard
  - PasteCsvDialog
  - DatasetTab (3 sites: row write, drag-reorder, bulk delete)

Tests pass — none asserted on the old key shape (mocks use stub
invalidateQueries that doesn't check args).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(parameters): UAT-driven UX polish across parameters + dataset surfaces

Bundles the full set of UAT findings from Phase 2 manual testing into one
coherent commit. All changes are scoped to UI/UX — no schema or contract
changes.

Parameter authoring:
- Pencil opens a new ParameterEditDialog covering type/default/required/
  sensitive/SELECT-source/allowed-values (was: name-only inline edit).
  Clicking the parameter name still triggers the rename usage-scan flow.
- Drop the manual `order` numeric input from the form — drag-reorder is
  the only ordering UX. New parameters auto-append at order = count.
- Required + Sensitive checkboxes moved to the right column next to
  Default in the Add form (was: their own row below the grid).
- ParametersTab body padding tightened (p-6 → px-6 py-2).

Dataset grid (Surface D):
- Read-mode cells render as a readonly shadcn <Input> instead of a styled
  span — guarantees pixel-identical box model with edit-mode <Input>;
  no layout shift when entering edit mode.
- Empty cells show a "Click to edit" placeholder so they're discoverable
  (previously empty <span> had zero clickable area; only the masked
  sensitive cells were clickable).
- Per-row label defaults to "Row N" (1-based) when adding a row.
- Min-widths on Label (140px) and parameter columns (180px) — applied
  via style on <th>/<td> since TanStack Table's minSize alone doesn't
  reach the DOM.
- Parameter column headers gain whitespace-nowrap so name + type chip +
  lock icon stay on one line.
- Table cells switched from align-top → align-middle so checkboxes and
  drag handles stay vertically centered when any cell grows.

Tiptap toolbar:
- Insert Parameter button (Braces icon) now opens a shadcn AsyncCombobox
  (search-filterable popover) instead of a modal Dialog — fewer clicks,
  matches the @-mention autocomplete pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(schema): add iteration audit action enum values

- Add ITERATION_VALUES_OVERRIDDEN, ITERATION_BULK_SKIPPED,
  ITERATION_RESULT_RECORDED to AuditAction enum
- Regenerate Prisma client + ZenStack OpenAPI artifacts

* feat(workers): scaffold iteration generation queue and worker

- Add ITERATION_GENERATION_QUEUE_NAME constant
- Add lazy getIterationGenerationQueue() factory mirroring duplicate-scan
  shape with attempts: 1 (no retry — partial retry creates duplicate
  iterations) and include it in getAllQueues()
- Register iterationGenerationWorker in build-workers entry points,
  ecosystem.config.js (PM2), and package.json (dev + prod scripts)
- Add stub worker that throws "not yet implemented" so accidentally
  enqueued jobs fail loudly until the fan-out logic lands in a
  follow-up wave

* feat(iterations): add cardinality preflight types, helpers, and API endpoint

- New PreflightResult / CardinalityBand / CardinalityThresholds types in
  lib/types/iterationCardinality.ts shared between the modal, the preflight
  API endpoint, and the upcoming generate-iterations endpoint.
- Pure-function helpers in lib/services/iterationCardinality.ts:
  classifyBand (sync/async/softConfirm/hardRefuse boundaries), computePreflight
  (rowCount × max(1, configCount), parameterized cases only, sorted desc),
  readCardinalityThresholds (env-var reader with safe numeric fallback).
- 24 co-located unit tests cover all four band boundaries, env-var parsing
  edge cases, perCase exclusion of non-parameterized cases, and the
  configCount=0 collapse-to-1 invariant.
- POST /api/test-runs/preflight-cardinality endpoint feeds the chip:
  enhanced DB for access enforcement, explicit projectId filter (Phase 1
  cross-project carry-forward), Zod-validated request body, returns the
  full PreflightResult shape.

* feat(iterations): implement fan-out materializer, worker, and status API

- New lib/services/iterationFanOut.ts with materializeIterations(testRunId, tx)
  and materializeForOneCase helpers. Snapshots dataset rows + parameters
  per parameterized TestRunCase, materializes one TestRunCaseIteration per
  row using the schema's rowIndex field (NOT iterationOrder), and writes
  TestRunCases.totalIterations transactionally. Caller owns the tx.
- Replaces the Wave 0 stub at workers/iterationGenerationWorker.ts. Real
  processor wraps materializeIterations in a 5-min prisma.$transaction,
  emits job.updateProgress every 50 cases (not every iteration), exposes
  parameterizedRunCaseCount in the result. attempts: 1 in the queue config
  guarantees no duplicate-iteration retries.
- POST /api/test-runs/[testRunId]/generate-iterations: enhanced-DB lookup,
  preflight cardinality, three-band response (sync ≤ 500 inline, async
  501..5000 via BullMQ, hardRefuse 422 with breakdown). Audit emission
  via captureAuditEvent (BULK_CREATE on TestRunCaseIteration).
- GET /api/test-runs/iterations/status/[jobId]: mirrors duplicate-scan
  status route exactly, including multi-tenant isolation.
- 7 mocked-tx unit tests + 8 live-DB integration tests (gated by
  RUN_DB_INTEGRATION=1; transactional rollback so DB stays clean).
  Live tests verify rowIndex field name, snapshot immutability under
  source-dataset edits, FK linkage, counter writes — the exact failure
  mode that Phase 2's mocked-tx suite missed (feedback_prisma_helper_live_db_test).

Note: NotificationService.createNotification call deferred to a future wave
to avoid adding a new NotificationType + NotificationContent + emailWorker
i18n branches in this commit. Polling status endpoint is the primary UX path.

* feat(iterations): wire AddTestRunModal to trigger fan-out post-create

- After each createTestRuns() call (one per configId), POST to
  /api/test-runs/[testRunId]/generate-iterations and handle the three
  documented response paths:
    * 422 hardRefuse  → toast.error with iterationCount + cap details
    * 200 async:true  → register on iterationProgressBus.start({jobId,
                       runId, runName, total}); Wave 3 toast/sidebar
                       consumes the bus.
    * 200 async:false → invalidate ZenStack caches via the locked
                       ["zenstack", "ModelName"] prefix so iteration
                       counts surface immediately in the run UI.
- Fan-out failures are non-fatal: the run row still exists, only iteration
  generation failed. Surface via toast.error but do NOT throw, so other
  configs in the loop still get their fan-out attempt.
- New stub lib/services/iterationProgressBus.ts with the same signature
  Wave 3 will fill in. Records jobs to a module-level Map so a future
  Wave 3 consumer mounted at app startup can recover in-flight jobs.
- Six new i18n keys under parameters.* for the run progress + hard-refuse
  surfaces (runProgressTitle, runProgressDescriptionAsync/Sync,
  runProgressFailed, runHardRefuseTitle, runHardRefuseDescription).
  Crowdin sync intentionally deferred to Task 15's dedicated i18n sweep.

* feat(iterations): extend submit-result with worst-of rollup and counters

Adds optional iterationId to the submit-result Zod schema and gates the
new behavior behind it. When set, all four transactional effects land in
one prisma.$transaction:

  1. TestRunResults row carries the iterationId FK.
  2. The TestRunCaseIteration row is marked completed (statusId,
     isCompleted=true, completedAt=now).
  3. passedIterations / failedIterations are recomputed from the live
     iteration set; totalIterations stays as written at fan-out time.
  4. TestRunCases.statusId is updated via worst-of priority (Failed >
     Exception > Blocked > Retest > Untested > Skipped > Passed).

When iterationId is absent the route remains byte-identical to before
(PARAM-07: non-parameterized cases see no behavior change).

The rollup helper lives co-located in worstOf.ts as a pure function,
keyed on the seeded Status systemNames. It takes a status lookup map so
the route can load Status rows once per request and feed them in. The
helper is unit-tested by a 7x7 pairwise priority matrix plus single-
iteration, null-statusId-as-untested, and empty-list defensive cases.

Audit emission for ITERATION_RESULT_RECORDED happens AFTER commit (best-
effort, never blocks the response). The audit boundary calls
redactValues against the snapshot's parametersJson so sensitive
parameter values are scrubbed for viewers without the canReadSensitive
permission.

Live-DB integration coverage exercises a full 3-iteration parameterized
run inside a rollback transaction: pass/fail/pass yields counters 2/1/3
with statusId resolved to Failed; partial-submit shows untested rollup
while iterations remain unset; FK persistence confirmed end-to-end.

* feat(iterations): add cardinality preflight chip and dialogs to AddTestRunModal

- Add RunPreflightChip with band-tinted Badge (sync/async/softConfirm/hardRefuse)
  and tooltip; debounces preflight queries at 250ms
- Add RunCardinalityHardRefuseDialog (informational breakdown table per case)
- Add RunCardinalitySoftConfirmDialog (AlertDialog with ETA, Confirm/Cancel)
- Wire chip + dialogs into AddTestRunModal Step 1 (TestCasesDialog footer);
  hard-refuse disables Submit and clicking the chip opens breakdown;
  soft-confirm gates Submit with confirmation latch
- Add 22 i18n keys under parameters.* for chip, breakdown, soft-confirm,
  progress toast, and generating-state surfaces

* feat(iterations): add iteration generation progress bus, polling hook, and toast

- Replace iterationProgressBus stub with real in-memory pub/sub
  (start/update/remove/subscribe/snapshot) that retains the Wave 1
  start() signature so AddTestRunModal needs no second edit
- Add useIterationGenerationProgress hook: subscribes to the bus and
  drives a single 2s polling loop per non-terminal job against
  /api/test-runs/iterations/status/[jobId]; treats 404 as completed
- Add RunGenerationProgressToast: side-effect-only component that emits
  a sticky sonner toast.custom for each active job (title, progress bar,
  done/total counter, ETA, dismiss button); on completion swaps the
  sticky toast for toast.success and invalidates the ZenStack
  TestRunCases + TestRunCaseIteration caches; on failure swaps for
  toast.error
- Mount RunGenerationProgressMount once in app/[locale]/layout.tsx
  (inside NextIntlClientProvider so useTranslations resolves)

* feat(iterations): add in-page generating-state overlay for iteration sidebar

- Add IterationSidebarGeneratingState that subscribes to
  iterationProgressBus for a single jobId; renders a LoadingSpinner,
  Progress bar, counter, helper copy, and a navigate-away link that
  calls onSkip
- Component reuses the global polling loop driven by
  useIterationGenerationProgress (no second poll started here)
- Includes a smoke test covering render, onSkip click, and bus
  subscription for the matching jobId
- Will be consumed by Wave 4 IterationSidebar (Task 10) when the active
  TestRunCase has totalIterations === 0 and a known active jobId

* refactor(iterations): replace systemName priority map with tier-based worst-of rollup

Tier-based rollup uses the (isSuccess, isFailure, isCompleted) flag triplet
plus Status.order as tiebreaker, instead of relying on admin-defined
systemName values that aren't reliable across projects.

Tier order (worst to best):
  Tier 4: isFailure=true                                  (failure)
  Tier 3: not isCompleted and not isFailure               (incomplete)
  Tier 2: isCompleted and not isSuccess and not isFailure (skipped)
  Tier 1: isSuccess=true                                  (passed)

Within a tier, ties break on Status.order (admin-defined ranking).
Iterations whose statusId is null fall back to the seeded untested row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(iterations): add iteration sidebar with rows, status pips, legend, and bulk toolbar

- IterationStatusPip with 6 locked SVG glyphs + glyph resolver + color map
- IterationStatusLegendPopover with all 6 glyph entries
- IterationRow with checkbox + pip + ordinal + values summary + active-row overflow menu
- IterationBulkToolbar (sticky, single Mark Skipped action; N/A dropped per locked decision)
- IterationSidebar container with generating-state, Cmd/Ctrl+A select-all, Esc clear
- useActiveIterationFromUrl + useSelectedIterationIds hooks
- en-US.json i18n keys for iteration sidebar/header/banner

* feat(iterations): add iteration header + values strip + banner; wire into run page

- IterationHeader (Surface B.2): title + status pip + overflow menu
- IterationValuesStrip (B.3): value chips with sensitive masking and Overridden badges
- IterationOverrideBanner (B.4): inline alert when any value is overridden
- IterationAwareTestRunCaseDetails wrapper hosts iterations query + active selection
- TestRunCaseDetails accepts new optional stepParameters prop and forwards to FieldValueRenderer
- Run page conditionally mounts the iteration UI only when totalIterations > 0 (PARAM-07)
- Wave 5 menu actions stubbed via console.log; Override / bulk-skip wired in Tasks 12 + 13

* feat(iterations): add override values dialog + API route + audit log

Phase 3 Wave 5 Task 12. Adds OverrideValuesDialog (Surface C) +
OverrideUnsavedAlertDialog (C.5) + a Zod factory shared between client and
server + a PATCH route at /api/repository/test-runs/[runId]/cases/[caseId]
/iterations/[iterId]/values. The snapshot's rowsJson is preserved untouched
(PARAM-07). Override emits an ITERATION_VALUES_OVERRIDDEN audit event with
both before (from the snapshot) and after redacted at the audit boundary.
Sensitive params render either a password-masked input + reveal toggle or
a disabled [REDACTED] input depending on the viewer's permission. Reset
iteration action also wired to write a new untested TestRunResults row via
the existing submit-result endpoint. Bulk-skip dialog is Task 13.

* feat(iterations): add bulk-skip dialog + API; extract worst-of rollup to shared service

Phase 3 Wave 5 Task 13. Adds IterationBulkConfirmDialog (Surface D) +
POST /api/repository/test-runs/[runId]/cases/[caseId]/iterations/bulk-skip
that writes one TestRunResults per iteration tied to the seeded skipped
status, in a single prisma.transaction, then recomputes the case rollup
+ counters once. Single-iteration skip routes through the same dialog
with a one-element array. Audit emits ITERATION_BULK_SKIPPED per
iteration after commit with redacted values metadata.

Worst-of rollup helper extracted to lib/services/iterationRollup.ts so
it can be reused by both the submit-result route and bulk-skip; the unit
test file moves with it. Co-located integration test files are now
exempted by the parameter-mutation-coverage guard so fixture seeding
under the route directories does not trip the helper-required scan.

* feat(iterations): add 'Last result' cross-link column to dataset grid

- DatasetTab conditionally renders a trailing 'Last result' column when
  the case has run history (caseHasRunHistory derived from
  useCountTestRunCases).
- Per-row cell shows IterationStatusPip + link button; click navigates
  to /projects/runs/{projectId}/{runId}?iteration={rowIndex+1}&selectedCase={caseId}
  via ~/lib/navigation router.
- Rows without a matching iteration render an em-dash placeholder.
- Adds three en-US.json keys under parameters.* (Wave 7 will run crowdin
  sync for es/fr).
- Component tests cover: column hidden without run history, column shown
  with history, empty cell when no iteration matches, click navigation,
  and most-recent iteration wins when rowIndex repeats.

* chore(i18n): audit Phase 3 iteration keys and sync crowdin locales

- Replace one hardcoded toast description in IterationAwareTestRunCaseDetails
  with new key parameters.iterationResetUntestedMissing
- Add missing UI-SPEC keys to parameters.* namespace: iterationListLoadError,
  iterationBulkNa, overrideTitle, overrideDescription, overrideValidationItem,
  overrideSensitiveDenied
- Run pnpm crowdin:sync to propagate to es-ES.json and fr-FR.json

* test(iterations): close phase 3 validation coverage gaps

- Add live-DB integration test for fan-out atomic rollback: confirms a
  thrown error after materializeIterations leaves no snapshot or
  iteration rows behind once the outer transaction rolls back.
- Add component tests covering URL-driven active-iteration state: a
  ?iteration=N URL param marks the matching row active on mount, and
  pressing Enter on a focused row fires router.replace with the right
  iteration ordinal.
- Fix latent assertion bug in the values-override integration test:
  snapshot rowsJson stores rows shaped as
  { sourceRowId, rowIndex, label, valuesJson }, so the original
  parameter value lives under .valuesJson, not at the top level. Assert
  the iteration's matching snapshot row by rowIndex and use the correct
  dataset row value (alice vs bob).

* feat(repository): add Edit option to test case action menu

Adds an Edit option to the case-row three-dot menu (top of the list)
when the user has canAddEdit permission and is not in run/selection mode.
Links to the case detail page with ?edit=true, which auto-enters edit
mode via a one-shot useEffect on mount.

Reduces friction for users authoring parameterized test cases — the
Configure Parameters sheet is post-create only, so users previously had
to navigate into the case and click Edit manually after saving.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(parameters): render parameter chips in run-mode steps with sensitive reveal and copy

The run-mode step renderer (StepsResults inside FieldValueRenderer)
previously didn't pass `parameters` to its TipTapEditor instances, so step
text in test runs lost the chip substitution wiring that was working in
case-detail/edit modes.

Threading the prop through closes the gap, plus extends the chip itself:

- Sensitive parameters (e.g. password) now render as @name: ••••••, with a
  click-to-reveal toggle (gated by viewer permission upstream).
- Every chip with a value gets a small copy icon that puts the real value
  into the clipboard, with a one-shot ping animation and a localized toast.
- All chip strings (Click to reveal, Copy value, copied/failed toasts) are
  threaded as a `messages` arg to createParameterMentionExtension; the
  TipTapEditor wires next-intl translations and stashes pre-formatted
  toast strings on data attributes for the global click handler to read.

The TipTapEditor's useEditor dependency array intentionally omits
chipMessages — useTranslations may return a new function ref each render,
which would otherwise infinite-loop the editor remount.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(iterations): per-iteration result panel with sequential pass-and-next walking

The case-level result toolbar didn't make sense for parameterized cases:
clicking it submitted a CASE-LEVEL result that the worst-of rollup would
overwrite the next time any iteration result was recorded. The mental
model was confusing — testers couldn't tell what a click would do.

Adds a new IterationResultPanel mounted between the iteration header and
the case body in iteration mode. The panel:

- Shows the iteration label ("Submit result for Iteration N of M") so
  there's no ambiguity about what the action targets.
- Has a primary "Pass & Next iteration" button plus a status dropdown
  for any other status the project has configured.
- After submit, advances to the next iteration by rowIndex (sequential
  walking), falling through to next-case nav after the last iteration.
- Add Result opens AddResultModal scoped to the iteration with steps +
  parameters threaded so per-step results capture works inside an
  iteration and the chip text substitutes the iteration's values.

TestRunCaseDetails takes new optional props (activeIterationId,
activeIterationLabel, stepParameters) and:

- Hides its case-level result toolbar in iteration mode (the panel
  replaces it) while keeping the assignment combobox + prev/next case nav.
- Replaces the case-level status dropdown with a read-only badge plus a
  SquareStack icon, indicating the status is computed across iterations.
- Threads iterationId/iterationLabel/parameters into AddResultModal so
  the modal title shows the iteration context and step text inside the
  modal renders chips with substituted values.

submitTestRunResult now accepts an optional iterationId, threaded into
both inline submit paths and the AddResultModal flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(iterations): drop hardcoded statuses; rollup by dominant frequency; bulk action picks any status

Status names are admin-defined per workflow — assuming a project has a
"Skipped" or "Untested" status by name was wrong. Several surfaces had
hardcoded assumptions; this commit removes them and reshapes the rollup
logic to match the agreed spec.

Status pip is now color-only: every glyph renders as a filled circle
tinted with the status's admin-configured color. The previous SVG glyph
map (notStarted outline / active triangle / passed check / failed dot /
skipped bar / blocked warning) is gone. resolvePipColor always prefers
the explicit statusColor when provided; the two pseudo-states (active,
notStarted) keep their semantic-token fallbacks.

Status legend (sidebar header info popover) drops the hardcoded 6-glyph
list and now fetches the project's actual Test-Run-scoped statuses,
rendering each with its real name + color. Pseudo "Not started" and
"Currently viewing" entries are removed (they aren't statuses).

Bulk action ("Mark with status…") replaces the hardcoded "Mark Skipped":

- The bulk-skip server route now accepts a statusId in the body and
  validates it's a Test-Run-scoped status enabled for the project, instead
  of resolving a hardcoded systemName='skipped'.
- The confirm dialog has a status Select populated with the project's
  actual statuses (default: first by Status.order). The dialog body
  carries a colored border + Confirm button tinted with the picked status
  color, with smooth transitions on subsequent picks (mirrors AddResultModal).
- Toolbar label reads "Mark N iterations with status…" with no plus/skip
  icon; cancel collapses to an icon-only X.
- Iteration row + header overflow menus replace the SkipForward icon
  with Plus and the action label is the same "Mark with status…".

Worst-of rollup rewritten per the locked spec:

- No recorded results → first untested-shape status (lowest order).
- Any failure → most-frequent failure status (tie → lowest order).
- No failures, any success → most-frequent success status (tie → lowest order).
- Some recorded, none success/failure → most-frequent recorded status.

The previous tier-based "incomplete beats passed" logic was wrong: 3
Passed + 1 Untested rolled up to Untested, which dragged the case down
when most iterations had actually passed. The new logic ignores
incomplete iterations once any result is recorded.

IterationSidebar takes a projectId prop now (used by the legend popover
to fetch project statuses); the sidebar test mocks useFindManyStatus so
it doesn't need a QueryClientProvider. IterationHeader's right padding
is bumped to clear the Sheet's top-right close button.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(iterations): read-only computed case status for parameterized cases in run page

The run-page test cases table showed a status dropdown for every row.
Picking a status on a parameterized case wrote a case-level result that
the worst-of rollup would overwrite the next time any iteration was
submitted — silently confusing.

For parameterized cases (totalIterations > 0), the status column now
renders a read-only button with a SquareStack icon hinting that the
status is computed across iterations. Clicking it opens the case sheet
(same selectedCase URL pattern as clicking the case name) so testers
land on the surface where iteration results are actually recorded.

The 3-dot action menu still appears for parameterized cases — Assign +
View in Repository remain — but Add Result and Add Result for Selected
are hidden, since results are submitted per-iteration inside the sheet.

Required plumbing: totalIterations is now selected in the
useFindManyTestRunCases query in Cases.tsx and propagated through the
row mapping so the status cell can detect parameterized cases. Long
status names (e.g. "Awaiting Review") truncate cleanly so the
SquareStack icon doesn't overlap the text.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(runs): smooth step transition + sticky footer + richer cardinality tooltip

Three small UX fixes to the Add Test Run modal flow.

Step transition: BasicInfoDialog and TestCasesDialog each rendered their
own DialogContent, so swapping between steps caused React to unmount one
and mount the other — Radix played exit + entry animations and the modal
visibly closed and reopened. Both step components now return Fragments
with their inner content; a single shared DialogContent in the parent
stays mounted across step changes, with className adapting to step (small
for step 0, full-screen for step 1). The transition is now an instant
content swap.

Sticky footer in step 2: the case list (ProjectRepository) lived in a
flex-1 div without min-h-0/overflow constraints, so the case body grew
beyond the viewport and pushed the footer (Back / Save + preflight chip)
below the fold. Adding min-h-0 + overflow-y-auto constrains the middle
area so it scrolls internally and the footer stays pinned.

Cardinality tooltip: previously read "{N} iteration(s) = {C} case(s) ×
{F} config(s) × dataset rows" — the count of dataset rows was missing,
and the math was misleading because cases × configs × rows assumes
uniform row counts. The tooltip now renders a header line with totals
plus a per-case breakdown showing each case's actual row count, with
proper contrast on the purple tooltip background.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(i18n): translations for iteration UI updates

Adds new keys for the iteration result panel, status legend, bulk dialog,
chip click-to-reveal/copy strings, AddTestRunModal preflight tooltip,
and the read-only parameterized status badge. Updates the bulk-action
labels ("Mark with status…") and removes obsolete keys for the
hardcoded-status legend rows that were dropped.

Synced via pnpm crowdin:sync to es-ES and fr-FR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(runs,sessions): show pulsing flame icon next to recently created items

Adds a small visual cue (orange pulsing Flame icon with "New" tooltip)
next to test runs and test sessions that were created in the last 5
minutes. Helps users spot the item they just created in a long list
without scanning for it.

TestRunItem reads createdAt from the run record (newly threaded through
TestRunDisplay). SessionItem already received createdAt; just consumes
it. Reuses the existing common.labels.new translation key.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style(parameters): apply prettier formatting to ConfigureParametersSheet

Whitespace-only diff: multi-line imports re-wrapped, no behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Merge branch 'main' into features/parameterized-test-cases

Brings 102 commits from main (v0.27.7) into the parameterized-test-cases
feature branch. Notable upstream changes integrated:

- Webhooks dispatch system (queues, workers, audit actions, schema
  expansion of WebhookConfig with subscribed events / endpoint health /
  retry tracking)
- 13-language i18n support (de, es, fr, it, ja, ko, nl, pl, pt-BR, vi,
  zh-CN, zh-TW)
- Stricter next-intl typed interpolation (numeric placeholders require
  explicit String() conversion at call sites)
- Hardened audit-context plumbing in /api/test-runs/submit-result
- Execution Log pre-built report
- Increased Node heap requirement to 24GB for build/type-check
- Various CodeQL fixes and CI workflow upgrades

Conflict resolutions:

- schema.zmodel: take main's expanded WebhookConfig model; keep BOTH
  branches' AuditAction enum additions (webhook events from main +
  iteration events from this branch)
- package.json / queueNames.ts / queues.ts / ecosystem.config.js /
  scripts/build-workers.js: keep both worker registrations (iteration
  generation from this branch + 3 webhook workers from main); preserve
  this branch's `node scripts/fix-zenstack-symlink.js` step in the
  generate / generate:dev scripts
- app/api/test-runs/submit-result/route.ts: combine main's
  isAutomatedRun / needsAutomatedFlip + ES-sync side effect with this
  branch's iteration audit emission and viewerCanReadSensitive resolution
- columns.tsx: preserve main's configuration injection into
  onOpenAddResultModal AND this branch's totalIterations prop
- AddTestRunModal.tsx: keep both branches' translation hooks (tParameters
  + tGlobal) and queryClient
- TextFromJson.tsx and StepsDisplay.tsx: take this branch's Phase 2
  parameter-chip enrichment (richer than main's pre-Phase-2 version)
- es-ES.json / fr-FR.json: take main's upstream Crowdin translations;
  next-intl falls back to en-US for missing keys until next CI sync
- 10 new locale files (de-DE etc.) come from main as tracked files

Type-strict next-intl fixups: wrapped numeric placeholders with String()
in 15+ files following main's pattern in commit 08a0c9cf.

Auto-generated artifacts (prisma/schema.prisma, lib/hooks/__model_meta.ts,
lib/openapi/zenstack-openapi.json) regenerated via `pnpm generate` after
the schema.zmodel merge.

Verification: type-check clean (24GB heap); test suite 7092 passing /
0 failing / 20 skipped (up from 6223 pre-merge — main brought ~870 tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(parameters): clean up lint errors in parameter authoring components

Pre-existing lint debt on the parameterized-test-cases branch surfaced
once main's stricter lint gate caught up. Three categories of fixes:

- Wrap fire-and-forget Promise calls (queryClient.invalidateQueries,
  loadDataset, commitCell) with the `void` operator so eslint's
  no-floating-promises rule passes.
- Drop unused imports / variables (zodResolver, AlertDialogAction,
  tActions, baseHandlers, dead `bubbled` flag in a Cancel-key test).
- Wrap bare JSX literals ("STRING" / "INTEGER" / "BOOLEAN" / "SELECT"
  in the type Select; "@" prefix on parameter chip labels) in JSX
  expression containers per react/jsx-no-literals.

No behavior change. 22 floating-promise errors + 11 jsx-no-literals +
5 unused-var errors removed; lint now reports 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(lint,i18n): codebase-wide cleanup sweep

Changes assembled from two passes that don't fit elsewhere:

- `pnpm exec eslint . --fix` swept ~20 files: replaced ad-hoc red color
  classes (text-red-500, text-red-600 dark:text-red-400) with the design
  system token text-destructive in EntityList; removed stale unused
  eslint-disable comments scattered across admin pages, tests, and a
  handful of components; minor formatting fixes.
- `pnpm crowdin:sync` pulled fresh translations for all 13 locales
  (de-DE, es-ES, fr-FR, it-IT, ja-JP, ko-KR, nl-NL, pl-PL, pt-BR, vi-VN,
  zh-CN, zh-TW) — mostly polishing webhook/MCP UI strings that landed in
  recent main commits.

No behavior change. Lint reports 0 errors after this commit (53
warnings remain, all pre-existing react-compiler / exhaustive-deps
notes inherited from main that would need careful per-case analysis to
"fix" without risking regressions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(iterations): three runtime bugs surfaced by 5,500-row UAT repro

Driven by stepping through Add Test Run -> async fan-out -> open the
parameterized case sheet on a case with 4,800+ iterations.

- TestRunCaseDetails received a `key` via spread props in page.tsx,
  which React 19 now flags as an error. Pass it as a JSX attribute.
- IterationAwareTestRunCaseDetails included `dataSetSnapshot` on every
  iteration row in a single findMany. Above ~1,500 rows the response
  duplicates the same snapshot N times and overflows Prisma's napi
  string buffer (HTTP 400, "Failed to convert rust String into napi
  string"). Split into a single findFirst for the snapshot plus a slim
  findMany for iteration rows. Two queries per case sheet open, no
  per-row fan-out.
- RunGenerationProgressToast set `{ id: job.jobId }` on toast.custom
  using BullMQ's sequential numeric job id ("2", "3"). Sonner's
  auto-generated toast ids are also small integers, so they collided
  and React warned about duplicate child keys in the Toaster. Namespace
  as `iter-job-${jobId}`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(parameters): paginate Dataset tab and polish for large datasets

Surfaced when seeding 5,500 dataset rows on a parameterized case for
cardinality UAT — the existing Dataset tab fetched and rendered every
row in one shot.

Server (api/repository/cases/[caseId]/dataset GET):
- Accept ?page= / ?pageSize= (default 50, max 200). Absent params keep
  the original full-fetch behavior for back-compat. Response now
  includes `totalRows`, `page`, `pageSize`.

DatasetTab:
- Pagination state + footer mirroring UnifiedSearch (showing X-Y of Z
  results + first/prev/page-input/next/last chevrons).
- Loading state with a 300ms grace window so quick fetches don't flash
  a spinner; no more "empty state" flash during the initial fetch.
- Sticky thead with solid bg-card so rows don't bleed through on scroll.
- Drag-drop insertion line: SortableDatasetRow exposes a
  top/bottom dropIndicator (computed from useSortable's over/active
  indices); DatasetTab renders a 3px primary-colored line absolute on
  the drag-handle cell. Drag-reorder writes back absolute rowIndex so
  page-scoped reorders don't shift other pages.
- Shift-click row selection range (mirrors the DataTable pattern in
  DuplicateResultsTable). Anchor index tracked in lastSelectedIndex.
- "Last Result" cell now uses the existing StatusDotDisplay component
  for the colored dot + status name, rendered as a plain hover-underline
  link instead of "View {Status} result" in primary color.
- Toolbar shows full totalRows (not visible-page count) and carries the
  editing-hint string under the count summary.
- Bulk-delete now fires onAfterDelete to refresh the page (latent
  staleness bug exposed by paging totalRows).

DatasetRowActions:
- New optional `onAfterDelete` callback so the parent can re-fetch.

i18n:
- en-US: rename datasetFooterHint -> datasetEditingHint, drop the
  unused datasetLoading key (use common.loading instead). Reuse
  common.pagination.showing / common.of / common.results /
  search.results.page rather than dataset-specific copies.
- Crowdin sync brought across renames + a few unrelated upstream
  translation edits (de-DE webhook hint, ja-JP polish, etc.).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(iterations): UAT polish — clearer nav + honest progress wording

Surfaced during Phase 3 UAT walkthrough.

Iteration header (IterationHeader):
- Add Previous / Next iteration arrow buttons flanking the
  "Iteration N of Total" title, wired to setActiveRowIndex so URL +
  active iteration update in place. Disabled at the ends; tooltips
  read "Previous iteration" / "Next iteration".

Case-nav buttons (TestRunCaseDetails):
- Add tooltips to the Back / Next chevron buttons in the case header.
  Users on parameterized cases were confusing them with iteration nav;
  the tooltips now read "Previous Case" / "Next Case" so the intent is
  obvious in both parameterized and non-parameterized contexts.

Cardinality soft-confirm dialog (RunCardinalitySoftConfirmDialog):
- Drop the "Estimated time: N minutes" paragraph and the
  "background job and may take a few minutes" sentence. The 1-minute-
  per-1000-iterations heuristic was wildly pessimistic — a 5,000-row
  generation finishes in seconds, so promising "5 minutes" then
  finishing immediately felt jarring.
- Tighten the description copy.

Generation progress toast (RunGenerationProgressToast):
- Drop the same "N minutes remaining" estimate from the bottom-right
  of the in-progress toast. The progress bar + done/total counter
  already communicate progress without misleading the user.

i18n:
- Drop unused softConfirmEta and runProgressEta keys.
- Add iterationPrevAria / iterationNextAria for the new arrows.
- Crowdin sync brings translations across.

IterationResultPanel:
- Drop redundant `mr-1` from icon-prefixed Buttons (shadcn handles
  icon+text spacing; matches the project's no-Button-gap convention).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(shared-datasets): add DataSetVersion model for immutable per-save history

Mirrors the RepositoryCaseVersions pattern. Every explicit save in the
shared-dataset editor will insert one row; rows are never edited or
soft-deleted (soft-deleting the parent DataSet is the correct cascade).

- New DataSetVersion model with monotonic version per dataSetId
- Denormalized rowCount so the cardinality preflight stays O(1)
- Named back-relations SnapshotPinnedVersion and AssignmentPinnedVersion
  reserved for sibling models added next
- Read/write access policies inherit from the parent DataSet project
  via the same SharedSteps RBAC matrix DataSet uses
- Back-relations wired on DataSet (versions, sharedAssignments) and
  User (createdDataSetVersions, createdSharedDataSetAssignments) so
  the relation graph is complete once sibling models land

* feat(shared-datasets): add CaseSharedDataSetAssignment with cross-project denial

Per-case binding of a project-scoped shared dataset, distinct from
TestCaseParameter.lookupDataSetId (which sources SELECT-allowed-values
for a single parameter). One shared assignment per case, enforced by
@@unique on caseId.

- Cross-project denial at the policy layer:
  @@deny('create,update', sharedDataSet.projectId != case.projectId)
  Defense-in-depth alongside the route-layer guards added in later
  plans of the milestone.
- pinnedVersionId nullable so "follow latest" is representable without
  a magic number; the iteration resolver treats null as "use the
  DataSet's current version pointer at run-create time."
- onDelete: Restrict on sharedDataSetId blocks hard-deleting a shared
  dataset that still has active assignments; soft-delete flow surfaces
  the count to the user before flipping isDeleted on both sides.
- mappingJson is non-nullable Json; the API route enforces non-empty
  payload because Prisma does not support default {} for Json columns.
- Indexes on caseId, sharedDataSetId, pinnedVersionId, createdById.
- Read/write policies mirror TestCaseParameter (TestCaseRepository
  area canAddEdit), scoped through case.project.*.
- Back-relation sharedDataSetAssignment added on RepositoryCases.

* feat(shared-datasets): record snapshot version pin via sourceVersionId

- Add nullable sourceVersionId + sourceVersion relation on
  TestRunCaseDataSetSnapshot (named relation SnapshotPinnedVersion,
  onDelete: SetNull) so audit can answer "which version drove this run?"
  even when the assignment follows latest.
- Add @@index([sourceVersionId]) for snapshot-by-version lookups.
- Add @@deny('create', ...) cross-project guard preventing a snapshot
  from pinning a version whose dataset belongs to a different project
  than the run.
- Existing sourceDataSetId/sourceDataSet/sourceDataSetName retained
  unchanged.

* chore(generated): regen ZenStack hooks + Prisma client for shared-datasets schema

Produced by `pnpm generate` after the Wave 1 schema additions land.

- New: lib/hooks/data-set-version.ts (full CRUD set)
- New: lib/hooks/case-shared-data-set-assignment.ts (full CRUD set)
- Updated: hooks/__model_meta.ts, hooks/index.ts,
  hooks/test-run-case-data-set-snapshot.ts (now exposes sourceVersion
  relation), prisma/schema.prisma, openapi/zenstack-openapi.json.

Project convention is to track these so CI builds stay deterministic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(shared-datasets): add project-scoped list/create endpoint

- New zod schema for the shared-dataset create payload
  (name + optional description, length-bounded).
- New API route GET /api/projects/[projectId]/datasets returning a
  paginated list of project-scoped shared datasets, including the
  latest version row (rowCount + parametersJson + createdBy) for
  version-label rendering and `_count` of sharedAssignments / versions
  for the soft-delete confirmation flow.
- New API route POST creates a shared dataset (no ownerCaseId,
  isShared=true, version=1) and emits a CREATE audit event.
- Cross-project access is enforced by the inherited DataSet read
  policy via getEnhancedDb(session); no route-level filter.

* feat(shared-datasets): add per-case shared-dataset assignment endpoint

- New zod payload schema (sharedDatasetAssignmentSchema) with bounded
  string lengths and the shared __skip__ sentinel constant.
- New /api/repository/cases/[caseId]/shared-dataset route exposing GET
  (read current assignment), PUT (atomic upsert with mappingJson +
  required-coverage validation), DELETE (hard-delete the assignment).
- Cross-project denial enforced at the route layer in addition to the
  schema @@deny: returns 422 cross_project when the requested shared
  dataset lives in a different project than the case.
- Required-parameter coverage check resolves the pinned (or latest)
  DataSetVersion, derives column names from parametersJson with a
  defensive rowsJson[0] fallback, and returns 422 required_unmapped
  with the missing parameter list.
- Audit metadata logs mapping COLUMN names only; values (parameter
  names) are intentionally omitted.
- Owner-dataset and shared-dataset assignment are allowed to coexist
  on the same case; this endpoint never refuses based on owner-dataset
  presence.

* feat(shared-datasets): resolve shared-dataset assignments in iteration fan-out

- Extract a pure applyMapping helper into lib/utils/datasetMapping (no
  Prisma imports, safe for client bundles). Uses Object.entries +
  hasOwnProperty.call so prototype-pollution attempts via crafted
  mapping keys cannot escalate into the output.
- Re-export applyMapping + SKIP_SENTINEL from iterationFanOut for
  ergonomic server callers; client code imports the pure module
  directly to avoid pulling Prisma into the client bundle.
- Extend materializeForOneCase with a two-step resolver: try the
  case's owner dataset first (unchanged); fall back to the case's
  shared-dataset assignment, walking pinnedVersionId or following
  latest. Owner-wins when both are present.
- Defense-in-depth cross-project check on the shared assignment
  (the resolver runs against the unenhanced client).
- Populate the new nullable sourceVersionId column on the snapshot.
  Owner-only path stays bit-for-bit identical on every other column.
- Unit tests for the mapping helper (9 cases including
  prototype-pollution and skip-sentinel paths).
- Live-DB integration test exercising 7 resolver paths: owner-only
  regression, shared+pinned, shared+follow-latest, skip-sentinel,
  owner+shared coexistence (owner-wins), cross-project defense-in-depth,
  and the no-source canonical shape. Skipped unless RUN_DB_INTEGRATION=1.
- Existing iterationFanOut mocked-tx test buildTx extended to stub
  the new caseSharedDataSetAssignment + dataSetVersion surfaces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(shared-datasets): add dataset save endpoint with versioning

- New POST /api/projects/[projectId]/datasets/[dataSetId]/save route
  that bumps DataSet.version and inserts a new immutable DataSetVersion
  capturing parametersJson + rowsJson + rowCount each save.
- On the first save of a previously-unversioned shared dataset, the
  handler performs a lazy v1 backfill: it captures the pre-edit baseline
  rows AND derives parametersJson from the column names of the first
  live row's valuesJson (each column becomes a STRING, non-required
  parameter at its natural index). This guarantees v1 always carries a
  non-null parametersJson matching the v2+ shape so downstream mapping
  validation always has a target.
- Live DataSetRow[] is replaced atomically inside the transaction:
  existing rows soft-deleted, new payload createMany'd as fresh rows
  (no deleteMany — preserves audit per the project's soft-delete rule).
- Refuses owner datasets (returns 422 if isShared === false) — owner
  datasets keep the inline-edit path; shared datasets are versioned via
  this explicit save endpoint.
- Audit emitted as UPDATE entityType DataSet with metadata
  { newVersion, rowCount, branchedFromVersionId, backfilledV1 }.
- Returns { ok, version, dataSetVersionId, rowCount }.
- Zod schema sharedDatasetSaveSchema validates payload with a 5,000-row
  cap matching the cardinality hard cap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(shared-datasets): add dataset detail + soft-delete endpoint (route.ts)

This commit adds the actual /api/projects/[projectId]/datasets/[dataSetId]
route handler (the dataset detail GET + soft-delete DELETE with the
?confirm=true assignment-count guard). The earlier commit 604b7e6f under
the same product feature was sequenced into a parallel-execution race in
the shared worktree and ended up capturing concurrent files from other
plans; the correct route file is shipped here.

- GET: parent DataSet row + latest DataSetVersion summary; 404 when the
  dataset is missing, not shared, or outside the path project.
- DELETE: returns 409 has_assignments + assignmentCount when active
  assignments exist and ?confirm=true is absent. With confirmation, the
  parent flips isDeleted=true and active CaseSharedDataSetAssignment
  rows are hard-deleted in the same transaction (per-row, no
  deleteMany). DataSetVersion rows remain intact; snapshot capture
  preserves historical fidelity for in-flight runs.

* feat(shared-datasets): consume shared-dataset row counts in cardinality preflight

- Extend PreflightCaseInput with optional assignedRowCount (defaults to
  0). computePreflight now uses Math.max(rowCount, assignedRowCount) so
  shared-dataset cases contribute their version row count to the
  iteration cap math. Owner-only callers are unaffected because
  Math.max(n, 0) is n, and the existing 24 cardinality unit tests stay
  green without edits.
- Extend the preflight route findMany select to fetch the case
  sharedDataSetAssignment plus nested pinnedVersion.rowCount and
  sharedDataSet.{id,version,isDeleted}. Per-case mapping derives
  assignedRowCount from either the pinned version (if any) or the
  highest-version DataSetVersion for the assigned dataset (follow-latest
  branch, one extra query per case-with-shared-and-no-owner).
- Owner-wins: when ownerRowCount > 0 the route forces assignedRowCount
  to 0 before passing to computePreflight, so a case carrying both an
  owner dataset and a shared assignment reports the owner row count.
  Matches CONTEXT Amendment A and the iterationFanOut resolver.
- Soft-deleted shared dataset (isDeleted) is silently dropped from the
  count so users do not get refused on rows they cannot reach.
- Live-DB integration test exercises 8 case-shape combinations,
  including the Pitfall 2 hard-refuse regression guard: 6 × 1000-row
  pinned cases must classify as hardRefuse, not async. Skipped unless
  RUN_DB_INTEGRATION=1; mirrors existing live-DB test pattern with a
  rolled-back prisma.$transaction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(shared-datasets): align preflight integration test bands with default thresholds

The pinned-shared-assignment case used 200 rows × 1 config, which
classifies as "sync" under the locked defaults (asyncCap=500). Bumping
to 700 rows lands in the asyncCap < total ≤ softCap window so the
"async" band is exercised. All 8 cases now pass against the live test
DB. Discovered when running with RUN_DB_INTEGRATION=1 (Rule 1 — author
miscount on band boundaries; resolver code itself is unaffected).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(shared-datasets): add Project Settings → Shared Datasets list view

- Adds a new "Shared Datasets" entry to the project settings nav
- New page at /projects/settings/{id}/datasets with a TanStack Table
  list of all shared datasets in the project (name, columns, rows,
  version, last edited, owner, in-use count)
- Create-dataset dialog (name + optional description) that POSTs to
  the project-scoped endpoint and routes the user to the new dataset
- Two-stage delete confirmation: stage 1 always, stage 2 surfaces the
  active assignment count returned by the 409 response so the user
  must explicitly acknowledge that consumer cases will be unassigned
  (snapshots remain intact)
- Uses shadcn AlertDialog/Dialog throughout — never window.confirm

* feat(shared-datasets): shared dataset editor + version picker

- DatasetTab gains an optional mode prop ("shared-editor" |
  "shared-readonly"). When set, rows are supplied by the parent and
  cell edits, add-row, drag-reorder, and bulk-delete all flow through
  onRowsChange instead of the per-case API. CSV/Paste affordances are
  hidden in shared mode. When mode is omitted the per-case behavior
  is preserved byte-for-byte (PARAM-07 invariant).
- New /projects/settings/{id}/datasets/{dataSetId} editor page wraps
  DatasetTab in shared-editor mode with a Save button (explicit commit
  boundary, no autosave) and a version picker. Save invalidates the
  ZenStack caches and toasts the new version number.
- Selecting a historical version flips the editor to read-only;
  starting to edit a historical version surfaces a banner explaining
  the change will branch, and Save produces a new version branched
  from that point.
- SharedDatasetVersionPicker is reusable across the editor and the
  upcoming assignment dialog via a mode prop ("editor" | "picker").
  In picker mode the "current" item is structurally absent — the
  consumer dialog provides its own "pin to current" radio.

* feat(shared-datasets): wire Configure Parameters to shared datasets

Add a Local/Shared source toggle to the dataset tab plus the
"Assign shared dataset" dialog so a parameterized case can pull its
iteration rows from a project-level shared dataset.

- DatasetTab: new toolbar segmented control, read-only shared view
  with version-pin badge + "View source" link, owner+shared
  coexistence info banner (iteration semantics: local wins), and an
  AlertDialog that confirms removing the shared assignment when
  switching back to Local.
- AssignSharedDatasetDialog: choose dataset, pick how versions are
  tracked (defaults to "Pin to current version", with "Follow latest"
  and "Pin to a specific version" reusing the version picker), and
  map dataset columns to the parameters on the case. Allowed to
  coexist with a local dataset (info banner only, no refusal).
- SharedDatasetMappingFields: case-insensitive auto-map, collapse
  matched rows, top alert listing required parameters that are not
  mapped, optional "Show all columns" toggle.
- datasetMapping.ts: extract pure autoMapColumns and
  findUnmappedRequiredParameters so the dialog and the existing CSV
  wizard share the same vocabulary without pulling Prisma into the
  client bundle.
- DatasetTab tests: add stubs for the new auto-generated hooks and
  next-intl Link so the existing assertions keep passing; the three
  pre-existing "View Failed result" failures are unchanged.
- en-US.json: add the new dataset-source, assign-shared-dataset, and
  mapping keys.

* test(shared-datasets): unit tests for routes + dialog + cardinality

- iterationCardinality.shared.test.ts: assignedRowCount path, owner-wins
  defensive merge, backward-compat with Phase 3 input shape, and
  hardRefuse / softConfirm classification across shared rows.
- datasets/route.test.ts: list filters by isShared+!isDeleted, POST
  creates with isShared:true / version:1 / null description default,
  audit emitted on create.
- datasets/[dataSetId]/route.test.ts: GET returns latest-version
  summary, DELETE soft-deletes, returns 409 has_assignments without
  ?confirm=true, removes assignments per-row when confirmed.
- datasets/[dataSetId]/save/route.test.ts: lazy v1 backfill on first
  save (parametersJson derived from live row column names; never null
  — even for empty pre-edit), v3→v4 path, branchedFromVersionId=2 with
  current=5 still writes v6 (audit-only branch source), 5000-row cap,
  owner-dataset 422.
- shared-dataset/route.test.ts: PUT upserts assignment without
  owner-dataset refusal (Amendment A coexistence), cross_project /
  not_shared / required_unmapped / unknown_columns guards, audit logs
  KEYS only (no parameter names).
- AssignSharedDatasetDialog.test.tsx: empty-state, three-pin radios,
  W4 picker mode (no "current" item), Amendment-A info banner with
  Save still working, required-unmapped disables Save.

* test(shared-datasets): add E2E specs covering happy path and denial

- shared-datasets-flow.spec.ts: project admin creates a shared dataset,
  saves the first version, declares matching parameters on a case,
  switches the Dataset tab to Shared, assigns the dataset (auto-mapped,
  default pin = current), verifies the read-only header renders.
- cross-project-denial.spec.ts: PUT to /api/repository/cases/<A>/
  shared-dataset with project B sharedDataSetId returns 422 with
  error=cross_project. Locks the route-layer cross-project guard.
- owner-shared-coexistence.spec.ts: regression guard for the
  Amendment-A coexistence flow — a case with a local dataset
  successfully accepts a shared-dataset assignment, the owner banner
  in the dialog renders, and the Dataset tab shows the
  owner+shared coexistence banner in both Local and Shared views.

Specs follow the data-testid-first hierarchy and use the existing
ApiHelper for project/folder/case fixture setup. They are intended to
run against the production build via:
  pnpm build && E2E_PROD=on pnpm test:e2e e2e/tests/shared-datasets/

* chore(i18n): crowdin sync — pull translations for shared dataset keys

en-US.json was unchanged: every key listed in the plan table already
shipped under semantically-identical names in Plans 04-05 / 04-06
(e.g. assignSharedTitle vs assignSharedDialogTitle, assignSharedSection2
vs assignSharedPinSectionTitle). Per the i18n-reuse rule, no
duplicates added. Crowdin sync pulled the missing translations into
all 12 non-English locales.

* fix(shared-datasets): two backend bugs surfaced by Phase 4 UAT

1. Cross-project @@deny on DataSet is not null-safe in ZenStack v2.
   The Phase 1 policy `ownerCaseId != null && ownerCase.projectId !=
   projectId` is logically null-safe via &&, but ZenStack v2's runtime
   Zod validator dereferences `ownerCase` regardless of the short-
   circuit and rejects the null relation when creating shared datasets
   (where `ownerCaseId` is null by definition). Workaround: split the
   POST into a project read-gate via the enhanced client + a raw
   `prisma` create. Cross-project denial is preserved by the read gate
   (the enhanced client returns null for projects the caller can't
   access). The schema policy itself is unchanged — owner-dataset
   paths still rely on it. Added an explicit `ownerCase != null`
   clause as defense-in-depth (it doesn't fix the runtime bug but
   documents intent and pre-empts future codegen changes).

2. Save endpoint for shared datasets collided with the
   `@@unique([dataSetId, rowIndex])` constraint. The save flow
   soft-deletes existing rows then creates new ones — but soft-deleted
   rows still occupy their `rowIndex` slot per the constraint, so
   re-using indices 0..N fails on the second save. Insert at
   `(maxExistingRowIndex + 1) + i` instead. The downstream consumers
   only use rowIndex for ordering, so monotonically growing values
   are fine.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(shared-datasets): column authoring + branch-from-historical UI

Plan 04-05 deferred shared-dataset column authoring to a follow-up;
combined with paste-CSV / import-CSV being hidden in shared mode, a
freshly-created shared dataset was unauthorable from the UI alone.
Phase 4 UAT surfaced the gap. This adds the missing affordances and
also fixes two related bugs:

DatasetTab:
- New "Add column" button (shared-editor mode only) opens an
  AlertDialog with a Column name input + duplicate-name guard.
  Submits via `onParametersChange` so the parent buffers the change
  alongside row edits and persists everything in one explicit Save.
- Drag-handle column locked to 32px and select column locked to 40px
  on both `<th>` and `<td>` so they no longer grow with cell content.

SharedDatasetEditor:
- Track `pendingParameters` alongside `pendingRows`; thread
  `onParametersChange` to DatasetTab.
- Send the editor's pending parameters in the Save POST body
  (previously the route received a `sourceParameters` snapshot from
  the latest version, which dropped any in-session column additions).
- Fix stale closure: `editorParameters` was missing from `handleSave`'s
  useCallback deps so column changes never reached the POST body.
- Historical view (selectedVersion !== "current") was forcing
  shared-readonly mode + hiding all edit affordances, making the
  branch-from-historical flow unreachable from the UI. Always use
  shared-editor mode in the editor page; the existing banner already
  warns that edits will create a new branch.

i18n:
…
Establishes the parent linkage between releases/v0.29 and main that was
lost when PR #320 squashed the parameterized-test-cases branch into a
single commit. The squashed commit already contained main's v0.28.0
work (i18n release + queue presets + CHANGELOG cleanup), so the only
conflicts were in three locale JSON files where main's bare entries
collided with the branch's parameterized strings.

Conflict resolutions (all in testplanit/messages/):

- de-DE.json: HEAD adds the datasets key main does not have. Took HEAD.
- ru-RU.json, tr-TR.json: main added these as bare locale stubs in
  PR #318; the squashed commit on this branch already carries them
  populated with the parameterized strings. Took HEAD for both — net
  result is identical to HEAD, so this commit is a parent-linkage
  merge with no tree changes.

Verified with pnpm crowdin:sync (no diff against the resolved tree)
and pnpm lint (0 errors).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread testplanit/lib/utils/datasetMapping.test.ts Fixed
CodeQL fixes:
- datasetMapping.test.ts: prototype-pollution test rebuilt with
  Object.create(null) + bracket assignment instead of the
  `__proto__: "p"` object literal that CodeQL flagged as an
  invalid prototype assignment.
- datasets-list.tsx: dropped the redundant `open={pendingDelete !== null}`
  comparison (already guarded by the surrounding `pendingDelete ? ...`).

Prettier pass on the 116 files that had been drifting from the project's
prettier config — the v0.29 merge surfaced them all at once. Pure
formatting; no behavior changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread testplanit/lib/utils/datasetMapping.test.ts Fixed
Comment thread testplanit/lib/integrations/adapters/GitHubAdapter.ts Fixed
therealbrad and others added 2 commits May 19, 2026 14:42
Short notice pointing users to the parameterized-test-cases user guide.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- datasetMapping.test.ts: previous fix (bracket-assignment to __proto__
  on a null-prototype object) was still flagged. Switched to
  Object.defineProperty per CodeQL's updated recommendation so the
  __proto__ key is unambiguously a normal own data property, never
  the prototype-setter literal.
- GitHubAdapter.ts: reordered isTiptapDoc's checks so `value !== null`
  precedes `typeof value === "object"`. CodeQL narrows value to non-null
  after the typeof check and was flagging the subsequent null comparison
  as comparing-against-an-impossible-type.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread testplanit/lib/utils/datasetMapping.test.ts Dismissed
CI's default `pnpm test` runs without a DATABASE_URL, so
data-model-foundation.test.ts was failing at setup time when its
PrismaClient tried the first query. Other live-DB integration suites
(lib/services/iterationFanOut.integration.test.ts and siblings)
already use a `RUN_DB_INTEGRATION=1` + DATABASE_URL gate via
`describeIntegration = describe.skip` — adopt the same pattern here.

Effect:
- CI: the 14 tests in this file silently skip; the suite passes.
- Local: set RUN_DB_INTEGRATION=1 with a seeded dev DB to run them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@therealbrad therealbrad merged commit 502e137 into main May 20, 2026
5 checks passed
@therealbrad therealbrad deleted the releases/v0.29 branch May 20, 2026 10:49
@therealbrad
Copy link
Copy Markdown
Contributor Author

🎉 This PR is included in version 0.29.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant