Releases/v0.29#321
Merged
Merged
Conversation
* 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>
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>
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>
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>
Contributor
Author
|
🎉 This PR is included in version 0.29.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
cases × configurations × dataset rowswhen a run is created. Synchronous below the async cap, queued via the newiteration-generationworker above it, hard-refused at the safety ceiling. Per-iteration result entry, with per-case rollup using worst-of status.iteration.result.recordedevent, plus per-iteration redacted values appended to the existingtest_run.completedpayload. Sensitive parameter values are redacted server-side for non-privileged viewers.Include parameterstoggle in the generation wizard. When enabled, the assistant emits a parameter schema alongside the steps.Included from v0.28.0
defaultJobOptionspresets across the queue layer.Related Issue
Ships the v0.29.0 milestone.
Type of Change
Testing
pnpm test)pnpm lint— 0 errorspnpm type-check— cleanpnpm build— cleanE2E_PROD=on pnpm test:e2e) — passing on the merged treeChecklist