Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
c65c625
test(e2e): prove protect-dynamodb items need no re-encryption under S…
tobyhede Nov 4, 2026
017d7bd
test(e2e): move encrypted values between DynamoDB and Postgres unchanged
tobyhede Nov 4, 2026
a0a5530
test(e2e): address review findings on the upgrade and cross-store suites
tobyhede Nov 4, 2026
fdb5e0c
docs(skills): normalize downcase columns on write as well as on query
tobyhede Nov 4, 2026
3549a5f
docs(migration): guide the protect-dynamodb upgrade to Stack, tied to…
tobyhede Nov 4, 2026
745e534
docs(migration): fix the upgrade guide's audit findings and pin its n…
tobyhede Nov 4, 2026
a543d31
docs(skills): narrow the encryptQuery result before reading hm
tobyhede Nov 4, 2026
8ad7326
docs(migration): remove only Protect's direct packages in the upgrade…
tobyhede Nov 6, 2026
2344d6e
test(e2e): add the migration harness glossary and Protect.js fixture app
tobyhede Nov 6, 2026
eb42d2a
docs(e2e): define App Host in the migration harness glossary
tobyhede Nov 6, 2026
cec9a18
docs(e2e): record migration harness architecture
tobyhede Nov 8, 2026
c460f18
docs(e2e): resolve migration harness planning review
tobyhede Nov 8, 2026
84d68e6
docs(e2e): clarify harness enforcement and evidence contracts
tobyhede Nov 8, 2026
2e8d8fe
docs(e2e): complete migration harness stage and review contracts
tobyhede Nov 8, 2026
0a73823
docs(e2e): keep migration harness contract in its ADR
tobyhede Nov 8, 2026
eb6d919
docs(e2e): reconcile migration harness review contracts
tobyhede Nov 8, 2026
6cdfcbf
feat(e2e): scaffold migration harness runs and isolation probes
tobyhede Nov 9, 2026
6b77bd0
fix(e2e): honor workflow build and filter invariants
tobyhede Nov 9, 2026
5264e3c
fix(e2e): enforce migration harness acceptance contracts
tobyhede Nov 9, 2026
e0ddf66
fix(e2e): keep harness reporting reachable and nonblocking
tobyhede Nov 9, 2026
892c1a6
feat(e2e): detect legacy ciphertext rewrites in migration Runs
tobyhede Nov 9, 2026
11797e5
test(e2e): exercise no-rewrite checks through real Runs
tobyhede Nov 9, 2026
593b80a
fix(e2e): publish store administration ports on Docker bridges
tobyhede Nov 9, 2026
5d215ba
fix(ci): declare harness self-test build dependencies through Turbo
tobyhede Nov 9, 2026
21b4abf
test(e2e): load local credentials for live no-rewrite Runs
tobyhede Nov 9, 2026
670ca41
fix(e2e): reconcile no-rewrite checks with isolated migration broker
tobyhede Nov 11, 2026
90f8e95
Merge branch 'feat/migration-harness-run' into reconcile/no-rewrite
tobyhede Nov 11, 2026
0b567c9
chore(e2e): carry the isolated Git runtime into no-rewrite tests
tobyhede Nov 11, 2026
2bfd348
Merge branch 'feat/migration-harness-run' into reconcile/no-rewrite
tobyhede Nov 11, 2026
c436e9d
Merge branch 'feat/migration-harness-run' into reconcile/no-rewrite
tobyhede Nov 11, 2026
e3aa646
Merge branch 'feat/migration-harness-run' into reconcile/no-rewrite
tobyhede Nov 11, 2026
a09f848
fix(e2e): preserve migration failures in snapshot checks
tobyhede Nov 11, 2026
97fb9ce
Merge branch 'feat/migration-harness-run' into reconcile/no-rewrite
tobyhede Nov 11, 2026
f18ce35
chore(e2e): merge the strace tracee attribution fix
tobyhede Nov 11, 2026
b918994
fix(e2e): report malformed snapshots as failed checks and 5xx stores …
tobyhede Nov 11, 2026
821c3e0
chore(e2e): merge the isolation Git ownership fix
tobyhede Nov 11, 2026
c142c7c
chore(e2e): merge the held-descriptor observer fix
tobyhede Nov 11, 2026
327ae59
chore(e2e): integrate migration audit fixes
tobyhede Nov 11, 2026
c8371e9
chore(e2e): integrate supervisor frame compatibility
tobyhede Nov 11, 2026
1f4dac2
chore(e2e): integrate tracee lifetime repair into no-rewrite checks
tobyhede Nov 12, 2026
8bcacdf
chore(e2e): integrate socket error classification into no-rewrite checks
tobyhede Nov 12, 2026
ce87501
chore(e2e): integrate friction schema into no-rewrite checks
tobyhede Nov 12, 2026
2c6440b
fix(e2e): keep snapshot-read evidence when a migrated app breaks the …
tobyhede Nov 12, 2026
e8fae24
fix(e2e): give each live no-rewrite Run its own stores
tobyhede Nov 12, 2026
1d3acfc
ci: run only the live harness test in the credentialed e2e job
tobyhede Nov 12, 2026
fc0c963
refactor(e2e): keep the Legacy Data column table in one harness module
tobyhede Nov 12, 2026
814f272
chore(e2e): integrate explicit process tracing fix
tobyhede Nov 12, 2026
cd118e5
chore(e2e): integrate traced actor identity repair
tobyhede Nov 12, 2026
bf9d945
ci: run the live harness test after unrelated e2e failures
tobyhede Nov 12, 2026
81b1595
Merge remote-tracking branch 'origin/feat/migration-harness-no-rewrit…
tobyhede Nov 12, 2026
08bb2cc
chore(e2e): integrate reviewed Run fixes into no-rewrite checks
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
12 changes: 12 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,18 @@ jobs:
env:
DATABASE_URL: ${{ steps.db.outputs.database-url }}

# Deterministic Fake Migrators only: no model credentials or token spend.
# Only the live file reads HARNESS_LIVE; migration-harness.yml runs the
# rest of the self-test on harness changes. Independent of the steps
# above: the harness installs @cipherstash/stack from npm inside its
# isolated containers and never loads this job's native bindings, so an
# unrelated failure earlier in the job must not hide its result.
- name: Run the credentialed Migration Harness self-test
if: ${{ !cancelled() }}
run: pnpm exec turbo run harness:selftest --filter @cipherstash/e2e --env-mode=loose -- migration-harness/__tests__/no-rewrite-live.test.ts
env:
HARNESS_LIVE: '1'

# Guarded on the project being set: if the stack never came up there is
# nothing to tear down, and an unguarded `-p ""` would mask the real error.
- name: Stop postgres
Expand Down
86 changes: 86 additions & 0 deletions e2e/migration-harness/__tests__/no-rewrite-live.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import 'dotenv/config'
import assert from 'node:assert/strict'
import { randomUUID } from 'node:crypto'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { test } from 'vitest'
import { createIsolation } from '../isolation.mjs'
import { createRealDriver } from '../run/real-driver.mjs'
import { run } from '../run/run.mjs'

const keys = [
'CS_WORKSPACE_CRN',
'CS_CLIENT_ID',
'CS_CLIENT_KEY',
'CS_CLIENT_ACCESS_KEY',
]
// HARNESS_LIVE=1 asks for these Runs, so a missing credential fails them
// rather than skipping them silently.
const enabled = process.env.HARNESS_LIVE === '1'
const missing = keys.filter((key) => !process.env[key])
for (const rewrite of [false, true]) {
test.skipIf(!enabled)(
`real Run ${rewrite ? 'detects a read rewrite' : 'keeps Legacy Data unchanged through mutations'}`,
async () => {
assert.deepEqual(
missing,
[],
`HARNESS_LIVE=1 requires ${keys.join(', ')}`,
)
const output = await mkdtemp(resolve(tmpdir(), 'mh-no-rewrite-'))
const isolation = createIsolation()
const manifest = { stack: '^1.2.1', guideHash: null }
const replacement =
'async get(pk) { if (pk === "legacy") { const customer = await decrypt(await stores.get(pk)); await this.put(customer); }'
const script = `import {readFile,writeFile} from "node:fs/promises"; const p="src/app.mjs"; let s=await readFile(p,"utf8"); s=s.replace("async get(pk) {", ${JSON.stringify(replacement)}); await writeFile(p,s); await writeFile("friction.json","[]")`
const tool = rewrite
? `node --input-type=module -e '${script}'`
: `printf '[]' > friction.json`
const adapter = resolve(output, 'adapter.mjs')
await (await import('node:fs/promises')).writeFile(
adapter,
`
import {createInterface} from 'node:readline';
const lines=createInterface({input:process.stdin});
console.log(JSON.stringify({id:'fake',method:'exec',command:['sh','-c',${JSON.stringify(tool)}]}));
for await(const line of lines){ const response=JSON.parse(line); process.exitCode=response.result.exitCode; break; }
lines.close();
`,
)
const command = `node '${adapter}'`
const driver = createRealDriver({
// Store names use the first eight characters, so they must vary per Run.
id: randomUUID(),
root: fileURLToPath(new URL('../../..', import.meta.url)),
output,
command,
isolation,
credentials: Object.fromEntries(
keys.map((key) => [key, process.env[key] ?? '']),
),
manifest,
})
try {
const report = await run({
id: 'live-no-rewrite',
manifest,
driver,
supervision: isolation,
})
const check = report.evidence.find(
(e) => e.checkId === 'no-rewrite.legacy-bytes',
)
assert.ok(check, JSON.stringify(report))
assert.equal(Object(check.payload).passed, !rewrite)
if (rewrite)
assert.match(JSON.stringify(check.payload), /legacy.*__source/)
} finally {
await driver.close()
await rm(output, { recursive: true, force: true })
}
},
600_000,
)
}
102 changes: 102 additions & 0 deletions e2e/migration-harness/__tests__/no-rewrite.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import assert from 'node:assert/strict'
import { test } from 'vitest'
import { requiredOracleChecks } from '../run/migration-checks.mjs'
import { noRewrite } from '../run/no-rewrite.mjs'
import { run } from '../run/run.mjs'

const baseline = {
dynamo: {
Item: {
pk: { S: 'legacy' },
email__source: { S: 'cipher' },
email__hmac: { S: 'term' },
},
},
postgres: [
{
pk: 'legacy',
email: { c: 'pg-cipher', hm: 'pg-term' },
notes: { c: 'note' },
},
],
}
for (const changed of [false, true]) {
test(`Run records ${changed ? 'rewritten' : 'unchanged'} Legacy Data`, async () => {
const current = structuredClone(baseline)
if (changed) current.dynamo.Item.email__source.S = 'rewritten'
const report = await run({
id: 'rewrite',
manifest: { guideHash: 'guide' },
driver: {
provision: async () => {},
installUnmigrated: async () => {},
initializeLegacyData: async () => {},
snapshot: async () => {},
migrate: async () => ({ exitCode: 0 }),
friction: async () => [],
installAppHost: async () => {},
behaviour: async () => [
noRewrite(baseline, current),
...requiredOracleChecks
.filter((id) => id !== 'no-rewrite.legacy-bytes')
.map((id) => ({ id, passed: true })),
],
},
})
assert.equal(report.outcome, changed ? 'behaviour' : 'pass')
const check = report.evidence.find(
(e) => e.checkId === 'no-rewrite.legacy-bytes',
)
assert.ok(check)
if (changed)
assert.match(
JSON.stringify(check.payload),
/dynamo.*legacy.*email__source/,
)
})
}

test('Run rejects a rewritten duplicate Legacy Data row even when the original is last', async () => {
const current = structuredClone(baseline)
const rewritten = structuredClone(current.postgres[0])
rewritten.email.c = 'rewritten'
current.postgres.unshift(rewritten)
const report = await run({
id: 'duplicate-legacy',
manifest: { guideHash: 'guide' },
driver: {
provision: async () => {},
installUnmigrated: async () => {},
initializeLegacyData: async () => {},
snapshot: async () => {},
migrate: async () => ({ exitCode: 0 }),
friction: async () => [],
installAppHost: async () => {},
behaviour: async () => [
noRewrite(baseline, current),
...requiredOracleChecks
.filter((id) => id !== 'no-rewrite.legacy-bytes')
.map((id) => ({ id, passed: true })),
],
},
})
assert.equal(report.outcome, 'behaviour')
assert.equal(report.editorEligible, true)
const check = report.evidence.find(
(e) => e.checkId === 'no-rewrite.legacy-bytes',
)
assert.ok(check, 'a malformed current snapshot still reports the check')
assert.equal(Object(check.payload).passed, false)
})

test('a deleted Legacy Data row is a failed check, while a malformed baseline still throws', () => {
const current = structuredClone(baseline)
current.postgres = []
const check = noRewrite(baseline, current)
assert.equal(check.id, 'no-rewrite.legacy-bytes')
assert.equal(check.passed, false)
assert.throws(
() => noRewrite(current, baseline),
/exactly one Legacy Data Postgres row/,
)
})
Loading
Loading