IStore interface
All store packages implement a common IStore interface from @flaky-tests/core. This is what makes backends swappable — the rest of the system never depends on a concrete store implementation, only on this contract.
Interface definition
Section titled “Interface definition”import type { IStore, InsertRunInput, UpdateRunInput, InsertFailureInput, GetNewPatternsOptions, GetRecentRunsOptions, FlakyPattern, RecentRun,} from '@flaky-tests/core'interface IStore { migrate(): Promise<void> insertRun(input: InsertRunInput): Promise<void> updateRun(runId: string, input: UpdateRunInput): Promise<void> insertFailure(input: InsertFailureInput): Promise<void> insertFailures(inputs: readonly InsertFailureInput[]): Promise<void> getNewPatterns(options?: GetNewPatternsOptions): Promise<FlakyPattern[]> getRecentRuns(options: GetRecentRunsOptions): Promise<RecentRun[]> close(): Promise<void>}Every method is async so adapters backed by an HTTP client (Turso, Supabase, Postgres) share the same signature as the synchronous SQLite adapter.
migrate()
Section titled “migrate()”Creates tables and runs any pending schema migrations. Migrations are versioned and idempotent — the sqlite and turso adapters track applied versions in a _flaky_tests_migrations table and skip anything already applied, so calling migrate() on every startup is safe and cheap. New schema versions are picked up automatically when the adapter package is upgraded.
Error contract
Section titled “Error contract”Implementations must wrap any driver-level error thrown by a public method in a StoreError with { package, method, message, cause } set. The cause field preserves the original stack for inspection. Validation errors raised by arktype (surfaced as ValidationError) propagate unwrapped so bad input stays distinguishable from a downstream driver failure.
import { StoreError } from '@flaky-tests/core'
try { await store.insertRun(input)} catch (error) { if (error instanceof StoreError) { console.error(error.package) // e.g. '@flaky-tests/store-supabase' console.error(error.method) // e.g. 'insertRun' console.error(error.cause) // original driver error } throw error}StoreError.message is formatted as [<package>] <method>: <message> so logs are self-describing without extra plumbing.
Retry behavior
Section titled “Retry behavior”Network-backed adapters (turso, supabase, postgres) automatically retry transient read failures using exponential backoff with jitter via withRetry() from @flaky-tests/core. Only these failures are retried:
- Network transport errors:
ECONNRESET,ECONNREFUSED,ETIMEDOUT,ENETUNREACH,EAI_AGAIN - Timeout messages (
timed out,socket hang up,fetch failed) - HTTP 5xx responses
HTTP 4xx responses, validation errors, and anything else propagate immediately so logic bugs are never masked by a retry loop. The default schedule is 3 attempts with a 100 ms base delay doubling each attempt; aborting the query’s AbortSignal cancels any pending backoff sleep.
Cancellation with AbortSignal
Section titled “Cancellation with AbortSignal”Both read methods — getNewPatterns(options?) and getRecentRuns(options) — accept an optional signal?: AbortSignal. When the signal aborts, the pending promise rejects with a native AbortError (the signal’s reason), even for drivers that cannot cancel the underlying request. In that case the in-flight query still resolves in the background but its result is discarded.
const controller = new AbortController()setTimeout(() => controller.abort(), 5_000)
try { const patterns = await store.getNewPatterns({ windowDays: 7, threshold: 2, signal: controller.signal, })} catch (error) { if ((error as Error).name === 'AbortError') { // query was cancelled — handle as expected } else { throw error }}signal is a TypeScript-only extension on the options type; arktype drops it during validation and adapters read it straight off the caller’s options object.
Registering with the plugin registry
Section titled “Registering with the plugin registry”Every adapter registers a descriptor at module import time so the dispatcher in @flaky-tests/core can find it by name. There is no hardcoded list of adapters anywhere in the dispatcher.
import { definePlugin, type Config, type IStore } from '@flaky-tests/core'
export const myStorePlugin = definePlugin({ name: 'store-mything', create(config: Config): IStore { return new MyStore(/* unpack config */) },})See the custom stores guide for a complete walkthrough.
InsertRunInput
Section titled “InsertRunInput”interface InsertRunInput { runId: string // stable id for the run (UUID) startedAt: string // ISO 8601 timestamp project?: string | null // optional project slug for multi-project stores gitSha?: string | null gitDirty?: boolean | null runtimeVersion?: string | null testArgs?: string | null}UpdateRunInput
Section titled “UpdateRunInput”interface UpdateRunInput { endedAt?: string // ISO 8601 timestamp durationMs?: number status?: 'pass' | 'fail' totalTests?: number passedTests?: number failedTests?: number errorsBetweenTests?: number}InsertFailureInput
Section titled “InsertFailureInput”interface InsertFailureInput { runId: string testFile: string // relative path to test file testName: string // full test name including describe blocks failureKind: FailureKind errorMessage?: string | null errorStack?: string | null durationMs?: number | null failedAt: string // ISO 8601 timestamp}FailureKind
Section titled “FailureKind”type FailureKind = 'assertion' | 'timeout' | 'uncaught' | 'unknown'Determined by categorizeError(error) in @flaky-tests/core.
GetNewPatternsOptions
Section titled “GetNewPatternsOptions”interface GetNewPatternsOptions { /** * Length of each detection window in days. * The current window is [now - windowDays, now]. * The prior window is [now - 2*windowDays, now - windowDays]. * Default: 7 */ windowDays?: number /** * Minimum failures in the current window to flag as a new pattern. * Default: 2 */ threshold?: number /** * Filter to a single project. Pass `null` / leave undefined to match * rows whose `project` column is NULL. */ project?: string | null /** Abort the query when this signal aborts; rejects with `AbortError`. */ signal?: AbortSignal}GetRecentRunsOptions
Section titled “GetRecentRunsOptions”interface GetRecentRunsOptions { limit: number /** Filter to a single project (same semantics as `GetNewPatternsOptions.project`). */ project?: string | null /** Abort the query when this signal aborts; rejects with `AbortError`. */ signal?: AbortSignal}FlakyPattern
Section titled “FlakyPattern”interface FlakyPattern { testFile: string testName: string recentFails: number // failures in the current window priorFails: number // failures in the prior window (always 0 for new patterns) failureKinds: FailureKind[] // distinct kinds seen in the current window lastErrorMessage: string | null lastErrorStack: string | null lastFailed: string // ISO 8601 timestamp of most recent failure}RecentRun
Section titled “RecentRun”interface RecentRun { runId: string project: string | null startedAt: string endedAt: string | null durationMs: number | null status: 'pass' | 'fail' | null totalTests: number | null passedTests: number | null failedTests: number | null errorsBetweenTests: number | null gitSha: string | null gitDirty: boolean | null}Contract suite
Section titled “Contract suite”@flaky-tests/core/test-helpers exports a shared runContractTests(label, makeStore) that exercises scenarios covering the full IStore surface — round-trip, validation, window-edge, threshold, multi-pattern sorting, distinct kinds, last-error capture, infra-blowup filter, AbortSignal cancellation, StoreError wrapping, and close() idempotency. Every built-in adapter passes it; custom adapters should invoke it too.
import { runContractTests } from '@flaky-tests/core/test-helpers'runContractTests('mything', () => new MyStore({ /* test config */ }))