Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6c19983
feat(e2e): start the Postgres v2 migration harness foundation
tobyhede Nov 11, 2026
6a4f85c
Merge branch 'reconcile/selftest' into feat/migration-harness-postgre…
tobyhede Nov 11, 2026
9d3e705
Merge branch 'reconcile/selftest' into feat/migration-harness-postgre…
tobyhede Nov 11, 2026
ea43a18
Merge branch 'reconcile/selftest' into feat/migration-harness-postgre…
tobyhede Nov 11, 2026
f739c25
feat(e2e): run Postgres v2 migrations through the isolated harness
tobyhede Nov 11, 2026
040d365
fix(e2e): project PostgreSQL snapshot domains as JSONB
tobyhede Nov 11, 2026
775f8d2
test(e2e): cover domain projection in PostgreSQL snapshots
tobyhede Nov 11, 2026
4dbebc7
Merge branch 'reconcile/selftest' into feat/migration-harness-postgre…
tobyhede Nov 11, 2026
0adf440
fix(e2e): classify PostgreSQL schema damage as migration failure
tobyhede Nov 11, 2026
b719568
fix(e2e): verify complete PostgreSQL migration storage
tobyhede Nov 11, 2026
da907ea
chore(e2e): merge the strace tracee attribution fix
tobyhede Nov 11, 2026
f337459
chore(e2e): merge the snapshot classification review fixes
tobyhede Nov 11, 2026
36d0604
chore(e2e): merge the npm 12 Stack resolution fix
tobyhede Nov 11, 2026
d462286
chore(e2e): merge the isolation Git ownership fix
tobyhede Nov 11, 2026
acf2883
chore(e2e): merge the held-descriptor observer fix
tobyhede Nov 11, 2026
4666aba
fix(e2e): run the PostgreSQL conversion through its own migrate entry…
tobyhede Nov 11, 2026
0909227
fix(e2e): validate the installed migration application
tobyhede Nov 11, 2026
95de9ee
chore(e2e): integrate audit fixes into PostgreSQL runs
tobyhede Nov 11, 2026
254b1da
chore(e2e): integrate supervisor frame compatibility
tobyhede Nov 11, 2026
5b7f28a
chore(e2e): integrate tracee lifetime repair into PostgreSQL runs
tobyhede Nov 12, 2026
8fa8a97
chore(e2e): integrate socket error classification into PostgreSQL runs
tobyhede Nov 12, 2026
1d42d00
chore(e2e): integrate friction schema into PostgreSQL runs
tobyhede Nov 12, 2026
a1b1576
chore(e2e): integrate explicit process tracing fix
tobyhede Nov 12, 2026
d4c3822
chore(e2e): integrate traced actor identity repair
tobyhede Nov 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions e2e/migration-harness/__tests__/installed-copy-checks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import assert from 'node:assert/strict'
import { cp, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { test } from 'vitest'
import { migrationChecks } from '../run/migration-checks.mjs'
import { run } from '../run/run.mjs'

const manifest = { dependencies: { '@cipherstash/stack': '1.2.1' } }
const lock = {
packages: {
'': manifest,
'node_modules/@cipherstash/stack': { version: '1.2.1' },
'node_modules/@cipherstash/protect-ffi': { version: '0.31.0' },
},
}

for (const change of [
{
name: 'a Protect import',
path: 'src/app.mjs',
text: 'import "@cipherstash/protect"',
check: 'migration.protect-dependencies-absent',
},
{
name: 'a CTS endpoint shim',
path: 'src/app.mjs',
text: 'process.env.CS_CTS_HOST = "https://example.invalid"',
check: 'migration.endpoint-shim-absent',
},
{
name: 'a ZeroKMS endpoint shim',
path: 'src/app.mjs',
text: 'process.env.CS_ZEROKMS_HOST = "https://example.invalid"',
check: 'migration.endpoint-shim-absent',
},
{
name: 'a legacy manifest dependency',
path: 'package.json',
text: JSON.stringify({
dependencies: {
...manifest.dependencies,
'@cipherstash/protect': '10.2.1',
},
}),
check: 'migration.protect-dependencies-absent',
},
{
name: 'a legacy lockfile dependency',
path: 'package-lock.json',
text: JSON.stringify({
packages: {
...lock.packages,
'node_modules/@cipherstash/protect': { version: '10.2.1' },
},
}),
check: 'migration.protect-dependencies-absent',
},
{
name: 'a legacy FFI declaration',
path: 'package.json',
text: JSON.stringify({
dependencies: {
...manifest.dependencies,
'@cipherstash/protect-ffi': '0.18.1',
},
}),
check: 'migration.protect-ffi-version',
},
]) {
test(`Run rejects ${change.name} introduced in the installed App Host copy`, async () => {
const directory = await mkdtemp(join(tmpdir(), 'mh-installed-checks-'))
const workspace = join(directory, 'migrator')
const installedWorkspace = join(directory, 'app-host')
try {
await mkdir(join(workspace, 'src'), { recursive: true })
await writeFile(join(workspace, 'package.json'), JSON.stringify(manifest))
await writeFile(
join(workspace, 'package-lock.json'),
JSON.stringify(lock),
)
await writeFile(
join(workspace, 'src/app.mjs'),
'import "@cipherstash/stack/v3"',
)
await cp(workspace, installedWorkspace, { recursive: true })
const inspect = () =>
migrationChecks({
workspace,
installedWorkspace,
stackVersion: '1.2.1',
ffiRange: '0.31.0',
mutationSnapshot: { dynamo: { Item: { active__source: 'cipher' } } },
})
assert.ok(
(await inspect()).every((check) => check.passed),
'The unchanged installed app must satisfy the migration requirements',
)
const report = await run({
id: 'installed-copy',
manifest: { guideHash: 'guide' },
driver: {
provision: async () => {},
installUnmigrated: async () => {},
initializeLegacyData: async () => {},
snapshot: async () => {},
migrate: async () => ({ exitCode: 0 }),
friction: async () => [],
installAppHost: async () => {
// Model a lifecycle script changing only the deployed copy.
await writeFile(join(installedWorkspace, change.path), change.text)
},
behaviour: async () => [
{ id: 'no-rewrite.legacy-bytes', passed: true },
...(await inspect()),
],
},
})
assert.equal(
report.outcome,
'behaviour',
'A clean Migrator workspace must not hide installed-copy regressions',
)
assert.ok(
report.evidence.some(
(entry) =>
entry.checkId === change.check &&
Object(entry.payload).passed === false,
),
`The Run must name the failed ${change.check} requirement`,
)
} finally {
await rm(directory, { recursive: true, force: true })
}
})
}
56 changes: 56 additions & 0 deletions e2e/migration-harness/__tests__/postgres-behaviour.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { expect, test } from 'vitest'
import {
postgresBehaviour,
postgresLegacy,
} from '../run/postgres-behaviour.mjs'

test('Postgres behaviour separates read-only checks from fresh writes', async () => {
const rows = new Map<string, Record<string, unknown>>(
postgresLegacy.map((row) => [row.pk, row]),
)
const calls: string[] = []
const result = await postgresBehaviour(async (method, args) => {
calls.push(method)
const value = args[0]
if (method === 'put') {
const row = Object.fromEntries(Object.entries(Object(value)))
rows.set(String(row.pk), row)
return null
}
if (method === 'get') return rows.get(String(value)) ?? null
const column = method === 'findEmail' ? 'email' : 'age'
return [...rows.values()]
.filter((row) => row[column] === value)
.sort((a, b) => String(a.pk).localeCompare(String(b.pk)))
})
expect(calls).not.toContain('put')
expect(result.checks.every((check) => check.passed)).toBe(true)
expect((await result.mutate()).every((check) => check.passed)).toBe(true)
expect(calls).toContain('put')
})

test('Postgres behaviour identifies a query mapping that returns only one equal-age row', async () => {
const result = await postgresBehaviour(async (method, args) => {
if (method === 'get')
return postgresLegacy.find((row) => row.pk === args[0])
if (method === 'findEmail')
return postgresLegacy.filter((row) => row.email === args[0])
return postgresLegacy.filter((row) => row.age === args[0]).slice(0, 1)
})
expect(result.checks).toContainEqual(
expect.objectContaining({
id: 'behaviour.postgres-age-lookup',
passed: false,
}),
)
})

test('Postgres failures retain safe diagnostic codes without provider or SQL text', async () => {
const result = await postgresBehaviour(async () => {
throw Object.assign(Error('secret-sentinel in SQL or provider response'), {
code: '42P01',
})
})
expect(result.checks[0].cause).toContain('42P01')
expect(JSON.stringify(result.checks)).not.toContain('secret-sentinel')
})
Loading
Loading