Unified · Split
| @@ -1161,6 +1161,59 @@ describe("local v1 -> v2 migration", () => { · 1160 unmodified lines | ||
| 1161 | 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 | 1217 | it("recovers after SIGKILL at every migration journal boundary", async () => { |
| 1165 | 1218 | // Each iteration rewrites the process-global XDG auth.json; keep this sweep |
| 1166 | 1219 | // sequential so phase secrets cannot bleed across concurrent migrations. |
| @@ -1191,7 +1244,7 @@ describe("local v1 -> v2 migration", () => { · 24 unmodified lines | ||
| 1191 | 1244 | await assertMigratedStripeDbAndSecret({ dbPath, scopeId, secret, walMarker }); |
| 1192 | 1245 | expect(existsSync(`${dbPath}.v1-v2-migration.json`)).toBe(false); |
| 1193 | 1246 | } |
| 1194 | − }); | |
| 1247 | + }, 30_000); | |
| 1195 | 1248 | |
| 1196 | 1249 | it("completes a built journal recovery before opening the canonical DB", async () => { |
| 1197 | 1250 | const scopeId = "executor-workspace-abcd1234"; |