frontend/jest.quarantine.ts
Rama master · réplica de lectura del repositorio original.
/**
* Jest adapter for the test quarantine.
*
* Schema contract: tools/hogli-commands/hogli_commands/quarantine/core.py; this
* file consumes that contract for `runner: "jest"` entries and must not
* reimplement parsing, date handling, or matching beyond what it documents.
* It is the jest sibling of `quarantine/pytest_support.py`.
*
* A jest id is `<repo-relative-file>::<full test name>`, where the name is the
* space-joined ancestor `describe` titles plus the `it`/`test` title (jest's
* `currentTestName`). Installed as a `setupFilesAfterEnv` module, it wraps the
* `describe`/`it`/`test` globals and lifecycle hooks so a matching active entry
* either skips the test (`mode: "skip"`, for hangs/state-polluters) or
* tolerates its body and matching hook failures (`mode: "run"`, since jest has
* no native non-strict xfail, so failures are swallowed instead of failing the
* suite).
*
* Fail-open: any problem reading or applying the file leaves the globals
* untouched and the run proceeds normally.
*
* Granularity note: `describe.each` block titles and `it.each` row titles are
* resolved by jest after collection. Exact row selectors support `mode: "run"`
* through the runtime test name; `mode: "skip"` still requires file, directory,
* or `product:` scope because jest decides skips during collection.
*/
import * as fs from 'fs'
import * as path from 'path'
const SCHEMA_VERSION = 1
// Mirrors PYTEST_RUNNER/JEST_RUNNER in core.py; pytest is also the schema default when an entry omits `runner`.
const PYTEST_RUNNER = 'pytest'
const JEST_RUNNER = 'jest'
const PRODUCT_PREFIX = 'product:'
// __dirname is <repo>/frontend, so the repo root is one directory up.
const REPO_ROOT = path.resolve(__dirname, '..')
// Overridable so the adapter's own runtime test can point a spawned jest worker
// at an isolated fixture file instead of the committed repo-root one.
const QUARANTINE_PATH = process.env.POSTHOG_TEST_QUARANTINE_PATH || path.join(REPO_ROOT, '.test_quarantine.json')
interface RawEntry {
id: string
runner?: string
mode?: string
reason?: string
owner?: string
issue?: string
added?: string
expires?: string
}
export interface QuarantineEntry {
id: string
mode: 'run' | 'skip'
reason: string
owner: string
issue: string
expires: string
}
// --- Pure contract logic (mirrors core.py; unit-tested in jest.quarantine.test.ts) ---
export function productPathPrefix(selector: string): string {
const name = selector.slice(PRODUCT_PREFIX.length)
return `products/${name.replace(/-/g, '_')}/`
}
/** Does `selector` cover `testId`? See the jest grammar in core.py's module docstring. */
export function selectorMatches(selector: string, testId: string): boolean {
if (selector.startsWith(PRODUCT_PREFIX)) {
return testId.startsWith(productPathPrefix(selector))
}
const trimmed = selector.replace(/\/+$/, '')
return (
testId === trimmed ||
testId.startsWith(`${trimmed}/`) ||
testId.startsWith(`${trimmed}::`) ||
testId.startsWith(`${trimmed}[`) ||
testId.startsWith(`${trimmed} `)
)
}
function expandedSelector(selector: string): string {
return selector.startsWith(PRODUCT_PREFIX) ? productPathPrefix(selector) : selector
}
/** The most specific (longest) matching selector wins, so a narrow skip overrides a broad run. */
export function findMatch(entries: QuarantineEntry[], testId: string): QuarantineEntry | null {
let best: QuarantineEntry | null = null
let bestLength = -1
for (const entry of entries) {
if (!selectorMatches(entry.id, testId)) {
continue
}
const length = expandedSelector(entry.id).length
if (length > bestLength) {
best = entry
bestLength = length
}
}
return best
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null
}
function isRawEntry(value: unknown): value is RawEntry {
return isRecord(value) && typeof value.id === 'string' && value.id.length > 0
}
/** Parse v1 quarantine JSON into raw entries. Throws only on invalid JSON (caller fails open). */
export function parseQuarantine(text: string): RawEntry[] {
const data: unknown = JSON.parse(text)
if (!isRecord(data) || data.version !== SCHEMA_VERSION || !Array.isArray(data.entries)) {
return []
}
return data.entries.filter(isRawEntry)
}
/**
* Expiry compares lexicographically, so a malformed date (e.g. `2026-9-05` or free
* text) would sort after today and quarantine a test forever. The contract's
* `date.fromisoformat` rejects those, and this guard keeps the reader in step
* (fail-safe: an unparseable expiry drops the entry rather than activating it).
*/
export function isIsoDate(value: string): boolean {
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
return false
}
const parsed = new Date(`${value}T00:00:00Z`)
return !isNaN(parsed.getTime()) && parsed.toISOString().slice(0, 10) === value
}
/** Unexpired `runner: "jest"` entries only; other runners and expired entries are excluded. */
export function activeJestEntries(entries: RawEntry[], todayIso: string): QuarantineEntry[] {
const active: QuarantineEntry[] = []
for (const entry of entries) {
if ((entry.runner ?? PYTEST_RUNNER) !== JEST_RUNNER) {
continue
}
if (typeof entry.expires !== 'string' || !isIsoDate(entry.expires) || entry.expires < todayIso) {
continue
}
if (entry.mode !== undefined && entry.mode !== 'run' && entry.mode !== 'skip') {
continue
}
active.push({
id: entry.id,
mode: entry.mode === 'skip' ? 'skip' : 'run',
reason: entry.reason ?? '',
owner: entry.owner ?? '',
issue: entry.issue ?? '',
expires: entry.expires,
})
}
return active
}
/** Repo-root-relative, forward-slash path for a jest `testPath` (absolute). */
export function repoRelativePath(absolutePath: string, repoRoot: string = REPO_ROOT): string {
return path.relative(repoRoot, absolutePath).split(path.sep).join('/')
}
// --- Runtime wiring: read the file, then wrap the jest globals ---
let activeEntries: QuarantineEntry[] = []
const describeStack: string[] = []
interface Decision {
mode: 'run' | 'skip'
label: string
testId: string
}
function todayIso(): string {
return new Date().toISOString().slice(0, 10)
}
function loadActiveEntries(): QuarantineEntry[] {
let text: string
try {
text = fs.readFileSync(QUARANTINE_PATH, 'utf-8')
} catch {
return [] // A missing file means quarantine is simply off.
}
let raw: RawEntry[]
try {
raw = parseQuarantine(text)
} catch {
return [] // Malformed file: fail open rather than break collection.
}
return activeJestEntries(raw, todayIso())
}
function stringifyName(name: unknown): string {
if (typeof name === 'function') {
return (name as { name?: string }).name ?? ''
}
return String(name)
}
// `expect` may not be initialized yet while setup files run, so read defensively.
function jestState(): ReturnType<typeof expect.getState> | undefined {
try {
return expect.getState()
} catch {
return undefined
}
}
function currentTestPath(): string | undefined {
return jestState()?.testPath ?? undefined
}
function currentTestName(): string | undefined {
const name = jestState()?.currentTestName
return typeof name === 'string' && name.length > 0 ? name : undefined
}
function toDecision(entry: QuarantineEntry, testId: string): Decision {
const attribution = entry.issue || entry.owner || 'no owner'
return {
mode: entry.mode,
label: `${testId}: quarantined until ${entry.expires}: ${entry.reason} (${attribution})`,
testId,
}
}
// testPath is constant for a whole file run, so cache its repo-relative form
// rather than recomputing path.relative on every test declaration.
let cachedTestPath: string | undefined
let cachedRelativePath = ''
function relativeTestPath(): string | null {
const testPath = currentTestPath()
if (testPath === undefined) {
return null
}
if (testPath !== cachedTestPath) {
cachedTestPath = testPath
cachedRelativePath = repoRelativePath(testPath)
}
return cachedRelativePath
}
function decideFor(testId: string): Decision | null {
const entry = findMatch(activeEntries, testId)
return entry ? toDecision(entry, testId) : null
}
function decideForTest(name: unknown): Decision | null {
const relativePath = relativeTestPath()
if (relativePath === null) {
return null
}
return decideFor(`${relativePath}::${[...describeStack, stringifyName(name)].join(' ')}`)
}
function decideForRunningTest(): Decision | null {
const relativePath = relativeTestPath()
const name = currentTestName()
if (relativePath === null || name === undefined) {
return null
}
return decideFor(`${relativePath}::${name}`)
}
function decideForFile(): Decision | null {
const relativePath = relativeTestPath()
return relativePath === null ? null : decideFor(relativePath)
}
function decideForScope(): Decision | null {
const relativePath = relativeTestPath()
if (relativePath === null) {
return null
}
const scopeId = describeStack.length > 0 ? `${relativePath}::${describeStack.join(' ')}` : relativePath
return decideFor(scopeId)
}
function errorText(error: unknown): string {
if (error instanceof Error) {
return error.stack ?? error.message
}
return String(error)
}
function warn(message: string): void {
// eslint-disable-next-line no-console
console.warn(message)
}
function warnSkip(decision: Decision): void {
warn(`[quarantine] skipping ${decision.label}`)
}
function warnTolerated(decision: Decision, error: unknown): void {
recordToleratedFailure(decision.testId)
warn(`[quarantine] tolerated failure in ${decision.label}\n${errorText(error)}`)
}
const recordedToleratedFailures = new Set<string>()
function recordToleratedFailure(testId: string): void {
const outputDirectory = process.env.JEST_JUNIT_OUTPUT_DIR
if (!outputDirectory || recordedToleratedFailures.has(testId)) {
return
}
try {
fs.mkdirSync(outputDirectory, { recursive: true })
fs.appendFileSync(
path.join(outputDirectory, `posthog-jest-quarantine-${process.pid}.jsonl`),
`${JSON.stringify({ test_id: testId })}\n`
)
recordedToleratedFailures.add(testId)
} catch {
// Telemetry is best-effort; quarantine enforcement must still work without it.
}
}
function isThenable(value: unknown): value is PromiseLike<unknown> {
return isRecord(value) && typeof (value as { then?: unknown }).then === 'function'
}
/** Run a callback swallowing any thrown error or rejected promise (jest's non-strict xfail analog). */
function tolerate(fn: (...args: unknown[]) => unknown, decision: Decision): (...args: unknown[]) => unknown {
return function (this: unknown, ...args: unknown[]): unknown {
try {
const result = fn.apply(this, args)
if (isThenable(result)) {
return result.then(undefined, (error: unknown) => {
warnTolerated(decision, error)
return undefined
})
}
return result
} catch (error) {
warnTolerated(decision, error)
return undefined
}
}
}
function runDoneStyle(
fn: jest.ProvidesCallback | ((...args: unknown[]) => unknown),
args: unknown[],
decision: Decision,
done: jest.DoneCallback,
thisArg: unknown
): void {
let finished = false
const finish = (): void => {
if (!finished) {
finished = true
done()
}
}
const tolerantDone = function (reason?: string | Error): void {
if (reason !== undefined) {
warnTolerated(decision, reason)
}
finish()
} as jest.DoneCallback
tolerantDone.fail = function (reason?: string | Error): void {
warnTolerated(decision, reason ?? new Error('done.fail() called'))
finish()
}
try {
const doneStyle = fn as (...callbackArgs: unknown[]) => unknown
doneStyle.call(thisArg, ...args, tolerantDone)
} catch (error) {
warnTolerated(decision, error)
finish()
}
}
/**
* Tolerate an `it`/`test` body or hook, handling the done-callback style that
* `tolerate` can't (it swallows via the promise/throw path).
*/
function tolerateProvidesWhen(
fn: jest.ProvidesCallback | undefined,
getDecision: () => Decision | null
): jest.ProvidesCallback | undefined {
if (typeof fn !== 'function') {
return fn
}
if (fn.length >= 1) {
const doneStyle = fn as (done: jest.DoneCallback) => void
return function (this: unknown, done: jest.DoneCallback): void {
const decision = getDecision()
if (decision === null || decision.mode !== 'run') {
doneStyle.call(this, done)
return
}
runDoneStyle(doneStyle, [], decision, done, this)
}
}
return function (this: unknown): unknown {
const decision = getDecision()
if (decision === null || decision.mode !== 'run') {
return (fn as () => unknown).call(this)
}
return tolerate(fn as () => unknown, decision).call(this)
} as jest.ProvidesCallback
}
/** Copy every own member (skip/only/todo/each/…) from a jest global onto its wrapper. */
function copyMembers(target: object, source: object): void {
for (const key of Object.getOwnPropertyNames(source)) {
if (key === 'length' || key === 'name' || key === 'prototype') {
continue
}
const descriptor = Object.getOwnPropertyDescriptor(source, key)
if (descriptor) {
Object.defineProperty(target, key, descriptor)
}
}
}
type EachFactory = (name: string, fn: (...args: unknown[]) => unknown, timeout?: number) => void
type EachEntry = (...args: unknown[]) => EachFactory
function eachArgumentCount(eachArgs: unknown[]): number | null {
const [table, ...templateValues] = eachArgs
if (templateValues.length > 0) {
return 1
}
if (!Array.isArray(table) || table.length === 0) {
return null
}
const counts = table.map((row) => (Array.isArray(row) ? row.length : 1))
return counts.every((count) => count === counts[0]) ? counts[0] : null
}
function tolerateEachWhen(
fn: (...args: unknown[]) => unknown,
getDecision: () => Decision | null,
argumentCount: number | null
): (...args: unknown[]) => unknown {
const wrapped = function (this: unknown, ...args: unknown[]): unknown {
const decision = getDecision()
if (decision === null || decision.mode !== 'run') {
return fn.apply(this, args)
}
if (argumentCount !== null && fn.length > argumentCount && args.length === argumentCount + 1) {
const done = args[argumentCount]
if (typeof done === 'function') {
runDoneStyle(fn, args.slice(0, argumentCount), decision, done as jest.DoneCallback, this)
return undefined
}
}
return tolerate(fn, decision).apply(this, args)
}
// jest-each compares callback arity with row width to decide whether to inject `done`.
Object.defineProperty(wrapped, 'length', { value: fn.length })
return wrapped
}
/** `.each`: collection-time file scopes can skip; runtime row identities can tolerate failures. */
function wrapEach(base: jest.It, skipBase: jest.It): jest.Each {
const wrapped = (...eachArgs: unknown[]): EachFactory => {
const factory = (base.each as unknown as EachEntry)(...eachArgs)
const decision = decideForFile()
if (decision?.mode === 'skip') {
warnSkip(decision)
return (skipBase.each as unknown as EachEntry)(...eachArgs)
}
const argumentCount = eachArgumentCount(eachArgs)
return (name: string, fn: (...args: unknown[]) => unknown, timeout?: number): void =>
factory(name, tolerateEachWhen(fn, decideForRunningTest, argumentCount), timeout)
}
return wrapped as unknown as jest.Each
}
function wrapIt(original: jest.It, wrapConcurrent = true): jest.It {
const run = (source: jest.It, name: string, fn?: jest.ProvidesCallback, timeout?: number): void => {
const decision = decideForTest(name)
if (decision?.mode === 'skip') {
warnSkip(decision)
original.skip(name, fn, timeout)
return
}
source(name, tolerateProvidesWhen(fn, decideForRunningTest), timeout)
}
const wrapped = ((name: string, fn?: jest.ProvidesCallback, timeout?: number): void => {
run(original, name, fn, timeout)
}) as jest.It
copyMembers(wrapped, original)
const wrappedOnly = ((name: string, fn?: jest.ProvidesCallback, timeout?: number): void => {
run(original.only, name, fn, timeout)
}) as jest.It
copyMembers(wrappedOnly, original.only)
wrappedOnly.each = wrapEach(original.only, original.skip)
wrapped.only = wrappedOnly
if (wrapConcurrent && typeof original.concurrent === 'function') {
wrapped.concurrent = wrapIt(original.concurrent, false)
}
wrapped.each = wrapEach(original, original.skip)
return wrapped
}
type DescribeName = Parameters<jest.Describe>[0]
type DescribeBody = Parameters<jest.Describe>[1]
function wrapDescribeVariant(variant: jest.Describe): jest.Describe {
const wrapped = ((name: DescribeName, fn: DescribeBody): void => {
variant(name, function (this: unknown): void {
describeStack.push(stringifyName(name))
try {
fn.call(this)
} finally {
describeStack.pop()
}
})
}) as jest.Describe
copyMembers(wrapped, variant)
return wrapped
}
function wrapDescribe(original: jest.Describe): jest.Describe {
const wrapped = wrapDescribeVariant(original)
// `.only`/`.skip` also nest, so keep the stack correct through them.
wrapped.only = wrapDescribeVariant(original.only)
wrapped.skip = wrapDescribeVariant(original.skip)
return wrapped
}
function wrapLifecycle(
original: jest.Lifecycle,
getDecision: () => Decision | null,
useDeclarationDecision = false
): jest.Lifecycle {
return ((fn: jest.ProvidesHookCallback, timeout?: number): void => {
const declarationDecision = useDeclarationDecision ? getDecision() : null
original(
tolerateProvidesWhen(
fn as jest.ProvidesCallback,
() => declarationDecision ?? getDecision()
) as jest.ProvidesHookCallback,
timeout
)
}) as jest.Lifecycle
}
function install(entries: QuarantineEntry[]): void {
if (entries.length === 0) {
return
}
const globals = globalThis as typeof globalThis & {
describe: jest.Describe
it: jest.It
test: jest.It
beforeEach: jest.Lifecycle
afterEach: jest.Lifecycle
beforeAll: jest.Lifecycle
afterAll: jest.Lifecycle
}
if (typeof globals.describe === 'function') {
globals.describe = wrapDescribe(globals.describe)
}
if (typeof globals.it === 'function') {
globals.it = wrapIt(globals.it)
}
if (typeof globals.test === 'function') {
globals.test = wrapIt(globals.test)
}
if (typeof globals.beforeEach === 'function') {
globals.beforeEach = wrapLifecycle(globals.beforeEach, decideForRunningTest)
}
if (typeof globals.afterEach === 'function') {
globals.afterEach = wrapLifecycle(globals.afterEach, decideForRunningTest)
}
if (typeof globals.beforeAll === 'function') {
globals.beforeAll = wrapLifecycle(globals.beforeAll, decideForScope, true)
}
if (typeof globals.afterAll === 'function') {
globals.afterAll = wrapLifecycle(globals.afterAll, decideForScope, true)
}
}
try {
activeEntries = loadActiveEntries()
install(activeEntries)
} catch (error) {
warn(`[quarantine] disabled: ${errorText(error)}`)
}