TML-2690: make rollback edges plannable and applyable via --to#635
Conversation
Signed-off-by: Will Madden <madden@prisma.io>
Signed-off-by: Will Madden <madden@prisma.io>
…plans `migration plan` now accepts an optional `--to <contract>` that targets an arbitrary resolved contract (e.g. an ancestor / rollback target) instead of always the emitted `contract.json`. It accepts the same reference grammar as `--from` (full hash, prefix, ref name, migration dir name, `<dir>^`, `./path`) resolved via `parseContractRef`. When supplied, the resolved contract becomes the planner destination and the source of the emitted `end-contract.*`, and the no-op short-circuit runs against the resolved hash; when omitted, behaviour is byte-identical to today. A rollback that drops an added model plans successfully with a destructive warning rather than refusing, since `migration plan` allows the destructive op class. The reference->contract resolution core of `resolveFromForPlan` is extracted into a shared `resolveContractRef` helper reused by both `--from` and the new `resolveToForPlan`; the greenfield / auto-baseline branches stay `--from`-only. Refs: TML-2690 Signed-off-by: Will Madden <madden@prisma.io>
…en-apply story The `migrate --to` path-unreachable diagnostic previously had `why` and `fix` each independently telling the user to run `migration plan`, which read redundantly. Rewrite the two halves so they compose into one sequence: - `buildPathNotFoundFailure.why` now states only the condition — no migration edge connects the current state to the target, and migrate replays existing edges rather than inventing one — without prescribing a command. - `errorPathUnreachable.fix` owns the recovery: plan the missing edge with `migration plan --from <current> --to <target> --name <slug>` (a real command as of the new `--to` support), then re-run `migrate --to <target>`. It adds a note that a rollback plan is expected to contain destructive (DROP) ops to review, and acknowledges that narrower cases (rename inference, NOT NULL re-add without a safe default, type change needing data) may need a hint. buildPathNotFoundFailure is exported (@internal) so a test can drive the real producer and assert the why+fix compose non-redundantly. Refs: TML-2690 Signed-off-by: Will Madden <madden@prisma.io>
…item (TML-2690) Signed-off-by: Will Madden <madden@prisma.io>
Signed-off-by: Will Madden <madden@prisma.io>
…rification (TML-2690) Signed-off-by: Will Madden <madden@prisma.io>
`migrate --to <node>` previously always verified the applied path against the emitted `contract.json`, so a rollback / arbitrary-target migrate dead-ended with DESTINATION_CONTRACT_MISMATCH (plan destination != emitted contract) unless the target contract had been re-emitted first. When `--to` resolves to an on-disk graph node, apply against THAT bundle`s `end-contract.json` instead of the emitted contract. The read is lifted to just before the apply (after the marker / invariant pre-checks) and reused for the optional ref-advancement snapshot, so the apply contract and the snapshot contract are sourced identically — no duplicated, divergent reads. A missing `end-contract.json` surfaces a structured file-not-found error rather than throwing raw. With `--to` omitted (or a target with no matching bundle), behaviour is byte-identical: the emitted contract stays the apply contract. migrate still refuses to invent a missing edge; this only redirects which contract is verified against. This closes the rollback round-trip end-to-end: from a two-migration applied state, `migration plan --to <dir>^` then `migrate --to <dir>^` succeeds and moves the marker back, with no contract-source edit. Refs: TML-2690 Signed-off-by: Will Madden <madden@prisma.io>
…L-2690) Make buildNeverPlannedFailure.why condition-only so it composes with errorPathUnreachable fix without redundant migration plan instructions. Document migration plan --to and the rollback plan-then-apply workflow in the migration subsystem doc and CLI README. Signed-off-by: wmadden-electric <286902546+wmadden-electric@users.noreply.github.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds ChangesMigration System
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
@prisma-next/extension-author-tools
@prisma-next/mongo-runtime
@prisma-next/family-mongo
@prisma-next/sql-runtime
@prisma-next/family-sql
@prisma-next/extension-arktype-json
@prisma-next/extension-cipherstash
@prisma-next/middleware-cache
@prisma-next/mongo
@prisma-next/extension-paradedb
@prisma-next/extension-pgvector
@prisma-next/extension-postgis
@prisma-next/postgres
@prisma-next/sql-orm-client
@prisma-next/sqlite
@prisma-next/target-mongo
@prisma-next/adapter-mongo
@prisma-next/driver-mongo
@prisma-next/contract
@prisma-next/utils
@prisma-next/config
@prisma-next/errors
@prisma-next/framework-components
@prisma-next/operations
@prisma-next/ts-render
@prisma-next/contract-authoring
@prisma-next/ids
@prisma-next/psl-parser
@prisma-next/psl-printer
@prisma-next/cli
@prisma-next/cli-telemetry
@prisma-next/emitter
@prisma-next/migration-tools
prisma-next
@prisma-next/vite-plugin-contract-emit
@prisma-next/mongo-codec
@prisma-next/mongo-contract
@prisma-next/mongo-value
@prisma-next/mongo-contract-psl
@prisma-next/mongo-contract-ts
@prisma-next/mongo-emitter
@prisma-next/mongo-schema-ir
@prisma-next/mongo-query-ast
@prisma-next/mongo-orm
@prisma-next/mongo-query-builder
@prisma-next/mongo-lowering
@prisma-next/mongo-wire
@prisma-next/sql-contract
@prisma-next/sql-errors
@prisma-next/sql-operations
@prisma-next/sql-schema-ir
@prisma-next/sql-contract-psl
@prisma-next/sql-contract-ts
@prisma-next/sql-contract-emitter
@prisma-next/sql-lane-query-builder
@prisma-next/sql-relational-core
@prisma-next/sql-builder
@prisma-next/target-postgres
@prisma-next/target-sqlite
@prisma-next/adapter-postgres
@prisma-next/adapter-sqlite
@prisma-next/driver-postgres
@prisma-next/driver-sqlite
commit: |
size-limit report 📦
|
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/1-framework/3-tooling/cli/README.md`:
- Line 1046: The statement "After each migration, the runner verifies the schema
against the target contract and updates the marker/ledger" is too broad; change
it to explicitly scope verification to the specific migration target/family that
was applied (e.g., "After each migration, the runner verifies the schema for the
applied target/family against its target contract and updates that target's
marker/ledger"). Update the README text to use the phrase "applied
target/family" or similar to clarify verification is per-target rather than
global.
In `@packages/1-framework/3-tooling/cli/src/commands/migrate.ts`:
- Around line 293-318: The JSON.parse(endContractRaw) call can throw for
malformed end-contract.json and currently escapes the deserializeContract catch,
causing an unexpected error; wrap the parse in the same validation error path:
try to JSON.parse(endContractRaw) and on any parse error return the structured
notOk(errorContractValidationFailed(...)) referencing endContractPath (mirroring
the deserializeContract catch), then proceed to call
familyInstance.deserializeContract(parsed) as before; ensure both parse and
deserialize failures produce the same validation error that includes the path
and the original error message.
In `@packages/1-framework/3-tooling/cli/src/commands/migration-plan.ts`:
- Around line 83-87: The error handler currently passes migrationDir to
errorFileNotFound, which points the CLI error at the bundle directory instead of
the actual missing snapshot file; change the call so it passes the concrete
artifact path that failed to be read (the path variable used for the sibling
read — e.g. the end-contract.json or end-contract.d.ts path such as
endContractJsonPath or endContractDtsPath) into errorFileNotFound, preserving
the same why/fix metadata.
In `@packages/1-framework/3-tooling/cli/src/utils/cli-errors.ts`:
- Around line 270-281: The planCommand builder currently treats meta.fromHash
set to the placeholder string '<empty>' as a real fromHash, producing an invalid
CLI snippet; update the guard in the planCommand IIFE to treat the placeholder
as absent by checking that fromHash is not null AND not equal to '<empty>' (use
the same check wherever fromHash is read), so branches use fromHash only when
fromHash !== null && fromHash !== '<empty>' (keep targetHash checks unchanged).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: 19269fb5-2c8c-42f4-ba6e-913d9890eebc
⛔ Files ignored due to path filters (1)
projects/migrate-to-rollback-plannable/spec.mdis excluded by!projects/**
📒 Files selected for processing (12)
docs/architecture docs/subsystems/7. Migration System.mdpackages/1-framework/3-tooling/cli/README.mdpackages/1-framework/3-tooling/cli/src/commands/migrate.tspackages/1-framework/3-tooling/cli/src/commands/migration-plan.tspackages/1-framework/3-tooling/cli/src/control-api/operations/migration-apply.tspackages/1-framework/3-tooling/cli/src/utils/cli-errors.tspackages/1-framework/3-tooling/cli/src/utils/plan-resolution.tspackages/1-framework/3-tooling/cli/test/cli-errors.test.tspackages/1-framework/3-tooling/cli/test/commands/migrate-to-contract.test.tspackages/1-framework/3-tooling/cli/test/commands/migration-plan-command.test.tspackages/1-framework/3-tooling/cli/test/utils/plan-resolution.test.tstest/integration/test/cli-journeys/plan-to-rollback.e2e.test.ts
…L-2690) Move target end-contract JSON.parse into the deserialize try/catch so corrupt snapshots surface PN-CLI-4003 with the file path. Name the missing sibling on ENOENT in readBundleEndArtifacts. Treat the empty-marker sentinel as absent when composing migration plan recovery commands. Reword migrate README post- apply bullet to match universal post-check semantics. Signed-off-by: wmadden-electric <286902546+wmadden-electric@users.noreply.github.com>
…ir-advertised-in-help-but-fails-with
…ir-advertised-in-help-but-fails-with Signed-off-by: Will Madden <madden@prisma.io> # Conflicts: # packages/1-framework/3-tooling/cli/src/commands/migrate.ts
Expose lazy graph-node contract materialization on aggregate members so CLI commands can resolve ref snapshots and bundle end-contract bookends without hand-rolled disk reads. Resolution mirrors plan-resolution semantics with typed MigrationToolsError codes for D6 wiring. Signed-off-by: Will Madden <madden@prisma.io>
Route --from/--to resolution in migration plan through the tolerant aggregate member's contractAt facet instead of hand-rolled readRefs, readRefSnapshot, and bundle end-contract reads. loadContractSpaceAggregateForCli now runs before from/to resolution; post-seed buildContractSpaceAggregate remains for validated planning. Signed-off-by: Will Madden <madden@prisma.io>
Use aggregate.app.refs and contractAt for target contract resolution instead of readRefs and inline end-contract.json reads. Extract mapContractAtError to a shared util reused by plan-resolution. Signed-off-by: Will Madden <madden@prisma.io>
Add consolidation design and D5-D8 dispatch plan to slice spec after operator review on PR #635. Signed-off-by: Will Madden <madden@prisma.io>
Signed-off-by: Will Madden <madden@prisma.io>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
packages/1-framework/3-tooling/migration/src/aggregate/aggregate.ts (1)
28-30: ⚡ Quick winAvoid the bare cast in
hasErrnoCode.This helper can narrow
codestructurally and stay within the repo's casting rule.Suggested change
function hasErrnoCode(error: unknown, code: string): boolean { - return error instanceof Error && (error as { code?: string }).code === code; + return ( + error instanceof Error && + 'code' in error && + typeof error.code === 'string' && + error.code === code + ); }As per coding guidelines, "No bare
asin production code. UseblindCast<T, "Reason">orcastAs<T>from@prisma-next/utils/casts; see theno-bare-castsskill for the decision tree."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/1-framework/3-tooling/migration/src/aggregate/aggregate.ts` around lines 28 - 30, The helper hasErrnoCode currently uses a bare cast (error as { code?: string }); replace that with the repo-approved cast helpers from `@prisma-next/utils/casts` (either blindCast<{ code?: string }, "Reason"> or castAs<{ code?: string }>) so the runtime check stays the same but avoids a bare `as`; update the import to bring in blindCast or castAs and use it in hasErrnoCode to access .code safely when error instanceof Error.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/1-framework/3-tooling/cli/src/utils/plan-resolution.ts`:
- Around line 108-118: The code currently classifies results as kind: 'snapshot'
based solely on refName input; change it to classify by the actual provenance
returned from member.contractAt(hash, ...). After calling member.contractAt,
inspect the returned object's provenance/source field (e.g., at.provenance,
at.source or similar indicator present on the returned `at` object) and if it
indicates the on-disk bundle/graph-node origin return the appropriate kind
(e.g., 'bundle' or 'graph-node') and include sourceDir, otherwise return kind:
'snapshot'; also ensure resolveFromPolicy() continues to receive and use that
provenance/kind instead of inferring from the original refName.
---
Nitpick comments:
In `@packages/1-framework/3-tooling/migration/src/aggregate/aggregate.ts`:
- Around line 28-30: The helper hasErrnoCode currently uses a bare cast (error
as { code?: string }); replace that with the repo-approved cast helpers from
`@prisma-next/utils/casts` (either blindCast<{ code?: string }, "Reason"> or
castAs<{ code?: string }>) so the runtime check stays the same but avoids a bare
`as`; update the import to bring in blindCast or castAs and use it in
hasErrnoCode to access .code safely when error instanceof Error.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: b0565561-2961-48c2-a331-031731d35a9b
⛔ Files ignored due to path filters (1)
projects/migrate-to-rollback-plannable/spec.mdis excluded by!projects/**
📒 Files selected for processing (21)
docs/architecture docs/subsystems/7. Migration System.mdpackages/1-framework/3-tooling/cli/src/commands/migrate.tspackages/1-framework/3-tooling/cli/src/commands/migration-plan.tspackages/1-framework/3-tooling/cli/src/control-api/operations/migration-apply.tspackages/1-framework/3-tooling/cli/src/utils/contract-at-errors.tspackages/1-framework/3-tooling/cli/src/utils/plan-resolution.tspackages/1-framework/3-tooling/cli/test/commands/migrate-to-contract.test.tspackages/1-framework/3-tooling/cli/test/commands/migration-plan-command.test.tspackages/1-framework/3-tooling/cli/test/commands/migration-status-aggregate-spaces.test.tspackages/1-framework/3-tooling/cli/test/control-api/apply-aggregate.test.tspackages/1-framework/3-tooling/cli/test/control-api/db-verify.per-member-verifier.test.tspackages/1-framework/3-tooling/cli/test/utils/plan-resolution.test.tspackages/1-framework/3-tooling/migration/src/aggregate/aggregate.tspackages/1-framework/3-tooling/migration/src/aggregate/loader.tspackages/1-framework/3-tooling/migration/src/aggregate/types.tspackages/1-framework/3-tooling/migration/src/errors.tspackages/1-framework/3-tooling/migration/src/exports/aggregate.tspackages/1-framework/3-tooling/migration/test/aggregate/check-integrity.test.tspackages/1-framework/3-tooling/migration/test/aggregate/contract-at.test.tspackages/1-framework/3-tooling/migration/test/fixtures.tspackages/3-mongo-target/1-mongo-target/src/core/control-target.ts
✅ Files skipped from review due to trivial changes (1)
- docs/architecture docs/subsystems/7. Migration System.md
🚧 Files skipped from review as they are similar to previous changes (17)
- packages/1-framework/3-tooling/cli/test/commands/migration-status-aggregate-spaces.test.ts
- packages/1-framework/3-tooling/migration/test/aggregate/check-integrity.test.ts
- packages/1-framework/3-tooling/cli/test/control-api/db-verify.per-member-verifier.test.ts
- packages/1-framework/3-tooling/migration/src/exports/aggregate.ts
- packages/1-framework/3-tooling/cli/test/commands/migrate-to-contract.test.ts
- packages/1-framework/3-tooling/cli/test/control-api/apply-aggregate.test.ts
- packages/1-framework/3-tooling/migration/test/fixtures.ts
- packages/1-framework/3-tooling/migration/src/aggregate/types.ts
- packages/1-framework/3-tooling/migration/src/aggregate/loader.ts
- packages/1-framework/3-tooling/migration/src/errors.ts
- packages/1-framework/3-tooling/cli/src/control-api/operations/migration-apply.ts
- packages/1-framework/3-tooling/cli/src/utils/contract-at-errors.ts
- packages/1-framework/3-tooling/cli/src/commands/migrate.ts
- packages/1-framework/3-tooling/cli/test/commands/migration-plan-command.test.ts
- packages/1-framework/3-tooling/migration/test/aggregate/contract-at.test.ts
- packages/1-framework/3-tooling/cli/src/commands/migration-plan.ts
- packages/1-framework/3-tooling/cli/test/utils/plan-resolution.test.ts
contractAt now tags results as snapshot vs graph-node (with sourceDir), so resolveContractRef classifies --from refs by actual provenance instead of assuming snapshot whenever refName is set. Signed-off-by: Will Madden <madden@prisma.io>
… union sourceDir is now statically present on the graph-node variant, removing the non-null assertion in resolveContractRef. Signed-off-by: Will Madden <madden@prisma.io>
Minimal contract envelopes must include an empty namespaces map so aggregate integrity checks can walk storage coordinates after ADR 221. Signed-off-by: Will Madden <madden@prisma.io>
Linked issue
Refs TML-2690.
At a glance
Rolling back a migration is now a two-command flow with no contract-source edit — plan the reverse edge, then apply it:
Before this PR,
migrate --to <dir>^was advertised in--helpbut dead-ended: it refused with apathUnreachableerror whose advice pointed at amigration plancommand that couldn't target an arbitrary state — and even once you hand-edited your contract to plan the edge,migrate --torefused a second time withDESTINATION_CONTRACT_MISMATCH.Decision
This PR makes a rollback (or any arbitrary-target migration) plannable and applyable as a normal two-command flow, with no contract-source surgery. Three coordinated changes:
migration plan --to <ref>—migration planaccepts an optional--to(and the already-existing explicit--from) using the same reference grammar--fromaccepts (hash / prefix / ref name / migration dir /<dir>^/./path). When supplied, the resolved contract becomes the planner destination instead of the emittedcontract.json; omitted, behaviour is byte-identical to today.migrate --to <node>verifies against the target contract —migratepreviously always verified the applied state against the emittedcontract.json, so any target that wasn't the emitted contract (every rollback) dead-ended atDESTINATION_CONTRACT_MISMATCHeven after the edge was planned. It now verifies/applies against the target bundle'send-contract.json, mirroringdb update --to. It still refuses to invent a missing edge — it only redirects which contract it verifies against; it never auto-plans.Coherent
pathUnreachablediagnostic — themigrate --torefusal now reads as one plan-then-apply sequence:whystates the condition (no edge between the two named states), andfixgives the exactmigration plan --from … --to … --name …command followed by themigrate --to …re-run, with a note that a rollback plan is expected to contain destructive (DROP) operations to review before applying. The siblingneverPlanneddiagnostic'swhywas tightened the same way so the recovery instruction lives in one place.migratekeeps refusing to invent a path (the correct invariant, see ADR 001 — Migrations as Edges); the reverse edge just becomes a real, committable migration package on disk.Reviewer notes
plan-resolution.ts(+275/−…). This extracts the ref→contract resolution core that--fromalready used (landed with TML-2629) into a shared resolver reused by both--fromand--to. The greenfield / auto-baseline branches stay--from-only. Spot-check that the--topath and the--frompath now share one resolver and that the auto-baseline branch is untouched.migrate --tochange (Package layering #2) was a mid-flight scope expansion, operator-confirmed. The slice originally assumedmigrate --toalready applied a planned arbitrary-target edge; implementing the end-to-end reproduction proved it dead-ended atDESTINATION_CONTRACT_MISMATCH. The fix mirrors the existingdb update --toprecedent (and reuses theend-contract.jsonreadmigrate.tsalready performed for the ref-advancement snapshot, so there's a single non-divergent read). The read is placed after the marker / invariant pre-checks so synthetic fixtures without anend-contract.jsondon't error before those pre-checks fire.DROP) operations.migration plan's policy allows thedestructiveop class (unlikedb init's additive-only policy), so a clean rollback plans successfully with a "may cause data loss" warning rather than refusing. Narrower cases (rename inference, NOT-NULL re-add without a safe default) can still fail fast for a hint; the diagnostic acknowledges this rather than promising a frictionless path in every case.projects/migrate-to-rollback-plannable/spec.md. It's the tracked home for this standalone slice's design rationale; it carries no implementation and will be cleaned up at project close-out.spawnSyncthe builtdist/cli.mjsand hit a 500ms cap against a ~680ms cold start (version.test.ts,removed-verb-redirects.test.ts,no-parallel-ci-detection.test.ts);control-api/contract-emit.test.tshas a separate occasional emit-race timeout. Both pre-date this work and are unrelated to the diff.Behavior changes & evidence
migration plan --to <ref>plans toward an arbitrary resolved contract (omitting--topreserves the emitted-contract default). Impl:migration-plan.ts,plan-resolution.ts. Evidence:plan-resolution.test.ts,migration-plan-command.test.ts.migrate --to <node>applies a planned arbitrary-target / rollback edge by verifying against the target'send-contract.json. Impl:migrate.ts. Evidence:migrate-to-contract.test.ts(pins the apply-contract selection),plan-to-rollback.e2e.test.ts(full plan→apply→marker-moves-back round-trip on the PGlite journey harness, no contract-source edit).migrate --torefusal reads as one plan-then-apply sequence with no double "run migration plan". Impl:cli-errors.ts,migration-apply.ts. Evidence:cli-errors.test.ts(composition tests driving the real producers for both thepathUnreachableandneverPlannedbranches).Testing performed
pnpm typecheck—@prisma-next/cli,@prisma-next/integration-tests: pass.@prisma-next/clipackage suite — 1036 passing (76 files), incl. the newmigrate-to-contractandcli-errorscomposition tests.@prisma-next/integration-testsplan-to-rollback.e2e.test.ts— green (real PGlite journey harness).pnpm fixtures:check— green, zero contract drift (after a cleanpnpm build).pnpm lint:deps— 0 violations; biome clean on touched files.Skill update
n/a — internal CLI behaviour + diagnostics. The change is additive (
migration plan --to) plus a bug fix (migrate --tonow applies arbitrary-target edges) with no breaking change to downstream consumers; no upgrade translation is required. User-facing surface is documented in7. Migration System.mdand the@prisma-next/cliREADME.Follow-ups
spawnSynctimeout cluster (and thecontract-emitemit-race timeout) are candidates for a separate fix — bump the spawn-test timeout or move those tests to the e2e suite. Out of scope here.Alternatives considered
migrate --to. Rejected:migratereplays existing edges and must never invent one — auto-planning a destructiveDROPinside an apply command is a footgun, and it would blur the plan/apply boundary. The plan step stays explicit and committable;migrate --toonly redirects which contract it verifies against.migrate --toverification change to a follow-up. Rejected: that would knowingly ship a diagnostic advertising amigrate --to <target>command that still dead-ends — a worse version of the "advertised but fails" trap this work exists to close.Checklist
git commit -s) per the DCO.TML-NNNN: <sentence-case title>form.Summary by CodeRabbit
New Features
Improvements
Documentation
Tests