Unified · Split
| @@ -1161,6 +1161,59 @@ describe("local v1 -> v2 migration", () => { · 1160 unmodified lines | |||
| 1161 | expect(auth[migratedItemId(scopeId, "stripe-key")]).toBe("sk_test_recovered"); | 1161 | expect(auth[migratedItemId(scopeId, "stripe-key")]).toBe("sk_test_recovered"); |
| 1162 | }); | 1162 | }); |
| 1163 | 1163 | ||
| 1164 | it("restarts from an unreadable journal when the canonical v1 database is intact", async () => { | ||
| 1165 | const scopeId = "executor-workspace-corruptjournal"; | ||
| 1166 | const dataDir = join(workDir, "data"); | ||
| 1167 | const dbPath = join(dataDir, "data.db"); | ||
| 1168 | const journalPath = `${dbPath}.v1-v2-migration.json`; | ||
| 1169 | mkdirSync(dataDir, { recursive: true }); | ||
| 1170 | await seedV1Db(dbPath, scopeId); | ||
| 1171 | await writeLiveWalMarker(dbPath, "wal-corrupt-journal"); | ||
| 1172 | |||
| 1173 | const authDir = join(process.env.XDG_DATA_HOME!, "executor"); | ||
| 1174 | mkdirSync(authDir, { recursive: true }); | ||
| 1175 | writeFileSync( | ||
| 1176 | join(authDir, "auth.json"), | ||
| 1177 | JSON.stringify({ [scopeId]: { "stripe-key": "sk_test_corrupt_journal" } }, null, 2), | ||
| 1178 | ); | ||
| 1179 | writeFileSync(journalPath, "{ this is not valid json", { mode: 0o600 }); | ||
| 1180 | |||
| 1181 | const result = await migrateLocalV1ToV2IfNeeded({ | ||
| 1182 | sqlitePath: dbPath, | ||
| 1183 | tables: collectTables(), | ||
| 1184 | namespace: "executor_local", | ||
| 1185 | tenantId: scopeId, | ||
| 1186 | }); | ||
| 1187 | |||
| 1188 | expect(result.migrated).toBe(true); | ||
| 1189 | expect(existsSync(journalPath)).toBe(false); | ||
| 1190 | await assertMigratedStripeDbAndSecret({ | ||
| 1191 | dbPath, | ||
| 1192 | scopeId, | ||
| 1193 | secret: "sk_test_corrupt_journal", | ||
| 1194 | walMarker: "wal-corrupt-journal", | ||
| 1195 | }); | ||
| 1196 | }); | ||
| 1197 | |||
| 1198 | it("fails closed on an unreadable journal when the canonical database is missing", async () => { | ||
| 1199 | const dataDir = join(workDir, "data"); | ||
| 1200 | const dbPath = join(dataDir, "data.db"); | ||
| 1201 | const journalPath = `${dbPath}.v1-v2-migration.json`; | ||
| 1202 | mkdirSync(dataDir, { recursive: true }); | ||
| 1203 | writeFileSync(journalPath, "{ this is not valid json", { mode: 0o600 }); | ||
| 1204 | |||
| 1205 | await expect( | ||
| 1206 | migrateLocalV1ToV2IfNeeded({ | ||
| 1207 | sqlitePath: dbPath, | ||
| 1208 | tables: collectTables(), | ||
| 1209 | namespace: "executor_local", | ||
| 1210 | tenantId: "executor-workspace-missingdb", | ||
| 1211 | }), | ||
| 1212 | ).rejects.toMatchObject({ _tag: "LocalV1V2MigrationError" }); | ||
| 1213 | expect(existsSync(dbPath)).toBe(false); | ||
| 1214 | expect(existsSync(journalPath)).toBe(true); | ||
| 1215 | }); | ||
| 1216 | |||
| 1164 | it("recovers after SIGKILL at every migration journal boundary", async () => { | 1217 | it("recovers after SIGKILL at every migration journal boundary", async () => { |
| 1165 | // Each iteration rewrites the process-global XDG auth.json; keep this sweep | 1218 | // Each iteration rewrites the process-global XDG auth.json; keep this sweep |
| 1166 | // sequential so phase secrets cannot bleed across concurrent migrations. | 1219 | // sequential so phase secrets cannot bleed across concurrent migrations. |
| @@ -1191,7 +1244,7 @@ describe("local v1 -> v2 migration", () => { · 24 unmodified lines | |||
| 1191 | await assertMigratedStripeDbAndSecret({ dbPath, scopeId, secret, walMarker }); | 1244 | await assertMigratedStripeDbAndSecret({ dbPath, scopeId, secret, walMarker }); |
| 1192 | expect(existsSync(`${dbPath}.v1-v2-migration.json`)).toBe(false); | 1245 | expect(existsSync(`${dbPath}.v1-v2-migration.json`)).toBe(false); |
| 1193 | } | 1246 | } |
| 1194 | }); | 1247 | }, 30_000); |
| 1195 | 1248 | ||
| 1196 | it("completes a built journal recovery before opening the canonical DB", async () => { | 1249 | it("completes a built journal recovery before opening the canonical DB", async () => { |
| 1197 | const scopeId = "executor-workspace-abcd1234"; | 1250 | const scopeId = "executor-workspace-abcd1234"; |