test(sema): censo de packs — cada regla puede disparar, y dispara donde el morfo dice

71 ficheros de pack (no el barrel: S-36 lo dejo incompleto) x 175 morfos,
cuatro invariantes mecanicos por regla de cascada:

1. TARGET — la parte del selector contra target + allowedTargets del
   evento. El estampado solo visita esas; apuntar a otra es la clase
   TextArea (S-12): un selector que ningun nodo satisface jamas.
2. CANAL — la firma declarada contra la activacion efectiva, con la
   precedencia completa (regla ?? evento ?? familia): D.8 prohibe la
   regla inerte que se lee como si hiciera algo.
3. CABEZA DEL TUNING — la ley de nombrado del catalogo aplicada al sitio
   de APLICACION, que es la mitad que el guard de claves no ve (S-39).
4. EMISION — alcanzabilidad POR REGLA contra los emisores reales: soma +
   eidos + layers, comentarios fuera (menu-dial pasaba por un doc-block
   mientras emite desde su wrapper de eidos; una regla por familia, como
   la del tooltip, era invisible para un censo por nombre de evento).

Nacio en rojo: reprodujo S-12/S-14/S-30/S-37/S-38/S-39 de la auditoria
2026-08-05 y destapo dos clases nuevas — las 12 redirecciones legitimas
que el morfo no declaraba (hoy allowedTargets, en verde sin excepciones)
y el contrato muerto del tooltip: 3 eventos que nadie emite y una regla
SILENT que nunca casa, silencio accidental en vez del declarado.

Los pendientes de decision quedan como excepciones FIRMADAS con hallazgo
y fecha (S-38 canal, S-39 tunings, S-14 gradient-picker, tooltip): el
waiver registra, no bendice. El guard caza toda deriva nueva desde ya, y
un test de honestidad borra la excepcion que deje de nombrar un caso vivo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-dir-prefs
dev 2 months ago
parent 0c1d945bae
commit 2f63525da7

@ -0,0 +1,545 @@
/**
* Census guard — a cascade rule must be able to FIRE, and fire WHERE the morfo
* says it will.
*
* WHY THIS EXISTS. A pack rule is a selector plus a signature. Both halves can
* be perfectly typed and still never meet the DOM: `semaSelector` proves the
* PART exists and the EVENT NAME exists, but nothing proved they belong
* together. The audit of 2026-08-05 measured the cost of that gap — 17 of 212
* rules across 13 packs either never matched, wrote into a channel their family
* never activates, or named a family in their tuning that was not the family
* they modified. None of it was visible: 256 sema tests passed with every one
* of them present, because a rule that never matches simply falls back to the
* family base and the component keeps making a sound. The wrong one.
*
* The class had already reincided twice inside a single file
* (`components/navigation-menu.ts`, whose own header documents the first
* repair while the two rules added in the same commit reintroduced the defect),
* which is the signature of a missing guard rather than a careless author.
*
* WHAT IT CHECKS, and why each one is mechanical:
*
* 1. TARGET — the part a rule selects must be the part the morfo declares as
* the event's `target`. That is where the runtime stamps `data-event*`, so
* any other part yields a selector no node can satisfy.
* 2. CHANNEL — a rule that declares a `sound` / `haptic` signature must have
* that channel in its effective activation (`rule.channels` when present,
* otherwise the family's `activeChannels`). D.8: declaring a signature the
* activation does not include leaves an INERT rule that reads as if it did
* something.
* 3. TUNING HEAD — `soundTuning('commit.subtle')` applied to a `signal` event
* lies about the base it modifies. The naming law lives in `sounds.ts` and
* `sounds-grammar.test.ts` guards the KEYS; this guards the APPLICATION,
* which is the half that can drift. Source-level on purpose: the key is a
* literal an author types, and `soundTuning()` returns a resolved override
* with no memory of it.
* 4. EMISSION — an event a rule tunes must be emitted by some provider.
* Otherwise the rule, its comment and the component README all describe a
* perception that never happens.
*
* Every exception is signed with the reason it earned the pass. Growing those
* lists is a decision, not a convenience.
*/
import { readFileSync, readdirSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { describe, expect, it } from 'vitest';
import { partMarkerAttr } from '../morfo/compile';
import type { PartRef } from '../types';
import type { Morfo, MorfoEvent } from '../morfo/types';
import type { SemaChannelId } from './channels';
import { SEMA_MAP, type Sema } from './sema-map';
import type { SemaFamily } from './types';
// ── Catalogue ─────────────────────────────────────────────────────────────
const packModules = import.meta.glob('./components/*.ts', { eager: true }) as Record<
string,
Record<string, unknown>
>;
const morfoModules = import.meta.glob('../morfo/components/*.ts', { eager: true }) as Record<
string,
Record<string, unknown>
>;
function isSema(value: unknown): value is Sema {
return (
typeof value === 'object' &&
value !== null &&
typeof (value as Sema).name === 'string' &&
Array.isArray((value as Sema).cascade)
);
}
function isMorfo(value: unknown): value is Morfo {
return (
typeof value === 'object' &&
value !== null &&
typeof (value as Morfo).kebab === 'string' &&
Array.isArray((value as Morfo).parts)
);
}
interface PackEntry {
/** File basename without extension — the pack's source of record. */
readonly file: string;
readonly pack: Sema;
}
/** Every pack FILE, not the barrel: the barrel is known to be incomplete. */
const PACKS: readonly PackEntry[] = Object.entries(packModules)
.filter(([path]) => !path.endsWith('/index.ts'))
.flatMap(([path, mod]) => {
const file = path.replace(/^.*\//, '').replace(/\.ts$/, '');
return Object.values(mod)
.filter(isSema)
.map((pack) => ({ file, pack }));
})
.sort((a, b) => a.file.localeCompare(b.file));
const MORFOS: readonly Morfo[] = Object.values(morfoModules)
.flatMap((mod) => Object.values(mod))
.filter(isMorfo);
/** marker attr → the (morfo, part) pairs that emit it. */
const MARKER_INDEX = new Map<string, { morfo: Morfo; part: string }[]>();
for (const morfo of MORFOS) {
for (const part of morfo.parts) {
const marker = partMarkerAttr(morfo.kebab, part.kebab);
const bucket = MARKER_INDEX.get(marker) ?? [];
bucket.push({ morfo, part: part.kebab });
MARKER_INDEX.set(marker, bucket);
}
}
// ── Selector parsing ──────────────────────────────────────────────────────
const SEGMENT_RE = /\[([a-zA-Z][\w-]*)(?:([~^|$*]?)="((?:[^"\\]|\\.)*)")?\]/g;
interface ParsedSelector {
/** The bare `[data-*]` part marker — the anchor of the compound. */
readonly marker: string | undefined;
readonly eventName: string | undefined;
readonly eventNamePrefix: string | undefined;
readonly eventFamily: SemaFamily | undefined;
readonly eventIntent: string | undefined;
}
function parseSelector(selector: string): ParsedSelector {
// An `ancestor` is joined with a space; the PART is always the last compound.
const compound = selector.trim().split(/\s+/).pop() ?? '';
let marker: string | undefined;
let eventName: string | undefined;
let eventNamePrefix: string | undefined;
let eventFamily: SemaFamily | undefined;
let eventIntent: string | undefined;
for (const match of compound.matchAll(SEGMENT_RE)) {
const [, attr, operator, value] = match;
if (value === undefined) {
if (attr.startsWith('data-') && marker === undefined) marker = attr;
continue;
}
if (attr === 'data-event') {
if (operator === '^') eventNamePrefix = value;
else eventName = value;
} else if (attr === 'data-event-family') {
eventFamily = value as SemaFamily;
} else if (attr === 'data-event-intent') {
eventIntent = value;
}
}
return { marker, eventName, eventNamePrefix, eventFamily, eventIntent };
}
function resolveMarker(marker: string, ownKebab: string): { morfo: Morfo; part: string } | undefined {
const bucket = MARKER_INDEX.get(marker);
if (!bucket || bucket.length === 0) return undefined;
// A marker can be ambiguous (`data-card-header` = card/header or
// card-header/provider). The pack's own morfo wins — that is the one the
// typed builder was called with.
return bucket.find((entry) => entry.morfo.kebab === ownKebab) ?? bucket[0];
}
function eventsOf(morfo: Morfo): readonly MorfoEvent[] {
return (morfo.events ?? []) as readonly MorfoEvent[];
}
/**
* The parts a rule may legitimately select for this event: the canonical
* `target` plus whatever `allowedTargets` declares. A provider redirects the
* stamp with `targetOverride` for repeated parts (the pressed day, the clicked
* page); the morfo is the only place that can distinguish that from drift.
*/
function stampPartsOf(event: MorfoEvent): readonly string[] {
const target = event.semantic.target as PartRef | undefined;
const canonical = target && typeof target === 'object' ? [target.target] : [];
const allowed = (event.semantic.allowedTargets ?? []).map((ref) => ref.target);
return [...canonical, ...allowed];
}
/** Events of `morfo` a rule's matchers can reach. */
function candidateEvents(morfo: Morfo, parsed: ParsedSelector): readonly MorfoEvent[] {
return eventsOf(morfo).filter((event) => {
if (parsed.eventName !== undefined && event.name !== parsed.eventName) return false;
if (parsed.eventNamePrefix !== undefined && !event.name.startsWith(parsed.eventNamePrefix)) {
return false;
}
if (parsed.eventFamily !== undefined) {
const allowed = [
event.semantic.family,
...(event.semantic.allowedFamilies ?? [])
] as readonly SemaFamily[];
if (!allowed.includes(parsed.eventFamily)) return false;
}
return true;
});
}
/** The families one candidate event can stamp — `eventFamily` pins it when present. */
function familiesOf(event: MorfoEvent, parsed: ParsedSelector): readonly SemaFamily[] {
if (parsed.eventFamily !== undefined) return [parsed.eventFamily];
return [
event.semantic.family,
...(event.semantic.allowedFamilies ?? [])
] as readonly SemaFamily[];
}
interface RuleEntry {
readonly pack: PackEntry;
readonly index: number;
readonly rule: Sema['cascade'][number];
readonly parsed: ParsedSelector;
readonly id: string;
}
const RULES: readonly RuleEntry[] = PACKS.flatMap((pack) =>
pack.pack.cascade.map((rule, index) => ({
pack,
index,
rule,
parsed: parseSelector(rule.selector),
id: `${pack.file}#${index}`
}))
);
// ── 1 · Target agreement ──────────────────────────────────────────────────
/**
* Rules whose selected part is neither the declared `target` nor one of the
* event's `allowedTargets`. Empty on purpose: a legitimate redirection is
* declared in the morfo, where soma, sema and eidos can all read it — not
* excused here, where only this file would know.
*/
const TARGET_EXCEPTIONS: Record<string, string> = {};
// ── 2 · Channel activation ────────────────────────────────────────────────
/** Channel ids a cascade rule can declare a signature for. */
const RULE_CHANNELS = ['sound', 'haptic'] as const satisfies readonly SemaChannelId[];
/**
* PENDING AUTHOR DECISION (S-30/S-38, audited 2026-08-05, waived 2026-08-06):
* five rules write a haptic signature their family never activates — inert
* since birth. D.8 (signed) gives the two exits: widen with `channels` plus a
* written perceptual reason (the slider / css-field / drag-drop pattern), or
* drop the block. Widening also requires COMPLETING the firma: shift/emerge
* have no `base.haptic`, and `applyOverride` clones a partial as if it were
* whole (S-31) — `{kind:'tick'}` alone ends in `navigator.vibrate(NaN)`.
* These waivers record the finding; they do not bless it.
*/
const CHANNEL_EXCEPTIONS: Record<string, string> = {
'dialog#7': 'S-38 pending: alertdialog pulse on emerge — widen with channels, or drop',
'dialog#9': 'S-38 pending: sheet appearance tap on emerge — widen with channels, or drop',
'editable#0': 'S-38 pending: enter-mode tick on shift — widen + complete firma, or drop',
'stepper#0': 'S-38 pending: step tick on shift — widen + complete firma, or drop',
'timeline#0': 'S-38 pending: feed-reveal tick on emerge, with no user gesture behind it'
};
// ── 3 · Tuning head ───────────────────────────────────────────────────────
/**
* PENDING AUTHOR DECISION (S-39, audited 2026-08-05, waived 2026-08-06): ten
* applications import a `commit.*` ladder into `shift` / `signal` / `contact`
* events, so the key's head lies about the base it modifies — and five
* validation warnings resolve 8–13× below the family that exists to claim
* attention. The candidate fix keeps today's levels (family-true keys with the
* gain each site resolves NOW, recalibration deferred to the perceptual
* snapshot); chronos stays waived regardless — write-excluded by policy.
*/
const TUNING_HEAD_EXCEPTIONS: Record<string, string> = {
'calendar:shift-navigate': 'S-39 pending: commit.subtle on shift',
'chronos:shift-navigate': 'S-39 + write-excluded by project policy',
'css-field:signal-warn-invalid': 'S-39 pending: commit.soft on signal',
'editable:shift-enter-mode': 'S-39 pending: commit.subtle on shift',
'file-upload:trigger-picker': 'S-39 pending: commit.subtle on contact',
'file-upload:signal-warn-reject': 'S-39 pending: commit.soft on signal',
'password-field:signal-notify-caps-state': 'S-39 pending: commit.subtle on signal',
'stepper:shift-step': 'S-39 pending: commit.subtle on shift',
'tags-input:signal-warn-reject': 'S-39 pending: commit.soft on signal',
'textarea:signal-warn-count-overflow': 'S-39 pending: commit.subtle on signal'
};
/**
* `{eventName → tuning key}` per pack file, read from the source: the key is a
* literal the author types and `soundTuning()` forgets it on return.
*/
function tuningApplications(file: string): { eventName: string; key: string }[] {
const source = readFileSync(join(__dirname, 'components', `${file}.ts`), 'utf-8');
const out: { eventName: string; key: string }[] = [];
// Rules are object literals that always open with `selector:`; slicing at
// each `selector:` gives one rule's text without needing a parser.
const blocks = source.split(/\bselector:/).slice(1);
for (const block of blocks) {
const eventName = block.match(/eventName:\s*'([^']+)'/)?.[1];
const key = block.match(/soundTuning\(\s*'([^']+)'/)?.[1];
if (eventName && key) out.push({ eventName, key });
}
return out;
}
// ── 4 · Emission ──────────────────────────────────────────────────────────
const SOMA_DIR = join(__dirname, '..', 'soma', 'components');
/** Shared emission helpers — `list-selection.ts` names the event for every
* listbox-shaped component, so the literal lives outside the component dir. */
const SOMA_LAYERS_DIR = join(__dirname, '..', 'soma', 'layers');
/** Some composition wrappers drive the provider runtime themselves —
* menu-dial / onion-menu call `runtime.trigger` from their eidos component,
* with not one call in their soma dir. Emission lives in both layers. */
const EIDOS_DIR = join(__dirname, '..', 'eidos', 'components');
const EMISSION_EXCEPTIONS: Record<string, string> = {
'chronos:handle-drag':
'chronos is write-excluded by project policy — audited 2026-08-06 (S-37), the provider never emits it',
'chronos:handle-resize':
'chronos is write-excluded by project policy — audited 2026-08-06 (S-37), the provider never emits it',
// PENDING AUTHOR DECISION (S-14, waived 2026-08-06): the Clear action lives
// in the composed generic Picker, which emits ITS commit-reset with the
// picker pack's firma. Either the event leaves this morfo (the delegated
// verb — same class as color/date/time pickers), or the provider emits it.
'gradient-picker:commit-reset': 'S-14 pending: delegated verb — retire the event, or emit it',
// PENDING AUTHOR DECISION (found by this census 2026-08-06, not in the 44):
// tooltip declares three events NOBODY emits, so its SILENT rule never
// matches and no data-event-* ever stamps (the eidos motion preset that
// reads `present` included). Today's silence is accidental, not the
// declared design. Either the provider emits, or the events retire.
'tooltip:present': 'dead declared contract — pending: emit, or retire the events',
'tooltip:dismiss': 'dead declared contract — pending: emit, or retire the events',
'tooltip:dismiss-escape': 'dead declared contract — pending: emit, or retire the events'
};
/**
* The source of a component's soma implementation, concatenated. Deliberately
* NOT limited to `trigger('…')`: providers compute the name into a variable
* (`const eventName = wasExpanded ? 'emerge-collapse' : 'emerge-expand'`), so
* the presence of the LITERAL anywhere in the implementation is the honest
* question — a name that appears nowhere cannot be emitted.
*/
const SOMA_SOURCE_CACHE = new Map<string, string | undefined>();
/**
* Strip comments so prose cannot satisfy the census. menu-dial's soma file
* documents `trigger('open' | 'close' | 'commit-select', …)` in a doc block
* while every real call lives in its eidos wrapper — a raw substring search
* passed it for the wrong reason.
*/
function stripComments(source: string): string {
return source
.replace(/\/\*[\s\S]*?\*\//g, '')
.replace(/<!--[\s\S]*?-->/g, '')
.replace(/(^|\s)\/\/[^\n]*/g, '$1');
}
function readSourceTree(dir: string): string | undefined {
if (!existsSync(dir)) return undefined;
const parts: string[] = [];
const walk = (current: string) => {
for (const entry of readdirSync(current, { withFileTypes: true })) {
const path = join(current, entry.name);
if (entry.isDirectory()) walk(path);
else if (/\.(ts|svelte)$/.test(entry.name) && !/\.(test|spec)\.ts$/.test(entry.name)) {
parts.push(stripComments(readFileSync(path, 'utf-8')));
}
}
};
walk(dir);
return parts.join('\n');
}
const SHARED_EMITTERS = readSourceTree(SOMA_LAYERS_DIR) ?? '';
function somaSourceOf(componentKebab: string): string | undefined {
if (SOMA_SOURCE_CACHE.has(componentKebab)) return SOMA_SOURCE_CACHE.get(componentKebab);
const own = [
readSourceTree(join(SOMA_DIR, componentKebab)),
readSourceTree(join(EIDOS_DIR, componentKebab))
].filter((tree): tree is string => tree !== undefined);
const source = own.length === 0 ? undefined : `${own.join('\n')}\n${SHARED_EMITTERS}`;
SOMA_SOURCE_CACHE.set(componentKebab, source);
return source;
}
// ── The census ────────────────────────────────────────────────────────────
describe('pack census — every cascade rule can fire, and fires where the morfo says', () => {
it('finds the catalogue (sanity: the census is not scanning an empty tree)', () => {
expect(PACKS.length).toBeGreaterThan(60);
expect(RULES.length).toBeGreaterThan(150);
expect(MORFOS.length).toBeGreaterThan(150);
});
it('resolves every rule selector to a real part of a real morfo', () => {
const unresolved = RULES.filter(({ rule, parsed, pack }) => {
void rule;
return parsed.marker === undefined || !resolveMarker(parsed.marker, pack.pack.name);
}).map(({ id, rule }) => `${id}: ${rule.selector}`);
expect(unresolved, 'a selector whose marker no morfo emits can never match').toEqual([]);
});
it('selects a part the morfo declares as a stamp target', () => {
const offenders: string[] = [];
for (const { id, rule, parsed, pack } of RULES) {
if (id in TARGET_EXCEPTIONS) continue;
if (parsed.eventName === undefined || parsed.marker === undefined) continue;
const resolved = resolveMarker(parsed.marker, pack.pack.name);
if (!resolved) continue;
const event = eventsOf(resolved.morfo).find((e) => e.name === parsed.eventName);
if (!event) continue;
const stampParts = stampPartsOf(event);
if (stampParts.length === 0 || stampParts.includes(resolved.part)) continue;
offenders.push(
`${id}: selects '${resolved.part}' but ${resolved.morfo.kebab}.${parsed.eventName} ` +
`stamps on [${stampParts.join(', ')}] — ${rule.selector}`
);
}
expect(
offenders,
'the runtime stamps on the declared target (or a declared `allowedTargets` part); a rule ' +
'aimed anywhere else never matches. Fix the selector, or declare the redirection in the morfo'
).toEqual([]);
});
it('declares no signature for a channel its activation does not include', () => {
const offenders: string[] = [];
for (const { id, rule, parsed, pack } of RULES) {
if (id in CHANNEL_EXCEPTIONS) continue;
if (parsed.marker === undefined) continue;
const resolved = resolveMarker(parsed.marker, pack.pack.name);
if (!resolved) continue;
// Activation resolves per (event, family) occurrence, honouring the
// full precedence: the rule's own `channels` (capa 5a) wins, then the
// EVENT's `channels` declared in the morfo (capa 4 — css-field /
// number-field / virtual-list use it), then the family default.
const activations: { family: SemaFamily; active: readonly SemaChannelId[] }[] = [];
for (const event of candidateEvents(resolved.morfo, parsed)) {
for (const family of familiesOf(event, parsed)) {
activations.push({
family,
active:
rule.channels ??
event.semantic.channels ??
SEMA_MAP.families[family]?.activeChannels ??
[]
});
}
}
if (activations.length === 0) continue;
for (const channel of RULE_CHANNELS) {
if (rule[channel] === undefined) continue;
if (activations.some(({ active }) => active.includes(channel))) continue;
const families = [...new Set(activations.map((a) => a.family))];
offenders.push(
`${id}: declares '${channel}' but families [${families.join(', ')}] ` +
`never activate it — ${rule.selector}`
);
}
}
expect(
offenders,
'D.8: a signature the activation does not include is an INERT rule — widen with `channels` ' +
'and write the perceptual reason, or drop the block'
).toEqual([]);
});
it('heads every applied tuning with the family it actually modifies', () => {
const offenders: string[] = [];
for (const { file, pack } of PACKS) {
const morfo = MORFOS.find((m) => m.kebab === pack.name);
if (!morfo) continue;
for (const { eventName, key } of tuningApplications(file)) {
const event = eventsOf(morfo).find((e) => e.name === eventName);
if (!event) continue;
const head = key.split('.')[0];
const family = event.semantic.family;
if (head === family) continue;
if (`${file}:${eventName}` in TUNING_HEAD_EXCEPTIONS) continue;
offenders.push(`${file}: soundTuning('${key}') on '${eventName}', family '${family}'`);
}
}
expect(
offenders,
"the key's first segment names the family whose base the tuning modifies — applying it to " +
'another family makes the resulting level unreadable, and turns aditive tunings into a trap'
).toEqual([]);
});
it('tunes only events some provider actually emits', () => {
const seen = new Set<string>();
const offenders: string[] = [];
for (const { id, parsed, pack } of RULES) {
void id;
if (parsed.marker === undefined) continue;
// Census the morfo the SELECTOR names — a pack may scope a rule to a
// composed component's part (media-player over Button / Slider), and it
// is that component's provider which emits.
const resolved = resolveMarker(parsed.marker, pack.pack.name);
if (!resolved) continue;
const source = somaSourceOf(resolved.morfo.kebab);
if (source === undefined) continue; // no implementation of its own
// Reachability is per RULE, not per named event: a family- or
// intent-matched rule (tooltip matches `eventFamily: 'emerge'`) is just
// as dead when none of the events it can see is ever emitted.
const candidates = candidateEvents(resolved.morfo, parsed)
.map((event) => event.name)
.filter((name) => !(`${resolved.morfo.kebab}:${name}` in EMISSION_EXCEPTIONS));
if (candidates.length === 0) continue;
const key = `${resolved.morfo.kebab}:[${candidates.join(',')}]`;
if (seen.has(key)) continue;
seen.add(key);
if (!candidates.some((name) => source.includes(`'${name}'`))) {
offenders.push(
`${pack.file}: none of [${candidates.join(', ')}] is emitted by any ` +
`${resolved.morfo.kebab} provider`
);
}
}
expect(
offenders,
'a rule for an event nobody emits is dead code that documents a perception that never happens'
).toEqual([]);
});
it('keeps the exception lists honest (every entry still names a live case)', () => {
const ruleIds = new Set(RULES.map((r) => r.id));
const stale = [
...Object.keys(TARGET_EXCEPTIONS).filter((id) => !ruleIds.has(id)),
...Object.keys(CHANNEL_EXCEPTIONS).filter((id) => !ruleIds.has(id)),
...Object.keys(TUNING_HEAD_EXCEPTIONS).filter((key) => {
const [file, eventName] = key.split(':');
const pack = PACKS.find((p) => p.file === file);
if (!pack) return true;
const morfo = MORFOS.find((m) => m.kebab === pack.pack.name);
return !morfo || !eventsOf(morfo).some((e) => e.name === eventName);
}),
...Object.keys(EMISSION_EXCEPTIONS).filter((key) => {
const [kebab, eventName] = key.split(':');
const morfo = MORFOS.find((m) => m.kebab === kebab);
return !morfo || !eventsOf(morfo).some((e) => e.name === eventName);
})
];
expect(stale, 'these exceptions excuse cases that no longer exist — delete them').toEqual([]);
});
});
Loading…
Cancel
Save

Powered by TurnKey Linux.