Repository navigation
feat(e2e): detect legacy ciphertext rewrites in migration Runs #1580
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c65c625
017d7bd
a0a5530
fdb5e0c
3549a5f
745e534
a543d31
8ad7326
2344d6e
eb42d2a
cec9a18
c460f18
84d68e6
2e8d8fe
0a73823
eb6d919
6cdfcbf
6b77bd0
5264e3c
e0ddf66
892c1a6
11797e5
593b80a
5d215ba
21b4abf
670ca41
90f8e95
0b567c9
2bfd348
c436e9d
e3aa646
a09f848
97fb9ce
f18ce35
b918994
821c3e0
c142c7c
327ae59
c8371e9
1f4dac2
8bcacdf
ce87501
814f272
cd118e5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| import 'dotenv/config' | ||
| import assert from 'node:assert/strict' | ||
| 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', | ||
| ] | ||
| const enabled = | ||
| process.env.HARNESS_LIVE === '1' && keys.every((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 () => { | ||
| 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({ | ||
| id: `live-${Date.now()}-${rewrite}`, | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1 · major Source: independent Codex reviewer |
||
| 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, | ||
| ) | ||
| } | ||
| 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/, | ||
| ) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
minor This runs the whole
harness:selftestsuite (17 files) on every PR, although onlyno-rewrite-live.test.tsreadsHARNESS_LIVE.migration-harness.ymlalready runs the other 16 on harness changes. Under!cancelled()the step also starts up to two 10-minute live Runs after an earlier step has failed. Pass the test file through (… --env-mode=loose -- migration-harness/__tests__/no-rewrite-live.test.ts) and drop!cancelled()so the defaultsuccess()gate applies.Source: code-review (medium) · claude-opus-5-5
Generated by Claude Code