Skip to content

Commit c20df92

Browse files
auxesisclaude
andcommitted
test(stack-prisma): run the shipped edges' own SQL in the live suite
The live migration suite only ever applied the installed @cipherstash/eql's SQL, so no carrier op's or upgrade edge's postcheck ever ran against a database. Those postchecks compare against literals interpolated at emit time (`eql_v3.version() = '3.0.6'`), so one that never matches what its own edge installs fails every customer apply while every offline test stays green. Two cases now apply the descriptor's baked SQL the runner's way: execute, then postchecks, one transaction per plan. - db init: the genesis edge alone, on an empty eql_v3, satisfies every one of its postchecks. - migrate: a 1.1.x database walks the 3.0.5 then the 3.0.6 edge. It starts from the 3.0.4 edge's bytes, which are the 1.1.x baseline's (both pin 63104a81). Each edge is asserted NOT pre-satisfied first, because the runner skips an op whose postchecks already hold, so the double install the 3.0.6 changeset warns about is shown to be real. The end state then satisfies the genesis edge's postchecks. Ops are typed by extending the framework's MigrationPlanOperation, as stale-vendored-space.test.ts does, so the descriptor's ops narrow to them without a cast across unrelated types. The byte-identity case stays, and is red by construction between this branch merging and #938 doing so. The baseline then bakes 3.0.6 while the installed package is still 3.0.5. The comment there and DEVELOPING.md say so. Verified against ghcr.io/cipherstash/postgres-eql:17-2.3.1. On this branch 6 of 7 pass; byte-identity fails, and its diff is exactly the two stamp lines. With #938's EQL files applied all 7 pass. Two mutants fail as they should. Dropping the 3.0.6 edge from the walk gives "expected '3.0.5' to be '3.0.6'". A 3.0.5 bundle checked against the genesis postchecks fails the 3.0.4 carrier's version check. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
1 parent 78ab894 commit c20df92

1 file changed

Lines changed: 136 additions & 22 deletions

File tree

‎packages/stack-prisma/test/live/migration-apply-live-pg.test.ts‎

Lines changed: 136 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,22 @@
99
* op carries the `cipherstash:install-eql-v3-bundle-v1` invariant, the op's own
1010
* postchecks hold against the live database, and no v2-style
1111
* `add_search_config` was executed (v3 needs no per-column search configuration).
12+
*
13+
* It then applies the shipped edges' OWN baked SQL, independent of the
14+
* installed `@cipherstash/eql`: the genesis edge alone on an empty schema
15+
* (the `db init` path), and a 1.1.x database (eql-3.0.4) walking the 3.0.5
16+
* and 3.0.6 upgrade edges (the `migrate` path).
1217
*/
1318

1419
import 'dotenv/config'
20+
import type { MigrationPlanOperation } from '@prisma/orm-framework/components/control'
1521
import type postgres from 'postgres'
1622
import { afterAll, beforeAll, expect, it } from 'vitest'
1723
import cipherstashDescriptor from '../../src/exports/control'
1824
import {
25+
CIPHERSTASH_V3_304_UPGRADE_MIGRATION_NAME,
26+
CIPHERSTASH_V3_305_UPGRADE_MIGRATION_NAME,
27+
CIPHERSTASH_V3_306_UPGRADE_MIGRATION_NAME,
1928
CIPHERSTASH_V3_BASELINE_MIGRATION_NAME,
2029
CIPHERSTASH_V3_INVARIANTS,
2130
} from '../../src/extension-metadata/constants-v3'
@@ -29,18 +38,83 @@ import { describeLivePg } from './helpers/live-gate'
2938

3039
const INSTALL_OP_ID = 'cipherstash.install-eql-v3-bundle'
3140

32-
interface MigrationOp {
33-
readonly id: string
34-
readonly invariantId: string
35-
readonly operationClass: string
36-
readonly execute: ReadonlyArray<{
37-
readonly description: string
38-
readonly sql: string
39-
}>
40-
readonly postcheck: ReadonlyArray<{
41-
readonly description: string
42-
readonly sql: string
43-
}>
41+
type Step = { readonly description: string; readonly sql: string }
42+
43+
/**
44+
* The SQL target's op shape: `@prisma/orm-target-postgres` adds the
45+
* `execute` / `postcheck` step lists to the framework's base op.
46+
*/
47+
interface MigrationOp extends MigrationPlanOperation {
48+
readonly execute: readonly Step[]
49+
readonly postcheck: readonly Step[]
50+
}
51+
52+
function descriptorOps(dirName: string): readonly MigrationOp[] {
53+
const migration = cipherstashDescriptor.contractSpace?.migrations.find(
54+
(m) => m.dirName === dirName,
55+
)
56+
if (!migration) throw new Error(`descriptor is missing migration ${dirName}`)
57+
return migration.ops as readonly MigrationOp[]
58+
}
59+
60+
async function stepHolds(
61+
sql: postgres.Sql | postgres.TransactionSql,
62+
step: Step,
63+
): Promise<boolean> {
64+
const rows = (await sql.unsafe(step.sql)) as unknown as Array<
65+
Record<string, unknown>
66+
>
67+
return Object.values(rows[0] ?? {})[0] === true
68+
}
69+
70+
/** The runner's skip rule: an op whose postchecks all hold is not executed. */
71+
async function postchecksAllHold(
72+
sql: postgres.TransactionSql,
73+
op: MigrationOp,
74+
): Promise<boolean> {
75+
if (op.postcheck.length === 0) return false
76+
for (const check of op.postcheck) {
77+
if (!(await stepHolds(sql, check))) return false
78+
}
79+
return true
80+
}
81+
82+
async function expectPostchecksHold(
83+
sql: postgres.Sql | postgres.TransactionSql,
84+
ops: readonly MigrationOp[],
85+
): Promise<void> {
86+
for (const op of ops) {
87+
for (const check of op.postcheck) {
88+
expect(
89+
await stepHolds(sql, check),
90+
`${op.id}: ${check.description}`,
91+
).toBe(true)
92+
}
93+
}
94+
}
95+
96+
/**
97+
* The runner's execute → postcheck order, per op. Unlike the runner it
98+
* never skips a pre-satisfied op; callers that rely on an op really
99+
* running assert `postchecksAllHold` is false first.
100+
*/
101+
async function applyEdge(
102+
tx: postgres.TransactionSql,
103+
dirName: string,
104+
): Promise<void> {
105+
for (const op of descriptorOps(dirName)) {
106+
for (const step of op.execute) await tx.unsafe(step.sql)
107+
await expectPostchecksHold(tx, [op])
108+
}
109+
}
110+
111+
async function eqlVersion(
112+
sql: postgres.Sql | postgres.TransactionSql,
113+
): Promise<string | undefined> {
114+
const [row] = await sql<{ version: string }[]>`
115+
SELECT eql_v3.version() AS version
116+
`
117+
return row?.version
44118
}
45119

46120
/**
@@ -52,17 +126,11 @@ interface MigrationOp {
52126
* because this suite only runs against a live database.)
53127
*/
54128
function readRuntimeDescriptorOp(): MigrationOp {
55-
const migration = cipherstashDescriptor.contractSpace?.migrations.find(
56-
({ dirName }) => dirName === CIPHERSTASH_V3_BASELINE_MIGRATION_NAME,
57-
)
58-
const op = migration?.ops.find(({ id }) => id === INSTALL_OP_ID) as
59-
| MigrationOp
60-
| undefined
129+
const ops = descriptorOps(CIPHERSTASH_V3_BASELINE_MIGRATION_NAME)
130+
const op = ops.find(({ id }) => id === INSTALL_OP_ID)
61131
if (!op) {
62132
throw new Error(
63-
`expected an op with id "${INSTALL_OP_ID}" in the v3 baseline migration, got [${(
64-
migration?.ops ?? []
65-
)
133+
`expected an op with id "${INSTALL_OP_ID}" in the v3 baseline migration, got [${ops
66134
.map(({ id }) => id)
67135
.join(', ')}]`,
68136
)
@@ -97,7 +165,9 @@ describeLivePg('v3 baseline migration bundle against live Postgres', () => {
97165
expect(op.operationClass).toBe('additive')
98166
expect(op.execute).toHaveLength(1)
99167
// Byte identity: what installEqlV3IfNeeded just applied IS the migration
100-
// bundle the control descriptor gives to `prisma-next migrate`.
168+
// bundle the control descriptor gives to `prisma-next migrate`. Red by
169+
// construction while an EQL bump's migration is on main ahead of the
170+
// Version Packages PR that moves @cipherstash/eql (DEVELOPING.md).
101171
expect(op.execute[0]?.sql).toBe(readInstallSql())
102172
})
103173

@@ -139,6 +209,50 @@ describeLivePg('v3 baseline migration bundle against live Postgres', () => {
139209
}
140210
}, 60_000)
141211

212+
// Postchecks compare against literals interpolated at emit time, so one
213+
// that never matches what its own edge installs fails every customer
214+
// apply while every offline test stays green.
215+
216+
it('db init: the genesis edge alone, on an empty eql_v3, satisfies every one of its postchecks', async () => {
217+
await uninstallEqlV3(sql)
218+
await sql.begin((tx) =>
219+
applyEdge(tx, CIPHERSTASH_V3_BASELINE_MIGRATION_NAME),
220+
)
221+
}, 240_000)
222+
223+
it('migrate: a 1.1.x database (eql-3.0.4) walks the 3.0.5 then 3.0.6 edges, installing the bundle twice', async () => {
224+
// 1.1.x baselines baked the same eql-3.0.4 bytes as the 3.0.4 edge
225+
// (both pin `63104a81…`). The path is pinned offline in
226+
// stale-vendored-space.test.ts; this runs its SQL.
227+
await uninstallEqlV3(sql)
228+
await sql.begin((tx) =>
229+
applyEdge(tx, CIPHERSTASH_V3_304_UPGRADE_MIGRATION_NAME),
230+
)
231+
expect(await eqlVersion(sql)).toBe('3.0.4')
232+
233+
// One transaction, as the runner applies a whole plan.
234+
await sql.begin(async (tx) => {
235+
for (const dirName of [
236+
CIPHERSTASH_V3_305_UPGRADE_MIGRATION_NAME,
237+
CIPHERSTASH_V3_306_UPGRADE_MIGRATION_NAME,
238+
]) {
239+
for (const op of descriptorOps(dirName)) {
240+
expect(
241+
await postchecksAllHold(tx, op),
242+
`${op.id} would be skipped as pre-satisfied`,
243+
).toBe(false)
244+
}
245+
await applyEdge(tx, dirName)
246+
}
247+
})
248+
249+
expect(await eqlVersion(sql)).toBe('3.0.6')
250+
await expectPostchecksHold(
251+
sql,
252+
descriptorOps(CIPHERSTASH_V3_BASELINE_MIGRATION_NAME),
253+
)
254+
}, 480_000)
255+
142256
it('executes no v2-style search configuration (no add_search_config)', () => {
143257
const bundle = readInstallSql()
144258
expect(bundle).not.toContain('add_search_config')

0 commit comments

Comments
 (0)