Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"census:body-dialect": "node scripts/body-dialect-census.mjs",
"census:tsconfig-test-parity": "node scripts/tsconfig-test-parity-census.mjs",
"census:cross-file-line-citations": "node scripts/cross-file-line-citation-census.mjs",
"census:dollar-dialect-aliases": "node scripts/dollar-dialect-alias-census.mjs",
"check:new-line-citations": "node scripts/check-new-cross-file-line-citations.mjs",
"check:control-bytes": "node scripts/check-control-bytes.mjs",
"check:action-ref-convention": "node scripts/check-action-ref-convention.mjs",
Expand Down
327 changes: 327 additions & 0 deletions scripts/__tests__/dollar-dialect-alias-census.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,327 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* Pins for `scripts/dollar-dialect-alias-census.mjs` (objectui#8568).
*
* Every assertion below corresponds to a way this instrument can lie, and all
* but one of them lie in the SAME DIRECTION: an alias the scanner cannot see is
* an alias the census reports as absent, with exit 0 and no error — and the
* number it prints is the one the maintainer is being asked to price a BREAKING
* change against. A confident zero is the expensive failure here.
*
* The one that lies in the other direction is the sibling dialect. Counting a
* `FILTER_OPERATOR_ALIASES` row as `$`-dialect reliance would make option 1
* look expensive for filters option 1 does not touch, which is the mirror-image
* mistake and just as decision-breaking.
*
* Five groups, failing for five different reasons:
*
* 1. THE DERIVATION. The alias set comes out of the map and the spec, so a
* changed map changes the census. A regression here silently freezes the
* census on a dialect the code no longer speaks.
* 2. THE SYNTAXES. One case per shape an author can write. A regression here
* shrinks the population without changing what was scanned.
* 3. THE CONFLATION GUARD. Case-sensitivity and the `$` requirement, which are
* the two things separating this dialect from the tolerant sibling one.
* 4. THE CLASSIFICATION. Payload vs mention, and role by path — the axes that
* stop a flat total from reading as adoption.
* 5. THE CONTROLS AND THE TREE. The controls must be capable of failing, and
* the tree as it stands today must still satisfy them.
*/

import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';

import {
CONVERTER_PATH,
IMPOSSIBLE_SPELLING,
SIBLING_DIALECT_FILE,
AUTHORED_ROLE,
operatorMapKeysFromSource,
deriveAliases,
canonicalTwin,
scanText,
roleOf,
isTestPath,
SELF_FILES,
siblingDialectBait,
evaluateControls,
runCensus,
} from '../dollar-dialect-alias-census.mjs';

const REPO_ROOT = join(__dirname, '..', '..');

const SPEC_OPERATORS = [
'$eq', '$ne', '$gt', '$gte', '$lt', '$lte', '$in', '$nin', '$between',
'$contains', '$notContains', '$startsWith', '$endsWith', '$icontains', '$null', '$exists',
];

const mapSource = (rows: string) => `
export function convertOperatorToAST(operator: string): string | null {
const operatorMap: Record<string, string> = {
${rows}
};

return operatorMap[operator] || null;
}
`;

const shapes = (text: string, spelling: string) =>
(scanText(text, spelling) as Array<{ shape: string; line: number }>).map((h) => `${h.shape}@${h.line}`);

describe('the alias set is DERIVED from the map and the spec, never listed', () => {
it('reads the map keys out of the source, quoted or bare', () => {
const keys = operatorMapKeysFromSource(mapSource(
" '$eq': '=',\n $ne: '!=',\n '$startswith': 'startswith',",
));
expect(keys).toEqual(['$eq', '$ne', '$startswith']);
});

it('calls a spelling the spec does not carry an alias, and nothing else', () => {
const keys = ['$eq', '$nin', '$notin', '$startsWith', '$startswith'];
expect(deriveAliases(keys, SPEC_OPERATORS)).toEqual(['$notin', '$startswith']);
});

it('derives an EMPTY set on a tree where the aliases were retired', () => {
// This is the shape of the tree after objectui#8568's option 1. A census
// that still hunted four hardcoded spellings would keep reporting on a
// dialect the code no longer speaks.
const keys = ['$eq', '$nin', '$notContains', '$startsWith', '$endsWith'];
expect(deriveAliases(keys, SPEC_OPERATORS)).toEqual([]);
});

it('picks up a FIFTH alias the day somebody adds one', () => {
const keys = ['$eq', '$startsWith', '$startswith', '$ncontains'];
expect(deriveAliases(keys, SPEC_OPERATORS)).toEqual(['$startswith', '$ncontains']);
});

it('THROWS rather than reading an empty map when the anchor is gone', () => {
// Silently reading zero keys would derive zero aliases and print a zero
// that means "this instrument broke", indistinguishable from "no author
// relies on the tolerance".
expect(() => operatorMapKeysFromSource('export const somethingElse = {};'))
.toThrow(/no longer declares `const operatorMap`/);
expect(() => operatorMapKeysFromSource(mapSource(' // nothing here')))
.toThrow(/parsed to zero keys/);
});

it('pairs an alias with its canonical twin only when the spec has one', () => {
expect(canonicalTwin('$startswith', SPEC_OPERATORS)).toBe('$startsWith');
expect(canonicalTwin('$notcontains', SPEC_OPERATORS)).toBe('$notContains');
// `$notin` is an alias OF `$nin`, not a case variant of it — the census
// must not invent a twin for it.
expect(canonicalTwin('$notin', SPEC_OPERATORS)).toBeNull();
});
});

describe('the scanner sees every shape an author can write a filter in', () => {
it('reads a bare operator key in an object literal', () => {
expect(shapes("const f = { email: { $startswith: 'a' } };", '$startswith')).toEqual(['payload@1']);
});

it('reads a quoted key, single or double — JSON metadata is the double form', () => {
expect(shapes("{ email: { '$startswith': 'a' } }", '$startswith')).toEqual(['payload@1']);
expect(shapes('{ "email": { "$startswith": "a" } }', '$startswith')).toEqual(['payload@1']);
});

it('reads a key written with space before the colon', () => {
expect(shapes('{ "$endswith" : ".com" }', '$endswith')).toEqual(['payload@1']);
});

it('reads the spelling handed to the converter as its whole argument', () => {
expect(shapes("expect(convertOperatorToAST('$notin')).toBe('nin');", '$notin')).toEqual(['payload@1']);
});

it('counts a payload once per line and calls the rest of that line mentions', () => {
// Two spellings on one line is a doc table row, not two filters.
expect(shapes("{ $notin: [] } // $notin again", '$notin')).toEqual(['payload@1', 'mention@1']);
});

it('finds payloads on the lines they are on, across a multi-line literal', () => {
const text = ['const f = {', " a: { $notin: [1] },", ' b: 2,', " c: { $notin: [3] },", '};'].join('\n');
expect(shapes(text, '$notin')).toEqual(['payload@2', 'payload@4']);
});
});

describe('the $ and the case are what separate this dialect from the tolerant sibling one', () => {
it('does NOT count the $-free row of the same name', () => {
// `FILTER_OPERATOR_ALIASES` really carries these rows, and
// `normalizeFilterOperator` lowercases before the lookup, so that dialect
// accepts every casing BY DESIGN. It is not objectui#8568's subject.
const table = [" starts_with: 'startswith',", " startswith: 'startswith',", " notin: 'nin',"].join('\n');
expect(scanText(table, '$startswith')).toEqual([]);
expect(scanText(table, '$notin')).toEqual([]);
});

it('is CASE-SENSITIVE in both directions', () => {
// A case-insensitive probe reports the canonical corpus as alias reliance,
// and a case-insensitive exclusion erases the aliases entirely. Both were
// live hazards while this census was being written.
expect(scanText("{ $startsWith: 'a' }", '$startswith')).toEqual([]);
expect(scanText("{ $startswith: 'a' }", '$startsWith')).toEqual([]);
});

it('will not match a longer identifier that merely begins with the spelling', () => {
expect(scanText("{ $notinclude: 1 }", '$notin')).toEqual([]);
expect(scanText("{ $startswithPrefix: 1 }", '$startswith')).toEqual([]);
});

it('will not match a spelling glued to the left of a longer token', () => {
expect(scanText('const x$notin = 1;', '$notin')).toEqual([]);
});

it('reads the impossible control spelling nowhere', () => {
expect(scanText(`{ ${IMPOSSIBLE_SPELLING}: 1 }`, '$startswith')).toEqual([]);
});
});

describe('payload versus mention, and role by path', () => {
it('calls a backticked prose span and a table cell mentions, not filters', () => {
expect(shapes('Lowercase aliases (`$startswith`, `$notcontains`) are accepted.', '$startswith'))
.toEqual(['mention@1']);
expect(shapes('| `$startsWith` / `$startswith` | `startswith` | … |', '$startswith'))
.toEqual(['mention@1']);
});

it('calls a roster entry a mention — an it.each list is a claim, not a filter', () => {
expect(shapes("it.each(['$startswith', '$notcontains', '$notin'])(", '$startswith'))
.toEqual(['mention@1']);
});

it('routes the map itself to implementation and its suites to the ledger', () => {
expect(roleOf(CONVERTER_PATH)).toBe('implementation');
expect(roleOf('packages/core/src/utils/__tests__/filter-converter.test.ts')).toBe('converter-ledger');
expect(roleOf('packages/data-objectstack/README.md')).toBe('converter-ledger');
expect(roleOf('packages/core/src/adapters/README.md')).toBe('refusal-ledger');
expect(roleOf('.changeset/8447-valuedatasource-dollar-operator-refusal.md')).toBe('release-notes');
});

it('routes the trees an author writes in to the authored corpus', () => {
for (const p of [
'examples/schema-catalog/src/schemas/x.json',
'apps/console/src/pages/system/AuditLogPage.tsx',
'content/docs/guide/dashboard-filters.md',
'e2e/x.spec.ts',
]) expect(roleOf(p)).toBe(AUTHORED_ROLE);
});

it('puts the map ahead of the tree-wide prefixes, so first match wins', () => {
// `packages/core/src/adapters/` would otherwise swallow nothing here, but
// the ordering is what keeps the converter out of `other`.
expect(roleOf(CONVERTER_PATH)).not.toBe('other');
expect(roleOf('packages/plugin-grid/src/ObjectGrid.tsx')).toBe('other');
});

it('knows a test inside an authored tree is still a test', () => {
expect(isTestPath('apps/console/src/pages/system/AuditLogPage.test.tsx')).toBe(true);
expect(isTestPath('scripts/__tests__/anything.ts')).toBe(true);
expect(isTestPath('apps/console/src/pages/system/AuditLogPage.tsx')).toBe(false);
});
});

describe('the controls can fail, which is the only reason their passing means anything', () => {
const base = {
aliases: ['$notin', '$startswith'],
specOperators: SPEC_OPERATORS,
byRole: { canonicalTotals: { $startsWith: 12 } },
authoredPayloadCanonical: [{ file: 'apps/x.ts', line: 1, spelling: '$gte' }],
siblingHits: 0,
siblingBait: 4,
impossibleHits: 0,
};
const idsFailing = (over: Record<string, unknown>) =>
(evaluateControls({ ...base, ...over }) as Array<{ id: string; ok: boolean }>)
.filter((c) => !c.ok).map((c) => c.id);

it('passes all four on a healthy reading', () => {
expect(idsFailing({})).toEqual([]);
});

it('fails canonical-twin when the scanner cannot see the camelCase spellings', () => {
expect(idsFailing({ byRole: { canonicalTotals: {} } })).toEqual(['canonical-twin']);
});

it('fails authored-reach when no $-dialect payload was found outside the tests', () => {
expect(idsFailing({ authoredPayloadCanonical: [] })).toEqual(['authored-reach']);
});

it('fails impossible when the scanner matches a spelling nobody wrote', () => {
expect(idsFailing({ impossibleHits: 1 })).toEqual(['impossible']);
});

it('fails sibling-dialect the moment a $-free row is counted', () => {
expect(idsFailing({ siblingHits: 1 })).toEqual(['sibling-dialect']);
});

it('fails sibling-dialect when its BAIT is gone, rather than passing vacuously', () => {
// "The census counted zero sibling rows" and "there are no sibling rows
// left to count" are the same reading unless the bait is measured. A
// negative control that cannot be tripped is not evidence.
expect(idsFailing({ siblingBait: 0 })).toEqual(['sibling-dialect']);
});

it('does not demand a lit twin on a tree that has no aliases left', () => {
// After option 1 there is nothing to light, and a control that cannot be
// satisfied would make the post-retirement census permanently "not a
// reading".
expect(idsFailing({ aliases: [], byRole: { canonicalTotals: {} } })).toEqual([]);
});
});

describe('the tree as it stands today', () => {
it('still declares the operatorMap this census reads', () => {
const source = readFileSync(join(REPO_ROOT, CONVERTER_PATH), 'utf8');
expect(() => operatorMapKeysFromSource(source)).not.toThrow();
expect(operatorMapKeysFromSource(source)).toContain('$eq');
});

it('carves ITSELF out, because it carries every spelling as fixture text', () => {
// Including `IMPOSSIBLE_SPELLING`. Scanning these two files makes the
// negative control fire on its own declaration, and the run stops being a
// reading — which is exactly how this was found, the first time they became
// tracked files.
expect(SELF_FILES.has('scripts/dollar-dialect-alias-census.mjs')).toBe(true);
expect(SELF_FILES.has('scripts/__tests__/dollar-dialect-alias-census.test.ts')).toBe(true);
});

it('still carries the $-free sibling rows the conflation control is aimed at', () => {
// If this file ever stops carrying them, the `sibling-dialect` control is
// testing nothing and the census needs a new anchor rather than a green
// control that cannot fail.
const source = readFileSync(join(REPO_ROOT, SIBLING_DIALECT_FILE), 'utf8');
expect(source).toMatch(/^\s*startswith:\s*'startswith',$/m);
expect(source).toMatch(/^\s*notin:\s*'nin',$/m);
expect(siblingDialectBait(source)).toBeGreaterThan(0);
});

it('counts bait only where the $ is absent, so the probe is not just a second alias grep', () => {
expect(siblingDialectBait(" startswith: 'startswith',\n notin: 'nin',\n")).toBe(2);
expect(siblingDialectBait(" $startswith: 'startswith',\n")).toBe(0);
});

it('passes every control on a real run, so the printed number is a reading', async () => {
const r = await runCensus(REPO_ROOT) as {
aliases: string[];
controls: Array<{ id: string; ok: boolean; detail: string }>;
rows: Array<{ role: string }>;
selfCarved: number;
};
const failed = r.controls.filter((c) => !c.ok).map((c) => `${c.id}: ${c.detail}`);
expect(failed, 'a census run whose controls fail is not a reading').toEqual([]);
// Not an exhaustiveness pin on the count — objectui#8568 reserves the
// ruling and the count is expected to move. What is pinned is that the
// instrument SAW something, because a zero everywhere is blindness.
expect(r.rows.length).toBeGreaterThan(0);
expect(r.aliases.length).toBeGreaterThan(0);
// The self carve-out really carved something out; a zero here would mean
// the fixture spellings had quietly moved back into the population.
expect(r.selfCarved).toBeGreaterThan(0);
});
});
Loading
Loading