| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| import { readFileSync } from "node:fs"; |
| import { dirname, resolve } from "node:path"; |
| import { fileURLToPath } from "node:url"; |
| import { convexTest } from "convex-test"; |
| import { describe, expect, test } from "vitest"; |
| import schema from "../schema"; |
| import { internal } from "../_generated/api"; |
|
|
| const modules = import.meta.glob("../**/*.ts"); |
|
|
| const __dirname = dirname(fileURLToPath(import.meta.url)); |
| const runnerSrc = readFileSync( |
| resolve(__dirname, "..", "broadcast", "rampRunner.ts"), |
| "utf-8", |
| ); |
|
|
| async function seedRampConfig( |
| t: ReturnType<typeof convexTest>, |
| overrides: Partial<{ |
| currentTier: number; |
| rampCurve: number[]; |
| waveLabelPrefix: string; |
| waveLabelOffset: number; |
| lastRunStatus: string | undefined; |
| lastWaveBroadcastId: string | undefined; |
| lastWaveSentAt: number | undefined; |
| pendingRunId: string | undefined; |
| pendingRunStartedAt: number | undefined; |
| pendingWaveLabel: string | undefined; |
| pendingSegmentId: string | undefined; |
| pendingAssigned: number | undefined; |
| pendingExportAt: number | undefined; |
| pendingBroadcastId: string | undefined; |
| pendingBroadcastAt: number | undefined; |
| active: boolean; |
| killGateTripped: boolean; |
| }> = {}, |
| ) { |
| await t.run(async (ctx) => { |
| await ctx.db.insert("broadcastRampConfig", { |
| key: "current", |
| active: overrides.active ?? true, |
| rampCurve: overrides.rampCurve ?? [500, 1500, 5000], |
| currentTier: overrides.currentTier ?? 0, |
| waveLabelPrefix: overrides.waveLabelPrefix ?? "wave", |
| waveLabelOffset: overrides.waveLabelOffset ?? 3, |
| bounceKillThreshold: 0.04, |
| complaintKillThreshold: 0.0008, |
| killGateTripped: overrides.killGateTripped ?? false, |
| lastRunStatus: overrides.lastRunStatus, |
| lastWaveBroadcastId: overrides.lastWaveBroadcastId, |
| lastWaveSentAt: overrides.lastWaveSentAt, |
| pendingRunId: overrides.pendingRunId, |
| pendingRunStartedAt: overrides.pendingRunStartedAt, |
| pendingWaveLabel: overrides.pendingWaveLabel, |
| pendingSegmentId: overrides.pendingSegmentId, |
| pendingAssigned: overrides.pendingAssigned, |
| pendingExportAt: overrides.pendingExportAt, |
| pendingBroadcastId: overrides.pendingBroadcastId, |
| pendingBroadcastAt: overrides.pendingBroadcastAt, |
| }); |
| }); |
| } |
|
|
| async function loadRow(t: ReturnType<typeof convexTest>) { |
| return await t.run(async (ctx) => |
| ctx.db |
| .query("broadcastRampConfig") |
| .withIndex("by_key", (q) => q.eq("key", "current")) |
| .first(), |
| ); |
| } |
|
|
| |
| |
| |
|
|
| describe("_claimTierForRun β lease lifecycle", () => { |
| test("claims successfully when no lease is held", async () => { |
| const t = convexTest(schema, modules); |
| await seedRampConfig(t); |
| const result = await t.mutation(internal.broadcast.rampRunner._claimTierForRun, { |
| runId: "run-1", |
| expectedCurrentTier: 0, |
| }); |
| expect(result.ok).toBe(true); |
| const row = await loadRow(t); |
| expect(row?.pendingRunId).toBe("run-1"); |
| expect(row?.pendingRunStartedAt).toBeTypeOf("number"); |
| }); |
|
|
| test("rejects when another lease is held and fresh β RACE GUARD", async () => { |
| |
| |
| |
| const t = convexTest(schema, modules); |
| await seedRampConfig(t); |
|
|
| const first = await t.mutation(internal.broadcast.rampRunner._claimTierForRun, { |
| runId: "run-A", |
| expectedCurrentTier: 0, |
| }); |
| expect(first.ok).toBe(true); |
|
|
| const second = await t.mutation(internal.broadcast.rampRunner._claimTierForRun, { |
| runId: "run-B", |
| expectedCurrentTier: 0, |
| }); |
| expect(second.ok).toBe(false); |
| expect(second.reason).toBe("lease-held"); |
| expect(second.heldBy).toBe("run-A"); |
| }); |
|
|
| test("rejects when expectedCurrentTier doesn't match β protects against tier-already-advanced", async () => { |
| const t = convexTest(schema, modules); |
| await seedRampConfig(t, { currentTier: 2 }); |
| const result = await t.mutation(internal.broadcast.rampRunner._claimTierForRun, { |
| runId: "run-1", |
| expectedCurrentTier: 1, |
| }); |
| expect(result.ok).toBe(false); |
| expect(result.reason).toBe("tier-moved"); |
| expect(result.actualTier).toBe(2); |
| }); |
|
|
| test("rejects EVEN A STALE lease β no automatic time-based override (P1#1)", async () => { |
| |
| |
| |
| |
| |
| const t = convexTest(schema, modules); |
| await seedRampConfig(t, { |
| pendingRunId: "run-very-old", |
| pendingRunStartedAt: Date.now() - 6 * 60 * 60 * 1000, |
| }); |
| const result = await t.mutation(internal.broadcast.rampRunner._claimTierForRun, { |
| runId: "run-fresh", |
| expectedCurrentTier: 0, |
| }); |
| expect(result.ok).toBe(false); |
| expect(result.reason).toBe("lease-held"); |
| expect(result.heldBy).toBe("run-very-old"); |
| const row = await loadRow(t); |
| |
| expect(row?.pendingRunId).toBe("run-very-old"); |
| }); |
| }); |
|
|
| |
| |
| |
|
|
| describe("forceReleaseLease β operator-only stale-lease recovery (P1#1)", () => { |
| test("clears the lease and sets lastRunStatus=partial-failure so recoverFromPartialFailure can pick up", async () => { |
| const t = convexTest(schema, modules); |
| await seedRampConfig(t, { |
| pendingRunId: "run-wedged", |
| pendingRunStartedAt: Date.now() - 6 * 60 * 60 * 1000, |
| |
| |
| pendingWaveLabel: "wave-7", |
| pendingSegmentId: "seg-wedged", |
| pendingAssigned: 5000, |
| pendingBroadcastId: "bc-wedged", |
| }); |
| const result = await t.mutation( |
| internal.broadcast.rampRunner.forceReleaseLease, |
| { reason: "cron action wedged 6h, no partial-failure recorded" }, |
| ); |
| expect(result.ok).toBe(true); |
| expect(result.releasedRunId).toBe("run-wedged"); |
|
|
| const row = await loadRow(t); |
| expect(row?.pendingRunId).toBeUndefined(); |
| expect(row?.pendingRunStartedAt).toBeUndefined(); |
| expect(row?.lastRunStatus).toBe("partial-failure"); |
| expect(row?.lastRunError).toMatch(/forced-release/i); |
| |
| expect(row?.pendingWaveLabel).toBe("wave-7"); |
| expect(row?.pendingSegmentId).toBe("seg-wedged"); |
| expect(row?.pendingBroadcastId).toBe("bc-wedged"); |
| }); |
|
|
| test("noop when no lease is held", async () => { |
| const t = convexTest(schema, modules); |
| await seedRampConfig(t); |
| const result = await t.mutation( |
| internal.broadcast.rampRunner.forceReleaseLease, |
| { reason: "no-op test" }, |
| ); |
| expect(result.ok).toBe(true); |
| expect(result.noop).toBe(true); |
| }); |
|
|
| test("after force-release, a fresh claim can succeed", async () => { |
| const t = convexTest(schema, modules); |
| await seedRampConfig(t, { |
| pendingRunId: "run-stuck", |
| pendingRunStartedAt: Date.now() - 60 * 60 * 1000, |
| }); |
| await t.mutation(internal.broadcast.rampRunner.forceReleaseLease, { |
| reason: "stuck", |
| }); |
| const claim = await t.mutation( |
| internal.broadcast.rampRunner._claimTierForRun, |
| { runId: "run-recovered", expectedCurrentTier: 0 }, |
| ); |
| expect(claim.ok).toBe(true); |
| }); |
| }); |
|
|
| |
| |
| |
|
|
| describe("_recordWaveSent β lease validation on success", () => { |
| test("clears the lease and advances tier when the lease is held by the same runId", async () => { |
| const t = convexTest(schema, modules); |
| await seedRampConfig(t, { |
| currentTier: 1, |
| pendingRunId: "run-X", |
| pendingRunStartedAt: Date.now(), |
| }); |
| const result = await t.mutation(internal.broadcast.rampRunner._recordWaveSent, { |
| runId: "run-X", |
| expectedCurrentTier: 1, |
| newTier: 2, |
| waveLabel: "wave-5", |
| broadcastId: "bc-test-123", |
| segmentId: "seg-test-456", |
| assigned: 1500, |
| sentAt: Date.now(), |
| }); |
| expect(result.ok).toBe(true); |
| const row = await loadRow(t); |
| expect(row?.currentTier).toBe(2); |
| expect(row?.lastWaveBroadcastId).toBe("bc-test-123"); |
| expect(row?.pendingRunId).toBeUndefined(); |
| expect(row?.pendingRunStartedAt).toBeUndefined(); |
| }); |
|
|
| test("throws when the lease has been overridden (lease-lost guard)", async () => { |
| |
| |
| |
| const t = convexTest(schema, modules); |
| await seedRampConfig(t, { |
| currentTier: 1, |
| pendingRunId: "run-OTHER", |
| pendingRunStartedAt: Date.now(), |
| }); |
| await expect( |
| t.mutation(internal.broadcast.rampRunner._recordWaveSent, { |
| runId: "run-MINE", |
| expectedCurrentTier: 1, |
| newTier: 2, |
| waveLabel: "wave-5", |
| broadcastId: "bc-test", |
| segmentId: "seg-test", |
| assigned: 1500, |
| sentAt: Date.now(), |
| }), |
| ).rejects.toThrow(/lease lost/i); |
| }); |
| }); |
|
|
| |
| |
| |
|
|
| describe("_recordRunOutcome β lease release on failure", () => { |
| test("clears the lease when runId matches the held lease", async () => { |
| const t = convexTest(schema, modules); |
| await seedRampConfig(t, { |
| pendingRunId: "run-Y", |
| pendingRunStartedAt: Date.now(), |
| }); |
| await t.mutation(internal.broadcast.rampRunner._recordRunOutcome, { |
| runId: "run-Y", |
| status: "partial-failure", |
| error: "test failure", |
| }); |
| const row = await loadRow(t); |
| expect(row?.pendingRunId).toBeUndefined(); |
| expect(row?.pendingRunStartedAt).toBeUndefined(); |
| expect(row?.lastRunStatus).toBe("partial-failure"); |
| }); |
|
|
| test("HARD NO-OP when runId differs β does NOT write status/error/active either (P1#2)", async () => { |
| |
| |
| |
| |
| const t = convexTest(schema, modules); |
| await seedRampConfig(t, { |
| pendingRunId: "run-OTHER", |
| pendingRunStartedAt: Date.now(), |
| lastRunStatus: "succeeded", |
| killGateTripped: false, |
| active: true, |
| }); |
| const result = await t.mutation( |
| internal.broadcast.rampRunner._recordRunOutcome, |
| { |
| runId: "run-MINE", |
| status: "partial-failure", |
| error: "should not land", |
| killGate: true, |
| killGateReason: "should not land", |
| deactivate: true, |
| }, |
| ); |
| expect(result.ok).toBe(false); |
| expect(result.reason).toBe("lease-lost"); |
|
|
| const row = await loadRow(t); |
| |
| expect(row?.pendingRunId).toBe("run-OTHER"); |
| |
| expect(row?.lastRunStatus).toBe("succeeded"); |
| expect(row?.lastRunError).toBeUndefined(); |
| expect(row?.killGateTripped).toBe(false); |
| expect(row?.killGateReason).toBeUndefined(); |
| expect(row?.active).toBe(true); |
| }); |
|
|
| test("HARD NO-OP when lease is cleared (operator already took control)", async () => { |
| |
| |
| |
| const t = convexTest(schema, modules); |
| await seedRampConfig(t, { |
| pendingRunId: undefined, |
| lastRunStatus: "partial-failure", |
| }); |
| const result = await t.mutation( |
| internal.broadcast.rampRunner._recordRunOutcome, |
| { |
| runId: "run-DISPLACED", |
| status: "succeeded", |
| error: "should not land", |
| }, |
| ); |
| expect(result.ok).toBe(false); |
| expect(result.reason).toBe("lease-lost"); |
| const row = await loadRow(t); |
| expect(row?.lastRunStatus).toBe("partial-failure"); |
| }); |
| }); |
|
|
| |
| |
| |
|
|
| describe("_recordPendingExport β persists progress + lease-validates", () => { |
| test("persists waveLabel/segmentId/assigned/exportAt when lease is owned", async () => { |
| const t = convexTest(schema, modules); |
| await seedRampConfig(t, { |
| pendingRunId: "run-X", |
| pendingRunStartedAt: Date.now(), |
| }); |
| const result = await t.mutation( |
| internal.broadcast.rampRunner._recordPendingExport, |
| { |
| runId: "run-X", |
| waveLabel: "wave-5", |
| segmentId: "seg-export", |
| assigned: 1500, |
| }, |
| ); |
| expect(result.ok).toBe(true); |
| const row = await loadRow(t); |
| expect(row?.pendingWaveLabel).toBe("wave-5"); |
| expect(row?.pendingSegmentId).toBe("seg-export"); |
| expect(row?.pendingAssigned).toBe(1500); |
| expect(row?.pendingExportAt).toBeTypeOf("number"); |
| }); |
|
|
| test("throws when lease has been force-released (P1#1+P1#4 interaction)", async () => { |
| |
| |
| const t = convexTest(schema, modules); |
| await seedRampConfig(t, { pendingRunId: undefined }); |
| await expect( |
| t.mutation(internal.broadcast.rampRunner._recordPendingExport, { |
| runId: "run-DISPLACED", |
| waveLabel: "wave-5", |
| segmentId: "seg-export", |
| assigned: 1500, |
| }), |
| ).rejects.toThrow(/lease lost/i); |
| }); |
|
|
| test("throws when lease is owned by a different run", async () => { |
| const t = convexTest(schema, modules); |
| await seedRampConfig(t, { |
| pendingRunId: "run-OTHER", |
| pendingRunStartedAt: Date.now(), |
| }); |
| await expect( |
| t.mutation(internal.broadcast.rampRunner._recordPendingExport, { |
| runId: "run-MINE", |
| waveLabel: "wave-5", |
| segmentId: "seg-export", |
| assigned: 1500, |
| }), |
| ).rejects.toThrow(/lease lost/i); |
| }); |
| }); |
|
|
| describe("_recordPendingBroadcast β persists broadcastId + lease-validates", () => { |
| test("persists broadcastId/broadcastAt when lease is owned", async () => { |
| const t = convexTest(schema, modules); |
| await seedRampConfig(t, { |
| pendingRunId: "run-Y", |
| pendingRunStartedAt: Date.now(), |
| pendingWaveLabel: "wave-5", |
| pendingSegmentId: "seg-export", |
| pendingAssigned: 1500, |
| pendingExportAt: Date.now() - 1000, |
| }); |
| const result = await t.mutation( |
| internal.broadcast.rampRunner._recordPendingBroadcast, |
| { runId: "run-Y", broadcastId: "bc-created" }, |
| ); |
| expect(result.ok).toBe(true); |
| const row = await loadRow(t); |
| expect(row?.pendingBroadcastId).toBe("bc-created"); |
| expect(row?.pendingBroadcastAt).toBeTypeOf("number"); |
| |
| expect(row?.pendingWaveLabel).toBe("wave-5"); |
| }); |
|
|
| test("throws when lease has been force-released", async () => { |
| const t = convexTest(schema, modules); |
| await seedRampConfig(t, { pendingRunId: undefined }); |
| await expect( |
| t.mutation(internal.broadcast.rampRunner._recordPendingBroadcast, { |
| runId: "run-DISPLACED", |
| broadcastId: "bc-created", |
| }), |
| ).rejects.toThrow(/lease lost/i); |
| }); |
| }); |
|
|
| describe("_recordWaveSent β clears all pending* progress markers (P1#4)", () => { |
| test("on success, every pending* field is cleared so the next run starts fresh", async () => { |
| const t = convexTest(schema, modules); |
| await seedRampConfig(t, { |
| currentTier: 1, |
| pendingRunId: "run-Z", |
| pendingRunStartedAt: Date.now(), |
| pendingWaveLabel: "wave-5", |
| pendingSegmentId: "seg-test", |
| pendingAssigned: 1500, |
| pendingExportAt: Date.now() - 5000, |
| pendingBroadcastId: "bc-test", |
| pendingBroadcastAt: Date.now() - 1000, |
| }); |
| await t.mutation(internal.broadcast.rampRunner._recordWaveSent, { |
| runId: "run-Z", |
| expectedCurrentTier: 1, |
| newTier: 2, |
| waveLabel: "wave-5", |
| broadcastId: "bc-test", |
| segmentId: "seg-test", |
| assigned: 1500, |
| sentAt: Date.now(), |
| }); |
| const row = await loadRow(t); |
| expect(row?.pendingWaveLabel).toBeUndefined(); |
| expect(row?.pendingSegmentId).toBeUndefined(); |
| expect(row?.pendingAssigned).toBeUndefined(); |
| expect(row?.pendingExportAt).toBeUndefined(); |
| expect(row?.pendingBroadcastId).toBeUndefined(); |
| expect(row?.pendingBroadcastAt).toBeUndefined(); |
| |
| expect(row?.pendingRunId).toBeUndefined(); |
| }); |
| }); |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| describe("clearPartialFailure β confirmNoExport guard (P1#3)", () => { |
| test("REFUSES when any pending* progress marker is set β even with confirmNoExport=true", async () => { |
| |
| |
| |
| const t = convexTest(schema, modules); |
| await seedRampConfig(t, { |
| lastRunStatus: "partial-failure", |
| pendingSegmentId: "seg-stamped", |
| }); |
| await expect( |
| t.mutation(internal.broadcast.rampRunner.clearPartialFailure, { |
| reason: "operator thinks no export happened", |
| confirmNoExport: true, |
| }), |
| ).rejects.toThrow(/refused: pending progress markers present/i); |
| }); |
|
|
| test("clears when no pending* markers AND confirmNoExport=true (truly pre-export failure)", async () => { |
| |
| |
| |
| const t = convexTest(schema, modules); |
| await seedRampConfig(t, { |
| lastRunStatus: "partial-failure", |
| pendingRunId: "run-Q", |
| pendingRunStartedAt: Date.now(), |
| |
| }); |
| const result = await t.mutation( |
| internal.broadcast.rampRunner.clearPartialFailure, |
| { |
| reason: "assignAndExportWave threw on Resend timeout before stamping", |
| confirmNoExport: true, |
| }, |
| ); |
| expect(result.ok).toBe(true); |
| const row = await loadRow(t); |
| expect(row?.lastRunStatus).toMatch(/partial-failure-cleared/); |
| expect(row?.pendingRunId).toBeUndefined(); |
| }); |
|
|
| test("noop when status isn't partial-failure", async () => { |
| const t = convexTest(schema, modules); |
| await seedRampConfig(t, { lastRunStatus: "succeeded" }); |
| const result = await t.mutation( |
| internal.broadcast.rampRunner.clearPartialFailure, |
| { reason: "test", confirmNoExport: true }, |
| ); |
| expect(result.noop).toBe(true); |
| }); |
| }); |
|
|
| |
| |
| |
|
|
| describe("recoverFromPartialFailure β exported-but-not-sent recovery", () => { |
| test("manual-finished: advances tier + records broadcastId from operator-completed wave", async () => { |
| const t = convexTest(schema, modules); |
| await seedRampConfig(t, { |
| currentTier: 1, |
| lastRunStatus: "partial-failure", |
| pendingRunId: "run-stuck", |
| pendingRunStartedAt: Date.now(), |
| }); |
| const result = await t.mutation( |
| internal.broadcast.rampRunner.recoverFromPartialFailure, |
| { |
| recovery: "manual-finished", |
| reason: "Sent wave-5 manually via Resend dashboard after createProLaunchBroadcast threw", |
| broadcastId: "bc-manual-789", |
| segmentId: "seg-manual-456", |
| sentAt: 1700000000000, |
| assigned: 1500, |
| }, |
| ); |
| expect(result.ok).toBe(true); |
| expect(result.recovery).toBe("manual-finished"); |
| expect(result.advancedToTier).toBe(2); |
|
|
| const row = await loadRow(t); |
| expect(row?.currentTier).toBe(2); |
| expect(row?.lastWaveBroadcastId).toBe("bc-manual-789"); |
| expect(row?.lastWaveSegmentId).toBe("seg-manual-456"); |
| expect(row?.lastWaveAssigned).toBe(1500); |
| expect(row?.lastWaveSentAt).toBe(1700000000000); |
| expect(row?.lastRunStatus).toMatch(/succeeded-via-manual-recovery/); |
| expect(row?.pendingRunId).toBeUndefined(); |
| }); |
|
|
| test("manual-finished: rejects when required fields are missing AND no persisted fallback", async () => { |
| const t = convexTest(schema, modules); |
| await seedRampConfig(t, { lastRunStatus: "partial-failure" }); |
| await expect( |
| t.mutation(internal.broadcast.rampRunner.recoverFromPartialFailure, { |
| recovery: "manual-finished", |
| reason: "test", |
| |
| }), |
| ).rejects.toThrow(/missing required field/i); |
| }); |
|
|
| test("manual-finished: AUTO-FILLS from persisted pending* state when operator omits args (P1#4)", async () => { |
| |
| |
| |
| |
| const t = convexTest(schema, modules); |
| await seedRampConfig(t, { |
| currentTier: 1, |
| lastRunStatus: "partial-failure", |
| pendingRunId: "run-died", |
| pendingRunStartedAt: Date.now(), |
| pendingWaveLabel: "wave-5", |
| pendingSegmentId: "seg-persisted", |
| pendingAssigned: 1500, |
| pendingExportAt: Date.now() - 60000, |
| pendingBroadcastId: "bc-persisted", |
| pendingBroadcastAt: Date.now() - 30000, |
| }); |
| const result = await t.mutation( |
| internal.broadcast.rampRunner.recoverFromPartialFailure, |
| { |
| recovery: "manual-finished", |
| reason: "Action timed out; manual send completed via Resend dashboard", |
| sentAt: 1700000000000, |
| |
| }, |
| ); |
| expect(result.ok).toBe(true); |
| expect(result.advancedToTier).toBe(2); |
| expect(result.broadcastId).toBe("bc-persisted"); |
| expect(result.segmentId).toBe("seg-persisted"); |
| expect(result.assigned).toBe(1500); |
| expect(result.waveLabel).toBe("wave-5"); |
| expect(result.usedPersistedFallback).toEqual({ |
| broadcastId: true, |
| segmentId: true, |
| assigned: true, |
| waveLabel: true, |
| }); |
|
|
| const row = await loadRow(t); |
| expect(row?.currentTier).toBe(2); |
| expect(row?.lastWaveBroadcastId).toBe("bc-persisted"); |
| expect(row?.lastWaveSegmentId).toBe("seg-persisted"); |
| expect(row?.lastWaveAssigned).toBe(1500); |
| expect(row?.lastWaveLabel).toBe("wave-5"); |
| expect(row?.lastWaveSentAt).toBe(1700000000000); |
| |
| expect(row?.pendingWaveLabel).toBeUndefined(); |
| expect(row?.pendingSegmentId).toBeUndefined(); |
| expect(row?.pendingAssigned).toBeUndefined(); |
| expect(row?.pendingExportAt).toBeUndefined(); |
| expect(row?.pendingBroadcastId).toBeUndefined(); |
| expect(row?.pendingBroadcastAt).toBeUndefined(); |
| expect(row?.pendingRunId).toBeUndefined(); |
| }); |
|
|
| test("manual-finished: operator override beats persisted fallback when both supplied", async () => { |
| const t = convexTest(schema, modules); |
| await seedRampConfig(t, { |
| currentTier: 1, |
| lastRunStatus: "partial-failure", |
| pendingBroadcastId: "bc-persisted", |
| pendingSegmentId: "seg-persisted", |
| pendingAssigned: 1500, |
| pendingWaveLabel: "wave-5", |
| }); |
| const result = await t.mutation( |
| internal.broadcast.rampRunner.recoverFromPartialFailure, |
| { |
| recovery: "manual-finished", |
| reason: "broadcast was re-created with a different id during manual recovery", |
| broadcastId: "bc-OVERRIDE", |
| segmentId: "seg-OVERRIDE", |
| assigned: 1234, |
| waveLabel: "wave-5-retry", |
| sentAt: 1700000000000, |
| }, |
| ); |
| expect(result.broadcastId).toBe("bc-OVERRIDE"); |
| expect(result.segmentId).toBe("seg-OVERRIDE"); |
| expect(result.assigned).toBe(1234); |
| expect(result.waveLabel).toBe("wave-5-retry"); |
| expect(result.usedPersistedFallback?.broadcastId).toBe(false); |
| expect(result.usedPersistedFallback?.segmentId).toBe(false); |
| expect(result.usedPersistedFallback?.assigned).toBe(false); |
| expect(result.usedPersistedFallback?.waveLabel).toBe(false); |
| }); |
|
|
| test("manual-finished: rejects when sentAt is omitted even with full persisted fallback", async () => { |
| |
| const t = convexTest(schema, modules); |
| await seedRampConfig(t, { |
| lastRunStatus: "partial-failure", |
| pendingWaveLabel: "wave-5", |
| pendingSegmentId: "seg-persisted", |
| pendingAssigned: 1500, |
| pendingBroadcastId: "bc-persisted", |
| }); |
| await expect( |
| t.mutation(internal.broadcast.rampRunner.recoverFromPartialFailure, { |
| recovery: "manual-finished", |
| reason: "test", |
| |
| }), |
| ).rejects.toThrow(/missing required field.*sentAt/i); |
| }); |
|
|
| test("discard-and-rotate: bumps waveLabelOffset + clears ALL pending* state", async () => { |
| |
| |
| |
| |
| const t = convexTest(schema, modules); |
| await seedRampConfig(t, { |
| currentTier: 1, |
| waveLabelOffset: 3, |
| lastRunStatus: "partial-failure", |
| pendingRunId: "run-stuck", |
| pendingRunStartedAt: Date.now(), |
| pendingWaveLabel: "wave-5", |
| pendingSegmentId: "seg-stamped", |
| pendingAssigned: 1500, |
| pendingBroadcastId: "bc-stamped", |
| }); |
| const result = await t.mutation( |
| internal.broadcast.rampRunner.recoverFromPartialFailure, |
| { |
| recovery: "discard-and-rotate", |
| reason: "wave-5 is unrecoverable; discarding the stamped batch", |
| }, |
| ); |
| expect(result.ok).toBe(true); |
| expect(result.recovery).toBe("discard-and-rotate"); |
| expect(result.newWaveLabelOffset).toBe(4); |
| expect(result.nextWaveLabel).toBe("wave-6"); |
|
|
| const row = await loadRow(t); |
| expect(row?.waveLabelOffset).toBe(4); |
| |
| expect(row?.currentTier).toBe(1); |
| expect(row?.lastRunStatus).toMatch(/partial-failure-discarded-rotated/); |
| expect(row?.pendingRunId).toBeUndefined(); |
| |
| expect(row?.pendingWaveLabel).toBeUndefined(); |
| expect(row?.pendingSegmentId).toBeUndefined(); |
| expect(row?.pendingAssigned).toBeUndefined(); |
| expect(row?.pendingBroadcastId).toBeUndefined(); |
| }); |
|
|
| test("noop when status is not partial-failure", async () => { |
| const t = convexTest(schema, modules); |
| await seedRampConfig(t, { lastRunStatus: "succeeded" }); |
| const result = await t.mutation( |
| internal.broadcast.rampRunner.recoverFromPartialFailure, |
| { |
| recovery: "discard-and-rotate", |
| reason: "operator confused, no actual partial-failure", |
| }, |
| ); |
| expect(result.noop).toBe(true); |
| expect(result.currentStatus).toBe("succeeded"); |
| }); |
| }); |
|
|
| |
| |
| |
|
|
| describe("end-to-end: lease prevents duplicate-send race", () => { |
| test("first claim wins; second is rejected without ANY side effect path being taken", async () => { |
| |
| |
| |
| |
| |
| const t = convexTest(schema, modules); |
| await seedRampConfig(t); |
|
|
| const claimA = await t.mutation(internal.broadcast.rampRunner._claimTierForRun, { |
| runId: "run-A", |
| expectedCurrentTier: 0, |
| }); |
| const claimB = await t.mutation(internal.broadcast.rampRunner._claimTierForRun, { |
| runId: "run-B", |
| expectedCurrentTier: 0, |
| }); |
|
|
| |
| expect([claimA.ok, claimB.ok].filter(Boolean).length).toBe(1); |
|
|
| |
| const winner = claimA.ok ? "run-A" : "run-B"; |
| await t.mutation(internal.broadcast.rampRunner._recordWaveSent, { |
| runId: winner, |
| expectedCurrentTier: 0, |
| newTier: 1, |
| waveLabel: "wave-3", |
| broadcastId: "bc-1", |
| segmentId: "seg-1", |
| assigned: 500, |
| sentAt: Date.now(), |
| }); |
|
|
| |
| const row = await loadRow(t); |
| expect(row?.currentTier).toBe(1); |
| expect(row?.pendingRunId).toBeUndefined(); |
| }); |
| }); |
|
|
| |
| |
| |
|
|
| describe("runDailyRamp β pendingRunId guard (PR2 review-fix 7)", () => { |
| test("refuses to schedule pickWaveAction when broadcastRampConfig.pendingRunId is held by a failed run", async () => { |
| |
| |
| |
| |
| |
| |
| const t = convexTest(schema, modules); |
| await seedRampConfig(t, { |
| pendingRunId: "stuck-run-1", |
| pendingRunStartedAt: Date.now() - 5 * 60_000, |
| pendingWaveLabel: "pro-launch-wave-4", |
| }); |
| |
| await t.run(async (ctx) => { |
| await ctx.db.insert("waveRuns", { |
| runId: "stuck-run-1", |
| waveLabel: "pro-launch-wave-4", |
| status: "failed", |
| failureSubstatus: "persist-failed", |
| requestedCount: 100, totalCount: 0, underfilled: false, |
| pushedCount: 0, failedCount: 0, batchSize: 50, |
| createdAt: Date.now() - 5 * 60_000, |
| updatedAt: Date.now() - 5 * 60_000, |
| }); |
| }); |
|
|
| const result = await t.action( |
| internal.broadcast.rampRunner.runDailyRamp, |
| {}, |
| ); |
| |
| expect(result).toMatchObject({ |
| status: "lease-held-by-failed-run", |
| }); |
| expect(result.detail).toContain("stuck-run-1"); |
| expect(result.detail).toContain("persist-failed"); |
|
|
| |
| |
| await t.finishInProgressScheduledFunctions().catch(() => {}); |
| }); |
|
|
| test("schedules pickWaveAction normally when no lease is held", async () => { |
| |
| const t = convexTest(schema, modules); |
| await seedRampConfig(t); |
| const result = await t.action( |
| internal.broadcast.rampRunner.runDailyRamp, |
| {}, |
| ); |
| expect(result.status).toBe("wave-scheduled"); |
| await t.finishInProgressScheduledFunctions().catch(() => {}); |
| }); |
| }); |
|
|
| |
| |
| |
| |
|
|