mirror of
https://github.com/hanzoai/insights.git
synced 2026-10-30 16:15:31 +00:00
The rule table mapped HOG_ -> FN_ ahead of any POSTHOG rule, so POSTHOG_FOO split into POST + HOG_ and came out POSTFN_FOO. Nothing sets that. The live pods set INSIGHTS_SKIP_MIGRATION_CHECKS, which bin/migrate-check reads on the container entrypoint, so the misspelling does not degrade a feature -- it runs migrate --check against a database whose history this tree does not share and the container never starts. Both replacements are measured against the shipped tree rather than chosen: every HOG_ name with a counterpart there is spelled INSIGHTS_ (HOG_QL -> INSIGHTS_QL, HOG_FUNCTION_TEMPLATES -> INSIGHTS_FUNCTION_TEMPLATES, 12 of 12 determinate cases), and FN_ appears in it nowhere. Upstream carries 95 HOG_ tokens and zero FN_ ones, so repairing FN_ back out is exactly the inverse of the bad rule and cannot collide with an upstream name. The rule is fixed for the next convergence, and the tree it already produced is repaired in the same commit -- the codemod is idempotent against its own output, so it cannot undo this itself.
594 lines
20 KiB
TypeScript
594 lines
20 KiB
TypeScript
/**
|
|
* Jest adapter for the test quarantine.
|
|
*
|
|
* Schema contract: tools/insightscli-commands/insightscli_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.INSIGHTS_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, `insights-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)}`)
|
|
}
|