Skip to content

feat(zenstack): upgrade to ZenStack v3.8.0 and remove Prisma#471

Open
therealbrad wants to merge 85 commits into
mainfrom
zenstack-v3-upgrade
Open

feat(zenstack): upgrade to ZenStack v3.8.0 and remove Prisma#471
therealbrad wants to merge 85 commits into
mainfrom
zenstack-v3-upgrade

Conversation

@therealbrad

Copy link
Copy Markdown
Contributor

Description

Upgrades the data layer from ZenStack v2.22.3 (on Prisma) to ZenStack v3.8.0 (its own Kysely-based, Prisma-API-compatible ORM) and removes Prisma from the repository entirely — both functionally and by name.

Highlights:

  • Access policies enforced. The @@allow / @@deny rules (incl. the ~198 nested ?[…] rules) are enforced at runtime by the v3 PolicyPlugin — this was the historical blocker and is now working in v3.8.0. No security regression: the previously-skipped ACL E2E tests pass with policy enforcement on.
  • Prisma fully gone. No @prisma/client / prisma runtime dependency; prisma generate removed from the build; production Docker images build clean against ZenStack only. The client layer lives in lib/zenstack.ts (rawClient / baseClient / policyClient, getAuthDb / getEnhancedDb), with thin shims at the old import paths. Prisma-named identifiers were renamed to db naming throughout (mocks included); only standalone Prisma-the-tool prose in comments remains.
  • $extends hooks → v3 plugin. The ~30-model audit + Elasticsearch + webhook side-effects from the old lib/prisma.ts were ported to a v3 sideEffectsPlugin (onQuery for business logic/audit/bulk ops, onEntityMutation for post-commit ES sync and in-tx webhooks).
  • Hooks API. Generated v2 named hooks (useFindManyX) converted to the v3 grouped client (client.x.useFindMany); custom hooks re-pointed.
  • NextAuth adapter rewritten ZenStack-native (no @auth/prisma-adapter).
  • A handful of real v3 behavioral differences were fixed along the way (explicit m2m join models for case tags/issues, $transaction options, Json? null handling, error-shape helpers, raw-SQL via the policy client, nested-orderBy, 63-char alias limit).

Related Issue

Closes #61 (relates to #60, #62, #63, #64, #66)

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Refactoring (no functional changes)
  • Performance improvement

How Has This Been Tested?

  • Unit tests
  • Integration tests
  • E2E tests
  • Manual testing

Full Vitest suite green. Production-build E2E (pnpm build && E2E_PROD=on pnpm test:e2e) runs at ~1290 passing — zero regression versus the pre-upgrade baseline (the few residual failures are pre-existing parallel-load flaky/race tests, not related to this change). All previously-skipped ACL tests pass with policies enforced.

Test Configuration:

  • OS: macOS (local) / Linux self-hosted (CI)
  • Browser (if applicable): Chromium (Playwright)
  • Node version: 24

Checklist

  • My code follows the project's style guidelines
  • 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
  • Any dependent changes have been merged and published
  • I have signed the CLA

Additional Notes

Intended to ship first as a beta (v1.0.0-beta.0 image tag) so it can be validated without disturbing the stable :latest. The release pipeline already supports this: a prerelease tag does not match the :latest semver guard in release.yml, so the beta images publish under their own tag only. Merging this PR to main is what would flip :latest to v3 — so the beta tag is pushed independently while this PR stays open for review.

Note on versioning at merge time: to cut a 1.0.0 stable release, the squash-merge title needs to signal a breaking change (e.g. feat(zenstack)!: … or a BREAKING CHANGE: footer); a plain feat: would bump a minor.

- Swap Prisma/ZenStack v2 deps for v3 (@zenstackhq/orm, plugin-policy,
  schema, server, tanstack-query, cli @3.8.0; add kysely + pg); remove
  @prisma/client, prisma, @zenstackhq/runtime, openapi, zenstack, and the
  unused @next-auth/prisma-adapter.
- Scripts: prisma db push -> zenstack db push; drop v2 fix-zenstack-symlink;
  generate into ./zenstack with explicit --schema.
- schema.zmodel: migrate to v3 via re-runnable codemod
  (scripts/migrations/zmodel-v2-to-v3.mjs): drop prisma generator + v2
  hooks/zod/openapi plugins, add policy plugin; !auth() -> auth() == null
  (x146); remove @password; future() -> post-update/before(); exclude
  create from non-owned relation rules; drop field-level policy override arg.
- Gitignore the generated /zenstack client (produced by postinstall).
- lib/zenstack.ts: ZenStackClient over a Kysely/pg pool with three layered
  views — rawClient (no plugins, @omit-readable), baseClient (+ side-effects),
  policyClient (+ @@allow/@@deny). getAuthDb(user) = policyClient.$setAuth(user).
- lib/zenstack-plugins/sideEffectsPlugin.ts: no-op plugin seam; the v2
  lib/prisma.ts $extends audit/ES/webhook/business-logic hooks get ported here
  in a follow-up (original preserved at the previous commit).
- lib/prisma.ts, lib/prismaBase.ts, server/db.ts: thin re-export shims
  (prisma->baseClient, db->policyClient, prismaBase/server.db->rawClient) so the
  ~590 existing call sites resolve unchanged.
- lib/auth/utils.ts: enhance(prisma,{user}) -> getAuthDb(user).
- next.config.ts: serverExternalPackages -> v3 (orm, plugin-policy, kysely, pg).

Verified live: layering + policy enforcement intact (anon=0, admin=full,
non-admin filtered).
…ts to v3

- Remove the generated lib/hooks/ (121 files): v3 TanStack Query hooks are
  runtime (useClientQueries), not generated — consumers convert in a follow-up.
- Codemod scripts/migrations/imports-prisma-models.mjs (idempotent): rewrite
  @prisma/client model-type and enum imports to ~/zenstack/models across 346
  files. Enums import as values; model types as 'import type'. Prisma /
  PrismaClient left as residual @prisma/client imports for the namespace pass.
- lib/zenstack.ts: add DbClient (= raw client type) and TxClient
  (= TransactionClientContract) aliases for the v2 PrismaClient /
  Prisma.TransactionClient types.
- Codemod scripts/migrations/imports-prisma-namespace.mjs (idempotent) rewrote
  123 files: Prisma.<Model>{WhereInput,GetPayload,Select,...} -> ~/zenstack/input;
  Prisma.TransactionClient -> TxClient; Prisma.JsonNull/DbNull/AnyNull and
  JsonValue/JsonObject/JsonArray (+ InputJsonValue->JsonValue) -> @zenstackhq/orm;
  Prisma.SortOrder.asc/desc -> string literals; Prisma.Decimal -> decimal.js;
  Prisma.PrismaClientKnownRequestError -> ORMError.
- add decimal.js direct dep.

73 files remain on @prisma/client for manual completion (flagged by the codemod):
new PrismaClient() in e2e/seed/test scripts (50), <Model>OrderByWithRelationInput,
plain Create/UpdateInput, raw SQL (Prisma.sql/empty), NullableJsonNullValueInput.
- Add lib/rawDbClient.ts: createRawDbClient() returns an independent raw
  ZenStackClient (own pool, no policy/side-effects) for seeds, scripts, and e2e
  tests that managed their own client lifecycle.
- Codemod scripts/migrations/new-prismaclient.mjs: replace bare new PrismaClient()
  -> createRawDbClient() and drop the PrismaClient import across 49 files
  (e2e specs, seed scripts, integration tests). Verified the factory connects
  and exposes $disconnect.

Remaining @prisma/client importers: 25 (OrderBy types, plain Create/UpdateInput,
raw SQL composition, NullableJson, PrismaClient param types, multiTenant factory).
- Workflow-migrated the final 25 residual files (Prisma.* arg types ->
  ~/zenstack/input, raw SQL -> kysely sql via $qb, multi-tenant factory ->
  ZenStackClient, OrderBy -> FindManyArgs['orderBy'], plain Create/UpdateInput
  -> XArgs['data']); each fix independently verified.
- Fix searchQueryBuilder, EditResultModal, TestResultHistory, reviews/columns:
  non-standard @prisma/client forms (runtime/library JsonValue, inline import()
  type, dynamic enum import) -> @zenstackhq/orm / ~/zenstack/models.
- reviewDecisions: correct the ORMError construction to
  new ORMError(ORMErrorReason.NOT_FOUND, ...) (was a broken {code:P2025} cast).
- Delete dead lib/prisma-middleware.ts (+ test): superseded by client
  extensions, only self-referenced, and used a dynamic @prisma/client import.

App code is now fully off @prisma/client. Remaining: 21 test files (vi.mock /
dynamic-import of @prisma/client) for Phase 7; v3 error-shape rework
(lib/utils/errors.ts: ORMError has no .code) for Phase 6.
Resolved conflicts: keep v3 lib/prisma.ts shim (CDC's slimmed $extends becomes
the Phase 3 plugin port); keep lib/hooks deleted (v3 runtime hooks); take main's
logic for the RPC route + 8 routes/components/workers (codemods re-applied next);
package.json = v3 deps + main's CDC scripts (apply-triggers, dcl-retention) + v0.40.6.
Post-merge: re-run import codemods to migrate main's new CDC files + reverted files.
Re-ran the idempotent import codemods over main's CDC additions + the
conflict-reverted files, plus hand-fixes:
- lib/audit/{auditedTransaction,gucContext}: Prisma.TransactionIsolationLevel
  -> TransactionIsolationLevel (@zenstackhq/orm).
- app/actions/searchProjectAuditLogUsers: raw Prisma.sql -> kysely sql via $qb.
- workers/testmoImportWorker: Prisma.*CreateManyInput -> *UncheckedCreateInput,
  Prisma.TestmoImportJobUpdateInput -> TestmoImportJobUpdateArgs['data'].
- delete leftover generated lib/hooks/data-change-log.ts (v3 hooks are runtime;
  DataChangeLog gets a v3 runtime hook).
- apply-triggers.ts already uses pg Client directly (ORM-agnostic, untouched).

App code fully off @prisma/client again. Verified live: client layer + policy
enforcement intact, DataChangeLog queryable via v3 client.
- Codemod scripts/migrations/hooks-v2-to-v3.mjs: convert v2 generated hooks
  (useFindManyProjects, useCreateTags, ...) imported from ~/lib/hooks to the v3
  runtime grouped client, inline:
    useFindManyProjects(args, opts)
      -> useClientQueries(schema).projects.useFindMany(args, opts)
  Inline preserves v3 per-call generic inference (select/include narrowing) and
  avoids placing a per-component client const. 278 files; longest-suffix model
  matching disambiguates (TestRuns vs TestRunResults). 0 unparsed imports.
- app/providers.tsx: repoint the ZenStack hooks provider from the v2
  runtime-v5/react Provider to v3 QuerySettingsProvider (@zenstackhq/tanstack-query
  /react), preserving endpoint=/api/model + the operationIdFetcher (X-Operation-Id
  header for CDC audit correlation).

Follow-up (Phase 6/7): 13 files manually invalidateQueries({queryKey:[...]}) with
v2-style keys that may differ under v3's grouped client.
- RPC route app/api/model/[...path]/route.ts: NextRequestHandler now takes
  getClient + a required apiHandler (new RPCApiHandler({ schema })); getPrisma
  returns the $setAuth'd client (deadlock-retry preserved).
- enhanceWithAudit -> thin alias for getAuthDb (policyClient.$setAuth); covers
  the RPC route + copy-move/import/shared-steps bulk routes unchanged. GUC actor
  injection moves to the Phase 3 side-effects plugin.
- auditedTransaction: auditedEnhancedTransaction inverts to
  getAuthDb(user).$transaction(...) since v3 can't enhance() a tx client.
- Codemod scripts/migrations/enhance-to-setauth.mjs: enhance(client,{user:U}) ->
  getAuthDb(U) across 9 entry points (stream/step-scan/export/preflight/metadata).
- change-password: read User.password with omit:{password:false} (v3 omits @omit
  fields on all clients; other auth/2FA/share-link reads already use explicit select).
- NextAuth adapter already takes DbClient + PrismaAdapter (v3 is Prisma-compatible).
- Fix codemod import-injection bug: hooks-v2-to-v3 + new-prismaclient injected
  imports after the FIRST LINE of a multi-line import, splitting it. Now prepend
  before the first import. Re-ran hooks codemod (278 files) cleanly; repaired the
  split in reindexElasticsearch; hooks codemod now skips test files.
- Reset 12 test files the hooks codemod broke (it rewrote v2 hook names used as
  vi.mock object keys) — test mock rework is Phase 7.
- hooks/useRepositoryCasesWithFilteredFields: type positions use ClientHooks
  indexing (typeof <call-expr> is invalid).
- lib/utils/errors.ts: rework for v3 ORMError (no .code) — dbErrorCode SQLSTATE
  (23505/23503) + reason NOT_FOUND, plus text-match for client {info:{message}};
  isAlreadyPendingError matches the partial-index text.
- 4 SCIM routes: Prisma.PrismaClientKnownRequestError check -> isUniqueConstraintError.
- 6 seed/scripts: restore the lost createRawDbClient import (also fixes the
  non-module 'prisma' redeclare).
…cimal, @omit

- lib/zenstack.ts: TxClient = Omit<typeof rawClient, tx-unsupported> instead of
  TransactionClientContract<typeof schema> — the latter widened tx query results
  to 'any', causing 73 implicit-any (TS7006) errors across transaction routes.
  This single fix cleared all 73.
- RPC route: type the handler result Response|null (baseHandler returns Response,
  not NextResponse) which cleared the 18 'response possibly null' errors;
  isolationLevel '"Serializable"' -> TransactionIsolationLevel.Serializable.
- lib/llm/services/llm-manager: wrap cost numbers in new Decimal() (v3 Decimal
  fields reject plain number on input).
- app/api/share/[shareKey]: read passwordHash via omit:{passwordHash:false}.

Non-test type errors: 198 -> ~73 real (+18 known next-intl t() local-tsc
false-positives that clear on a Next build).
…y allows isolationLevel)

v3 $transaction options accept only { isolationLevel } (no Prisma maxWait/timeout)
— passing them broke overload resolution (TS2769). Stripped the option objects
across testmo imports (automationImports/issueImports/configurationImports/
testmoImportWorker) and the hardcoded worker/service calls; auditedTransaction
now forwards only isolationLevel. Cleared 17 of 19 TS2769.

Non-test type errors: ~54 real remaining (+18 known i18n false-positives).
…orCode

- LLM integration cost fields -> new Decimal() (Add/Edit/llm-manager)
- JSON fields with possibly-undefined values cast as JsonValue (credentials,
  Issue.data in jira/create + create-issue trackerFields)
- .code===Pxxxx checks -> isUniqueConstraintError/isNotFoundError/dbErrorCode
  (admin/tags/create, reviews/decide, webhooks/coalescing)
…t, share audit

- transformSteps accepts null; AddResultModal notes/evidence, SessionResultForm
  resultData, issueImports settings, seed AppConfig value, webhooks-seed payload,
  test-credentials settings -> JsonValue casts
- verifyEmail emailVerified -> Date (not toISOString string)
- removed dead lazy @zenstackhq/runtime enhance() in SyncService
- getScimTokenWithSecret() opts @omit secret back in for probe decryption
- share-link creation audit takes hasPassword bool (hash no longer sent to client)
- webhook-config JSON path filter -> string (v3 shape)
…Table/TestRunDisplay props

- AddCase/[caseId] sharedStepGroup useFindMany: let v3 infer (drop manual type)
- admin/users + runs/page: cast v3 query results to ExtendedUser[]/TestRunsWithDetails[]
…rt, TS2589, find callbacks

- filterOrphanedFieldValues unconstrained generic (v3 result types too complex
  for the old constraint); loosen post-fetch filter param
- sso nested samlConfig upsert: v3 requires where -> { providerId }
- EditResultModal/[caseId] find(): drop stale explicit element annotations
- TS2589 deep-instantiation on testmoImportJob/user .update(): args cast
  (data already typed at declaration)

Real (non-test) type-check is now GREEN. Remaining 21 are next-intl t() local-tsc
false-positives (clear on a Next build).
Replaces the no-op stub with the full write-side-effects port from main's
lib/prisma.ts $extends, adapted to the v3 plugin API:

- onQuery: arg-rewriting business logic (TestRuns auto-completedAt)
- beforeEntityMutation: app.audit_context GUC SET LOCAL inside the mutation's
  transaction so CDC triggers attribute the actor (skipped when already inside
  an auditedTransaction); before-image capture for update/delete diffs
- afterEntityMutation (runAfterMutationWithinTransaction: true): Elasticsearch
  sync (fire-and-forget), in-tx webhook emits (case/run/session/issue/JUnit/
  testRunResult/sessionResult), the draft-case run-exclusion business logic,
  and the remaining semantic audit (SSO/system-config) + API-token cache evict

The CDC refactor emptied AUDITED_CONFIG_MODELS and SEMANTIC_ACCESS_AUDIT_MODELS
(triggers are sole source for row audit), so the generic config/access audit
hooks are intentionally not ported. Type-check + production build green.

Runtime parity (GUC attribution, webhook atomicity, bulk handling) is validated
in the Phase 7 E2E pass.
v3 grouped data hooks call React context internally and crash component renders
without the app providers. A default inert stub (per-file mocks still override)
clears 359 unit failures (709->350) with no regressions.
…udit

Routes use getAuthDb(user) (or enhanceWithAudit alias) instead of
enhance(prisma,{user}). Updated 9 test files: 6 getAuthDb route tests +
audit-log export + import/route (enhanceWithAudit) + auth/utils (getEnhancedDb
delegates to getAuthDb; getUserWithRole reads baseClient).
…eClientQueries

Replace vi.mock("~/lib/hooks") + imports with vi.hoisted mock fns wired into a
per-file useClientQueries mock (component reads via the v3 grouped client).
…ntQueries

Hoist the two driven read hooks; inline inert stubs for the other 7 model ops
the component calls. Proves the multi-hook component-test pattern.
… merge)

Codemod now matches lib/hooks and lib/hooks/<subpath> factories together and
merges all into one useClientQueries mock per file (avoids the double-mock that
silently dropped hooks). Applied to 68 test files.

Unit failures 709 -> 60.
…a codes)

v3 has no Prisma and no P-codes. Construct ORMError directly with the Postgres
SQLSTATE in dbErrorCode (23505 unique / 23503 FK) and the NOT_FOUND reason,
matching what the helpers actually detect; drop the fake makePrismaError.
…aw-SQL

The action issues its filtered query via Kysely sql``.execute(prisma.$qb); mock
$qb.getExecutor() with passthrough transform/compile (to inspect the raw node)
and executeQuery returning { rows }.
Same prisma.$qb.getExecutor() mock as scimConflictLogActions; adds a boundValues
helper that walks the Kysely raw node's ValueNodes (replacing v2 Prisma
Sql.values) to assert the project-scoped bound parameters.
…on fixtures)

- probe.test mocks getScimTokenWithSecret (the fn probe.ts now calls)
- scimTokenActions + 4 scim/v2 route tests build a real v3 ORMError unique
  violation (dbErrorCode 23505) instead of a malformed Prisma P2002 object
…ard message

- coalescing: v3 unique-violation (23505) / FK (23503) ORMErrors, not Prisma codes
- iterationEvents + events: tx-guard assertions match the v3 'requires a
  TxClient' message (was Prisma.TransactionClient/transaction)
…v3 rewrite

- matrixCellCount: $qb executor wraps rows for the two preflight COUNT queries
- enhanceWithAudit: rewritten to assert the v3 reality (thin getAuthDb alias),
  dropping the obsolete v2 enhance(prismaBase)-vs-hooked-client invariant
…d types

@testplanit/mcp-server was the last package using Prisma. It types its ZenStack
RPC query args (where/include/select) with schema-specific types that previously
came from the testplanit-generated Prisma client.

- Source those types from testplanit's ZenStack-generated types via a `@db/*`
  tsconfig path-alias (+ @zenstackhq/orm for JsonValue); drop @prisma/client.
- Convert all 40 source files: `Prisma.<X>` -> the ZenStack `<X>` types; derive
  the few v3 doesn't export top-level (WhereUnique / OrderByWithRelation /
  CountOutputType) as local aliases.
- Apply the RepositoryCases tags/issues -> caseTags/caseIssues join-model change
  across the case/issue tools: read includes, where filters, create/update
  writes (nested join-model create + deleteMany-inside-update, preserving the
  soft-delete invariant), and row mappers. Sessions/TestRuns stay implicit m2m.
  Public tool output shapes (tags, issues, counts) are unchanged.
- Update the 8 affected test files to assert the new shapes.

Verified: typecheck 0 errors, 653/653 tests pass, tsup build succeeds, zero
@prisma/client references.
Cosmetic follow-up to the Prisma removal — the shim files and the `prisma`
client variable kept the legacy name. Rename them to ZenStack-appropriate names
(no behavior change):

  lib/prisma.ts      -> lib/db.ts       ; export `prisma`(baseClient) -> `baseDb`
                                          (export `db` = policyClient unchanged)
  lib/prismaBase.ts  -> lib/rawDb.ts    ; export `prisma`(rawClient)  -> `rawDb`
  lib/multiTenantPrisma.ts -> lib/multiTenantDb.ts
       getTenantPrismaClient -> getTenantDbClient
       getPrismaClientForJob -> getDbClientForJob
  lib/prisma.config-audit.test.ts -> lib/config-audit.test.ts
  server/db.ts unchanged (already db-named)

All ~450 call sites updated (import paths + the `prisma` identifier -> baseDb /
rawDb, scope-aware per source). vi.mock paths + factory keys updated to match.

Verified: type-check clean of rename errors, Vitest 9724 pass, prod build OK.
Final cosmetic pass of the Prisma removal — eradicate the `prisma` name from
all remaining identifiers (test mocks, heritage type aliases, helper fns, and
local client vars). Token-level rename Prisma->Db / prisma->db, e.g.:

  mockPrisma -> mockDb, prismaMock -> dbMock, buildPrismaMock -> buildDbMock,
  fakePrisma -> fakeDb, prismaClient -> dbClient, createCustomPrismaAdapter ->
  createCustomDbAdapter, scimFilterToPrismaWhere -> scimFilterToDbWhere,
  PrismaLike -> DbLike, PrismaClientType -> DbClientType, RawPrismaClient ->
  RawDbClient, the local `const prisma = createRawDbClient()` -> `db`, etc.

Intentionally left (not identifiers): standalone `Prisma` in comment prose
(describes the ORM/historical context), the real `_prisma_migrations` Postgres
table name, the `feedback_prisma_helper_live_db_test` doc slug, and the
`PrismaClientValidationError` proper-noun comment reference.

No behavior change. Verified: type-check (no new errors), Vitest 9724 pass,
prod build OK.
scripts/fix-zenstack-symlink.js is a ZenStack v2 artifact: it resolves
@zenstackhq/runtime (absent under v3) and copies v2 runtime files (.zenstack
symlink, enhance.js, policy.js) that v3 doesn't produce — under v3 it errors
immediately and exits 0 (no-op). It was invoked 4x in the Dockerfile and was
the source of the "Cannot find module '@zenstackhq/runtime/package.json'" build
warnings. Remove the script + its Dockerfile invocations.

Verified: production + workers Docker images build clean (bake exit 0).
All three were test fragility, not app bugs — no app code changed.

- markdown-paste-and-import :105/:213: read-after-write race. saveChanges()
  returned when the Edit button reappeared (isSubmitting flips it back the
  instant Save is clicked), but the field-value POST was still in flight, so the
  API read-back sometimes beat the create. New saveChangesAndWaitForFieldValue()
  waits for the POST /api/model/caseFieldValues/{create,update} response (the
  real "persisted" signal) before reading back; removed the fixed sleeps.

- authoring-step-mentions :14: the undeclared-parameter warning ribbon only
  fires on a parameterMention NODE, but @-autocomplete only offers DECLARED
  params, so typing "@notDeclared" + Escape left plain text that never became a
  mention. The step now pastes an undeclared parameterMention node (reproduces
  the real "parameter removed from the declared set" state the ribbon guards).

- drag-drop-modifier-aware :276 (+ 6 dependents): react-dnd's HTML5Backend only
  reacts to native HTML5 drag DOM events; the helper drove the drag with
  mouse.down/move/up, which never reach the backend, so the drop handler (and
  the cursor-anchored popover) never fired. New nativeDragDrop.ts helper
  synthesizes genuine HTML5 drag events (one threaded DataTransfer, modifier
  keys on dragover/drop, a frame flush for the rAF-scheduled hover); the
  modifier/badge tests use the hold-drag variant so intent commits before drop.

Verified green: markdown-paste 3x consecutive; drag-drop 8/8 (no retries);
all 3 specs together 12/12.
All three were pre-existing test-side timing races (no app behavior changed),
surfaced as hard failures in a full run where the retry didn't save them.

- sorting :341 (Verify Name Column Sort Order - Ascending): getColumnValues read
  the table during the post-sort refetch and caught the 10 DataTable skeleton
  rows (no data-row-id). Wait for exactly 3 real rows via an auto-retrying
  toHaveCount before reading the column (mirrors the sibling sorting tests).

- session-duplication :108: AddSessionModal seeds its name field from a one-shot
  reset() effect that fires after first render and rewrites the name back to
  "<original> - Duplicate"; the test's clear()+fill(newName) was being clobbered,
  so the session got created under the wrong name and the lookup returned null.
  Fill the name under an expect(...).toPass() retry loop (the ref-guarded reset
  fires once, so the re-fill wins) + poll the read-back.

- test-run-case-execution :366: Pass & Next only escalates to the Add Result
  modal once the templateResultAssignment findMany resolves (gated on case/
  template load); clicking before it resolved took the non-escalation path.
  Wait for that response before clicking Pass.

Verified: session-duplication 12/12 + 3/3 stress; test-run-case-execution 9/9 +
6/6 stress; sorting 2/2 — all zero retries.
The Descending sibling of the Ascending sort test had the same getColumnValues
skeleton-row race (read 10 placeholders during the post-sort refetch). Add the
same auto-retrying toHaveCount(3) on data-row-id rows before reading. Verified:
full sorting.spec green (33 passed), :341 + :390 both pass.
…tion

Deploys ran `zenstack db push --accept-data-loss` on every container boot, which
would silently drop data on any lossy schema change — the v2->v3 implicit->explicit
m2m conversion (case<->tag / case<->issue) being the immediate case (94k rows).

Switches the project to ZenStack/Prisma migrations:

- migrations/20260625193632_init: baseline = the schema as of the last db-push
  release (v2-equivalent, incl. the implicit _RepositoryCasesToTags /
  _IssueToRepositoryCases join tables).
- migrations/20260625193819_explicit_case_tag_issue_join: converts the implicit
  m2m to the explicit RepositoryCaseTag / RepositoryCaseIssue join models,
  DATA-PRESERVING (create -> copy -> drop in one transaction). The generated diff
  dropped first; it was reordered and given INSERT...SELECT copies (with the
  reversed Issue/RepositoryCases column mapping handled).

- docker-entrypoint.sh + all three docker-compose db-init services:
  `db push --accept-data-loss` -> `migrate deploy` (non-destructive).
- Dockerfile: ship the migrations/ dir so the entrypoint can apply it.
- package.json: `generate`/`generate:dev` are now codegen-only (they no longer
  db push against whatever DATABASE_URL points at); added db:migrate(:dev),
  db:deploy, db:status, and a db:push escape hatch for throwaway local use.
- migrations/README.md: the one-time baseline procedure for existing
  (pre-migration) databases — `migrate resolve --applied 20260625193632_init`
  then `migrate deploy`.

Validated end-to-end on a scratch PG15: fresh install (init+change -> v3
structure) and the existing-DB path (db-push origin -> baseline resolve ->
deploy) both preserve the seeded m2m links with the correct column mapping and
drop the implicit tables.
Brings #467-472 (audit-trigger self-heal + CLI guard, seed default-preservation,
dependency bumps incl. react-day-picker 10 / react-resizable-panels 4 /
next-intl 4.13 / @zxcvbn-ts 4) onto the ZenStack v3 branch.

Conflict resolutions kept v3 patterns while absorbing main's fixes:
- package.json / mcp-server: main's dep bumps applied; prisma + zenstack-v2 deps
  stay removed; zenstack v3 deps + migrate scripts kept.
- scripts/apply-triggers.ts: took main's exportable self-heal applier (advisory
  lock, robust path resolution, CLI guard), retargeted prisma/ -> db/.
- db/seed.ts, db/seedPromptConfig.ts: took main's admin-default-preservation
  logic on the v3 db client (prisma. -> db.).

Validated: prod build passes; source type-checks clean (remaining tsc errors are
the pre-existing v3 test-file baseline); lockfile consistent (frozen-lockfile OK).
react-resizable-panels v4 renders the group element's data-testid from its `id`
prop and ignores a passed `data-testid`. The v4-compat wrapper spread the prop
through but v4 overrode it with an auto-generated id, so every group testid
(e.g. data-testid="repository-layout") rendered as a random id — the repository
page, AddCase, and the project-overview panel group all became unqueryable,
failing their E2E specs (the merge that bumped resizable-panels to v4 surfaced
this). Map an explicit data-testid onto the v4 `id` so the rendered testid
matches. Verified: the group now renders data-testid="repository-layout".
v4 renamed the internal DOM hooks the overview/steps specs queried:
- [data-panel-group] -> [data-group]
- [data-panel-resize-handle-id] -> [role="separator"]
- [data-panel="<id>"] -> getByTestId(id) (v4 derives the element data-testid
  from the panel id; data-panel is now the boolean "true")

Fixes the overview panel-group/handle assertions and the shared-steps /
steps-display handle locators. (The separate left-panel collapse-behavior
assertion is still under review.)
…group)

Extends 31af532 to ResizablePanel. v4 derives the panel element's data-testid
from its `id` and ignores a passed `data-testid`, so panels that set both an
`id` (identity) and a different `data-testid` (test hook) — e.g. the repository
left panel: id="repository-left" + data-testid="repository-left-panel" —
rendered the wrong testid, breaking the folder-tree specs (folder-creation,
tree-navigation, view-switching, folder-delete/edit) that query the panel. Prefer
data-testid for the v4 id (the rendered testid is what callers query; the id is
internal layout state). Verified at render level: group, both panel forms resolve
the expected testids.
react-resizable-panels v4 renders each panel with overflow:visible (v3 clipped),
so a panel collapsed to width 0 still showed its content — the min-content width
of flex children spills out of the 0-width panel. Restore v3 clipping via a
globals.css rule ([data-panel]{overflow:hidden!important}; the library ignores an
inline style override). The resize handle sits outside the panel and menus use
portals, so only in-panel content is affected (in-panel scrolling stays on the
inner wrapper).

Also harden the overview collapse test: a clipped element keeps its layout
bounding box, so Playwright's toBeHidden on the content text is unreliable;
assert the collapse structurally via the panel width (0 collapsed, >0 expanded).

Verified: project-overview-dashboard.spec.ts 10/10 green.
Fixes CI 'Check formatting' (pnpm format:check). The v3 migration's bulk codemods
+ the merged prettier-plugin-tailwindcss 0.8.0 bump left ~367 files unformatted
(local precommit was run scoped because full eslint OOMs locally). Pure prettier
--write output — line-wrapping to print width + Tailwind class re-sorting, no
logic changes.
CI 'Run linter' (eslint . && tsc --noEmit) failed on 15 @typescript-eslint/no-unused-vars
errors — leftovers from the v3 codemod and the merge's $transaction-options removal:
unused db / ORMError / TxClient imports, the dead SharedStepGroupWithItems +
SharedStepItemDetail (+ now-unused JsonValue) interfaces, and dead TRANSACTION_*
timeout consts (v3 $transaction only takes isolationLevel). Removed them.
CI 'Run linter' also runs tsc --noEmit, which had 37 errors — v3-migration type
debt in test files (masked because vitest uses esbuild, not tsc) plus one source
file. All fixed with minimal, behavior-preserving changes:
- milestoneActions/matrixCellCount tests: v3's overloaded $transaction/$qb mocks
  weren't callable under the inferred type — annotated/cast the mock callbacks.
- server/auth-adapter.ts: TS2589 deep-instantiation on db.user.update — cast the
  call target to break inference depth (external Promise<AdapterUser> preserved).
- ORMError .code -> .dberrorCode/'23505' (v3 ORMError has no .code; matches lib/utils/errors.ts).
- scim tokens test: omit:{secret:false} to opt the @omit secret back in (mirrors source).
- scim groups/users + share-links + data-model-foundation: cast raw-JSON WhereInput,
  fix stale hasPassword fixture, enhance() -> getAuthDb (v3 equivalent).

pnpm type-check exits 0; eslint clean on changed files; affected vitest files pass.
prettier format:check fix for iterationGenerationWorker.ts (the unused
TRANSACTION_* const removal in 8f5acce left a double blank line).
…589 cast)

The E2 source-shape sanity test counted /\.user\.update\(/ which no longer matched
updateUser after it cast the method to break a TS2589 deep-instantiation
((db.user.update as ...)(...)). Match the identifier db.user.update\b instead —
still exactly two, still excludes updateMany, intent (catch an accidental 3rd
user mutation) preserved.
…dle crash

The rich-text editor configured ImageWithResize as inline:true while the custom
Image extension sets group:"block" and all stored content places images at block
level. That contradiction (inline node at block level) was tolerated by @tiptap
3.26 but @tiptap 3.27 (bumped in the main merge) validates content strictly and
threw 'Called contentMatchAt on a node with invalid content', crashing the
always-mounted DragHandle (ContentItemMenu) on ANY doc containing an image —
shared steps, documentation, rich case fields. Found via the tpiv1 prod-data
smoke test (shared-steps group with an image); the E2E seed has no block images
so it slipped through.

Set inline:false so block images validate. Adds blockImageSchema.test.ts (getSchema
+ Node.check) reproducing the crash and guarding against reintroducing inline:true.
Reflects the v3 dependency shift: Prisma removed (ORM now built on Kysely +
node-postgres), added Kysely / node-postgres (pg) / decimal.js notices, dropped
the removed Atlaskit editor-jira-transformer entry, and corrected license fields.
Production deploys (*.testplanit.com) run exclusively on arm64, so building
linux/amd64 images was wasted CPU. Removes the build-amd64 and docker-manual-amd64
jobs and the now-unnecessary multi-arch manifest-merge jobs; the arm64 build now
publishes the final tags directly (${version}, ${version}-workers,
workers-sha-${sha}) and moves :latest/:latest-workers only when the tag is the
newest semver. Smoke-test and BASE_DOMAIN wildcard behavior are unchanged.
Clicking the Reports tab after running a custom report bounced back to Report
Builder (URL flipped to tab=builder&reportType=test-execution with a stray
?snapshotId). Root cause: the Reports tab's first pre-built report is
automation-candidates, so the click mounts AutomationCandidatesReportPreset,
whose 'default to newest snapshot' effect ran on mount and called
router.replace(new URLSearchParams(searchParams) + snapshotId). That searchParams
snapshot is stale during the in-flight tab navigation, so the replace overwrote
the tab/reportType change — a classic multi-writer URL race.

Make the automatic default update local state only; never write the URL from the
auto-default. Explicit snapshot selection from the dropdown still goes through
setSelectedSnapshotId and writes ?snapshotId (an un-pinned auto-default resolves
to 'latest', which is the newest snapshot it selects anyway), so share fidelity
is unchanged.

Pre-existing on main; surfaced by the v3 prod-data smoke test.
Running a custom report then clicking the Reports tab bounced straight back to
Report Builder. Reproduced live and traced to the report-type Select: while the
tab switches, the Select momentarily emits onValueChange("") (its current value
is briefly not among the freshly-rendered options), and handleReportTypeChange
coerced that empty value to the "test-execution" fallback — reverting the tab,
reportType, and URL back to the builder. Compounding it, the auto-run effect
re-ran the previous custom report and runReport rewrote the URL, and the tab/
reportType-from-URL sync effects reverted optimistic state off the stale URL.

Fixes:
- handleReportTypeChange ignores empty / unknown report types instead of falling
  back to a default (the actual bounce trigger).
- runReport gains a persistUrl option; only an explicit Run persists selections
  to the URL, so an auto-run can no longer rewrite (and clobber) the URL.
- handleTabChange switches reportType together with the tab (resolveTabChange) so
  the Reports tab shows a valid pre-built report, with pendingTab/pendingReportType
  guards so the URL-sync effects don't revert the switch mid-navigation.

Pure helpers (resolveTabChange / resolveSyncedActiveTab / resolveSyncedReportType)
are unit-tested. Verified end-to-end in a browser: Builder->Reports stays, the
round-trip is stable, and the report dropdown still selects correctly. Pre-existing
on main; surfaced by the v3 prod-data smoke test.
// its URL write would clobber the new tab/reportType (the bounce). The
// selections are already in the URL from the explicit run, so auto-runs
// don't need to rewrite them.
if (updateUrl && !currentReport?.isPreBuilt) {
Adds a Playwright E2E that reproduces the reported flow — build + run a custom
report, click the Reports tab — and asserts the Reports tab stays active (URL
tab=reports, reportType no longer test-execution) instead of bouncing back to
Report Builder, plus a stable Reports<->Builder round-trip. Adds data-testid
'reports-tab' / 'report-builder-tab' to the tab triggers for robust selectors.
stream-json v3 restructured the package: the old capital-A "stream-json/Assembler"
entry no longer exists and fails to resolve with MODULE_NOT_FOUND at runtime on
case-sensitive filesystems (Linux/Docker) — breaking the testmo-import-worker. It
stayed latent on macOS (case-insensitive FS). Point the import at the new
"stream-json/assembler.js" subpath and update the ambient declaration to match.

Also deletes the dead TestmoExportAnalyzer.original.ts backup (nothing imported
it; a leftover from the initial public release) which still carried the broken
import — so the legacy capital-A type declaration is no longer needed.
… image

The build stage's 'next build' could not resolve ~/zenstack/models when building
the image from a fresh clone: testplanit/zenstack/ is gitignored (generated), so
it isn't in the build context, and the base stage's 'pnpm zenstack generate' was
missing '--schema schema.zmodel -o zenstack' — so it wrote the v3 client to the
default output dir instead of testplanit/zenstack. Install runs with
--ignore-scripts, so the postinstall generate never fires either. Match
package.json's generate command so the client lands at ~/zenstack/*.
…ettier 3.9

Adopt the previously-deferred majors except ESLint 10, plus minor bumps. Two
adjustments keep the suite/precommit green:

- Revert eslint + @eslint/js to ^9.39.4: eslint-plugin-react@7.37.5 (latest)
  peers eslint up to ^9.7 and crashes on ESLint 10's removed
  context.getFilename() during React-version auto-detection. ESLint 10 stays
  deferred until eslint-plugin-react supports it.
- vitest.config.mts: set SKIP_ENV_VALIDATION for the test env. Under TS 6 the
  transform no longer elides a transitive env.js import, so @t3-oss/env validated
  DATABASE_URL/NEXTAUTH_URL at collection and 3 (skipped) suites failed to load.
  Unit tests must not require real runtime env.

TypeScript 6 type-check clean (0 errors); unit suite 9742 pass / 0 fail.
Prettier 3.8 -> 3.9 adjusts whitespace/line-wrapping (e.g. collapses short union
types onto a single line). Mechanical reformat only; no behavior change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Update ZenStack packages to v3

1 participant