@hyperfixi/testing-framework 3.2.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +203 -1
- package/package.json +9 -9
- package/src/agent-bench/README.md +48 -1
- package/src/agent-bench/agent-bench.test.ts +7 -0
- package/src/agent-bench/harness.ts +39 -50
- package/src/multilingual/README.md +30 -11
- package/src/multilingual/bare-render-fidelity.ts +1 -1
- package/src/multilingual/cli.ts +5 -3
- package/src/multilingual/direct-path-shapes-gate.ts +56 -0
- package/src/multilingual/direct-path-shapes.1.test.ts +12 -0
- package/src/multilingual/direct-path-shapes.2.test.ts +12 -0
- package/src/multilingual/direct-path-shapes.3.test.ts +12 -0
- package/src/multilingual/direct-path-shapes.cases.json +2621 -0
- package/src/multilingual/direct-path-shapes.ts +459 -0
- package/src/multilingual/engine-parser-parity.test.ts +67 -0
- package/src/multilingual/orchestrator.ts +2 -1
- package/src/multilingual/render-fidelity.ts +1 -1
- package/src/multilingual/shipped-examples-execution.test.ts +44 -33
- package/src/multilingual/shipped-examples-execution.ts +55 -75
- package/src/multilingual/shipped-sources-engine.test.ts +106 -0
- package/src/multilingual/shipped-sources-localized.test.ts +87 -0
- package/src/multilingual/shipped-sources-validity.ts +127 -74
- package/src/multilingual/validators/execution-validator.test.ts +81 -23
- package/src/multilingual/validators/execution-validator.ts +140 -94
- package/src/multilingual/validators/parse-validator.ts +7 -20
- package/src/multilingual/value-matrix.accepted.test.ts +8 -11
- package/src/multilingual/value-matrix.isolation.test.ts +7 -6
- package/src/multilingual/value-matrix.ts +72 -86
- package/src/multilingual/shipped-sources-validity.test.ts +0 -95
|
@@ -42,17 +42,29 @@
|
|
|
42
42
|
* Each cell runs its English source on the real `hyperscript.org` engine: that
|
|
43
43
|
* result is the ORACLE. Then, on the same fixture:
|
|
44
44
|
*
|
|
45
|
-
* - `en` hyperfixi's English path (core's parser and runtime);
|
|
46
45
|
* - `en-rt` semantic's English parse rendered back to English, on upstream:
|
|
47
46
|
* where it fails, every translation inherits the loss;
|
|
48
|
-
* - `<L
|
|
49
|
-
* `
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
47
|
+
* - `<L>/up` each of 23 languages — `render(parse_en(src), L)` — through
|
|
48
|
+
* `@lokascript/hyperscript-adapter` (`preprocess`, back to
|
|
49
|
+
* English) on upstream: the multilingual product for original
|
|
50
|
+
* _hyperscript users;
|
|
51
|
+
* - `eng` the English source on `@hyperfixi/engine`, the engine that
|
|
52
|
+
* replaces core's;
|
|
53
|
+
* - `<L>/eng` the adapter's English (the string the `/up` lane runs) on
|
|
54
|
+
* that engine: where it differs from `<L>/up`, the two ENGINES
|
|
55
|
+
* differ, since they were given the same text.
|
|
53
56
|
*
|
|
54
57
|
* A (cell, lane) pair FAILS when its result differs from the oracle's.
|
|
55
58
|
*
|
|
59
|
+
* A `<L>` lane ran each translation on core's direct path (compiled with
|
|
60
|
+
* `{ language: L }`) until Phase C2 of the engine cutover, which made text the
|
|
61
|
+
* multilingual interchange. It retired after the measurement the plan asked
|
|
62
|
+
* for: on the committed baseline no cell failed `<L>/eng` while passing `<L>`
|
|
63
|
+
* (the only foreign failures, three Italian `di` cells, failed all three).
|
|
64
|
+
* An `en` lane ran the English source on `@hyperfixi/core`'s parser and runtime
|
|
65
|
+
* until Phase C4, when it left with core's engine; its only failures were the
|
|
66
|
+
* eight accepted cells of core's `the X of Y as T` reading, which `eng` passes.
|
|
67
|
+
*
|
|
56
68
|
* ## The ratchet
|
|
57
69
|
* `baselines/value-matrix.json` lists every failing pair, per cell. The gate
|
|
58
70
|
* fails on a failing pair it does not list AND on a listed pair that now
|
|
@@ -64,12 +76,13 @@
|
|
|
64
76
|
* One jsdom window for the whole run; each lane resets `<body>` and the
|
|
65
77
|
* globals, installs the handler on a fresh button, clicks it, and reads
|
|
66
78
|
* `#out`. Variables are window globals (see GLOBALS), so no source needs a
|
|
67
|
-
* `set` prefix.
|
|
68
|
-
* within one macrotask.
|
|
79
|
+
* `set` prefix. Both engines run synchronously unless the source waits.
|
|
69
80
|
*
|
|
70
|
-
* A translation that loses a loop's condition can loop forever.
|
|
71
|
-
*
|
|
72
|
-
*
|
|
81
|
+
* A translation that loses a loop's condition can loop forever. Upstream has
|
|
82
|
+
* no cap and blocks the thread, so every upstream run gets an evaluation
|
|
83
|
+
* budget (see EVAL_BUDGET). The new engine has no cap and no hook for one: a
|
|
84
|
+
* string that spent the budget on upstream is not run on it, and its `/eng`
|
|
85
|
+
* lane reads `✗budget` too.
|
|
73
86
|
*
|
|
74
87
|
* Node-only: imports the real `hyperscript.org` build off disk and needs the
|
|
75
88
|
* node vitest environment (see the shipped-examples gate for why).
|
|
@@ -731,11 +744,14 @@ export const FOREIGN_LANGUAGES = [
|
|
|
731
744
|
|
|
732
745
|
/** Every lane, in report order. */
|
|
733
746
|
export const LANES: readonly string[] = [
|
|
734
|
-
'en',
|
|
735
747
|
'en-rt',
|
|
736
|
-
|
|
748
|
+
'eng',
|
|
749
|
+
...FOREIGN_LANGUAGES.flatMap(language => [`${language}/up`, `${language}/eng`]),
|
|
737
750
|
];
|
|
738
751
|
|
|
752
|
+
/** The lanes a cell runs per language it does not skip. */
|
|
753
|
+
export const LANES_PER_LANGUAGE = 2;
|
|
754
|
+
|
|
739
755
|
/**
|
|
740
756
|
* Upstream evaluations allowed per run. A cell uses well under a hundred; a
|
|
741
757
|
* loop whose condition a translation lost uses them all and stops, where it
|
|
@@ -743,10 +759,14 @@ export const LANES: readonly string[] = [
|
|
|
743
759
|
*/
|
|
744
760
|
const EVAL_BUDGET = 20_000;
|
|
745
761
|
|
|
746
|
-
/**
|
|
747
|
-
interface
|
|
762
|
+
/** What a lane needs of a host that reads scripts off attributes: upstream, or the new engine. */
|
|
763
|
+
interface ScriptHost {
|
|
748
764
|
parse(source: string): { errors?: Array<{ message: string }> } | undefined;
|
|
749
765
|
processNode(element: Element): void;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/** The upstream surface this uses (`hyperscript.org`'s ESM default export). */
|
|
769
|
+
interface UpstreamEngine extends ScriptHost {
|
|
750
770
|
internals: {
|
|
751
771
|
runtime: { unifiedEval(parseElement: unknown, context: unknown): unknown };
|
|
752
772
|
};
|
|
@@ -775,8 +795,8 @@ export interface MatrixEngines {
|
|
|
775
795
|
*
|
|
776
796
|
* Until `close()`, the console is silenced and unhandled rejections are
|
|
777
797
|
* trapped: thousands of lanes fail by design, both engines report a failure
|
|
778
|
-
* on the console, and
|
|
779
|
-
* in it rejects a promise nobody holds. The process's own rejection
|
|
798
|
+
* on the console, and a handler that waits on something runs on after its
|
|
799
|
+
* click, so an error in it rejects a promise nobody holds. The process's own rejection
|
|
780
800
|
* listeners (vitest's, under test) are set aside for the run and restored.
|
|
781
801
|
*/
|
|
782
802
|
export async function initMatrixEngines(): Promise<MatrixEngines> {
|
|
@@ -803,12 +823,14 @@ export async function initMatrixEngines(): Promise<MatrixEngines> {
|
|
|
803
823
|
};
|
|
804
824
|
process.on('unhandledRejection', trap);
|
|
805
825
|
|
|
806
|
-
const { hyperscript } = await import('@hyperfixi/core');
|
|
807
826
|
const { parseSemantic, render } = await import('@lokascript/semantic');
|
|
808
827
|
const { preprocess } = await import('@lokascript/hyperscript-adapter');
|
|
809
828
|
const require = createRequire(import.meta.url);
|
|
810
829
|
const esm = require.resolve('hyperscript.org').replace(/[^/\\]+$/, '_hyperscript.esm.js');
|
|
811
830
|
const upstream: UpstreamEngine = (await import(pathToFileURL(esm).href)).default;
|
|
831
|
+
const engineModule = await import('@hyperfixi/engine');
|
|
832
|
+
engineModule.register(...engineModule.everything);
|
|
833
|
+
const engine: ScriptHost = engineModule.api;
|
|
812
834
|
|
|
813
835
|
// The evaluation budget: every upstream evaluation goes through unifiedEval.
|
|
814
836
|
const runtime = upstream.internals.runtime;
|
|
@@ -821,13 +843,6 @@ export async function initMatrixEngines(): Promise<MatrixEngines> {
|
|
|
821
843
|
|
|
822
844
|
const window = dom.window;
|
|
823
845
|
const document = window.document;
|
|
824
|
-
type Ast = Parameters<typeof hyperscript.execute>[0];
|
|
825
|
-
|
|
826
|
-
// hyperfixi keeps its global variables in one Map that every context shares,
|
|
827
|
-
// and writes a window global there (`increment n`). It outlives the run, and
|
|
828
|
-
// it shadows window's copy, so every later lane would read the write.
|
|
829
|
-
const coreGlobals = hyperscript.createContext().globals;
|
|
830
|
-
const coreGlobalsAtStart = new Map(coreGlobals);
|
|
831
846
|
const headAtStart = document.head.innerHTML;
|
|
832
847
|
const names = collidingNames();
|
|
833
848
|
|
|
@@ -844,8 +859,6 @@ export async function initMatrixEngines(): Promise<MatrixEngines> {
|
|
|
844
859
|
html.innerHTML = `<head>${headAtStart}</head><body></body>`;
|
|
845
860
|
}
|
|
846
861
|
document.body.innerHTML = FIXTURE;
|
|
847
|
-
coreGlobals.clear();
|
|
848
|
-
for (const [name, value] of coreGlobalsAtStart) coreGlobals.set(name, value);
|
|
849
862
|
for (const [name, make] of Object.entries(GLOBALS)) {
|
|
850
863
|
const value = make();
|
|
851
864
|
Reflect.set(window, name, value);
|
|
@@ -862,29 +875,21 @@ export async function initMatrixEngines(): Promise<MatrixEngines> {
|
|
|
862
875
|
};
|
|
863
876
|
const read = (): string => document.getElementById('out')?.textContent ?? '✗no #out';
|
|
864
877
|
|
|
865
|
-
/** Run English on
|
|
866
|
-
const
|
|
867
|
-
const errors =
|
|
878
|
+
/** Run English on a host that reads the script off the button. */
|
|
879
|
+
const onHost = (host: ScriptHost, source: string): string => {
|
|
880
|
+
const errors = host.parse(source)?.errors ?? [];
|
|
868
881
|
if (errors.length) return `✗parse: ${errors[0]?.message.split('\n')[0] ?? ''}`;
|
|
869
882
|
const button = reset();
|
|
870
883
|
button.setAttribute('_', source);
|
|
871
884
|
evaluations = 0;
|
|
872
885
|
const before = reportedErrors;
|
|
873
|
-
|
|
886
|
+
host.processNode(button);
|
|
874
887
|
click(button);
|
|
875
888
|
if (evaluations > EVAL_BUDGET) return '✗budget';
|
|
876
889
|
const got = read();
|
|
877
890
|
return reportedErrors > before && got === '∅' ? '✗threw' : got;
|
|
878
891
|
};
|
|
879
|
-
|
|
880
|
-
/** Install a compiled handler on hyperfixi, click, and settle. */
|
|
881
|
-
const onHyperfixi = async (ast: Ast): Promise<string> => {
|
|
882
|
-
const button = reset();
|
|
883
|
-
await hyperscript.execute(ast, hyperscript.createContext(button));
|
|
884
|
-
click(button);
|
|
885
|
-
await new Promise(resolve => setTimeout(resolve, 0));
|
|
886
|
-
return read();
|
|
887
|
-
};
|
|
892
|
+
const onUpstream = (source: string): string => onHost(upstream, source);
|
|
888
893
|
|
|
889
894
|
const guard = async (run: () => Promise<string> | string): Promise<string> => {
|
|
890
895
|
try {
|
|
@@ -904,16 +909,11 @@ export async function initMatrixEngines(): Promise<MatrixEngines> {
|
|
|
904
909
|
}
|
|
905
910
|
const lanes = result.lanes;
|
|
906
911
|
|
|
907
|
-
lanes.en = await guard(async () => {
|
|
908
|
-
const compiled = hyperscript.compileSync(cell.source);
|
|
909
|
-
if (!compiled.ok || !compiled.ast) return '✗compile';
|
|
910
|
-
return onHyperfixi(compiled.ast);
|
|
911
|
-
});
|
|
912
|
-
|
|
913
912
|
const english = parseSemantic(cell.source, 'en').node;
|
|
914
913
|
lanes['en-rt'] = await guard(() =>
|
|
915
914
|
english ? onUpstream(render(english, 'en')) : '✗untranslatable'
|
|
916
915
|
);
|
|
916
|
+
lanes.eng = await guard(() => onHost(engine, cell.source));
|
|
917
917
|
|
|
918
918
|
for (const language of FOREIGN_LANGUAGES) {
|
|
919
919
|
if (cell.skip?.includes(language)) continue;
|
|
@@ -924,17 +924,20 @@ export async function initMatrixEngines(): Promise<MatrixEngines> {
|
|
|
924
924
|
code = null;
|
|
925
925
|
}
|
|
926
926
|
if (code === null) {
|
|
927
|
-
lanes[language] = '✗untranslatable';
|
|
928
927
|
lanes[`${language}/up`] = '✗untranslatable';
|
|
928
|
+
lanes[`${language}/eng`] = '✗untranslatable';
|
|
929
929
|
continue;
|
|
930
930
|
}
|
|
931
931
|
const translated = code;
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
lanes[`${language}/
|
|
932
|
+
// The adapter's English, once, for both hosts.
|
|
933
|
+
const adapted = await guard(() => preprocess(translated, language));
|
|
934
|
+
const up = (lanes[`${language}/up`] = adapted.startsWith('✗')
|
|
935
|
+
? adapted
|
|
936
|
+
: await guard(() => onUpstream(adapted)));
|
|
937
|
+
lanes[`${language}/eng`] =
|
|
938
|
+
adapted.startsWith('✗') || up === '✗budget'
|
|
939
|
+
? up
|
|
940
|
+
: await guard(() => onHost(engine, adapted));
|
|
938
941
|
}
|
|
939
942
|
return result;
|
|
940
943
|
},
|
|
@@ -967,7 +970,7 @@ export async function runValueMatrix(cells: readonly MatrixCell[]): Promise<Cell
|
|
|
967
970
|
export interface BaselineEntry {
|
|
968
971
|
/**
|
|
969
972
|
* The failing lanes, space-separated, in LANES order, with two shorthands:
|
|
970
|
-
* `*
|
|
973
|
+
* `*up` for all 23 languages on upstream, `*eng` for all 23 on the new engine.
|
|
971
974
|
*/
|
|
972
975
|
lanes: string;
|
|
973
976
|
/** Where the loss sits, for reading the burn-down (see familyOf); not asserted. */
|
|
@@ -987,23 +990,23 @@ export interface ValueMatrixBaseline {
|
|
|
987
990
|
entries: Record<string, BaselineEntry>;
|
|
988
991
|
}
|
|
989
992
|
|
|
990
|
-
const DIRECT_LANES: readonly string[] = FOREIGN_LANGUAGES;
|
|
991
993
|
const ADAPTER_LANES: readonly string[] = FOREIGN_LANGUAGES.map(language => `${language}/up`);
|
|
994
|
+
const ENGINE_LANES: readonly string[] = FOREIGN_LANGUAGES.map(language => `${language}/eng`);
|
|
992
995
|
|
|
993
996
|
/** Lanes (in LANES order) as a baseline string, with a shorthand for each full group. */
|
|
994
997
|
export function compressLanes(lanes: readonly string[]): string {
|
|
995
998
|
const set = new Set(lanes);
|
|
996
999
|
const out: string[] = [];
|
|
997
|
-
const direct = DIRECT_LANES.every(l => set.has(l));
|
|
998
1000
|
const adapter = ADAPTER_LANES.every(l => set.has(l));
|
|
1001
|
+
const engine = ENGINE_LANES.every(l => set.has(l));
|
|
999
1002
|
for (const lane of LANES) {
|
|
1000
1003
|
if (!set.has(lane)) continue;
|
|
1001
|
-
if (direct && DIRECT_LANES.includes(lane)) continue;
|
|
1002
1004
|
if (adapter && ADAPTER_LANES.includes(lane)) continue;
|
|
1005
|
+
if (engine && ENGINE_LANES.includes(lane)) continue;
|
|
1003
1006
|
out.push(lane);
|
|
1004
1007
|
}
|
|
1005
|
-
if (direct) out.push('*direct');
|
|
1006
1008
|
if (adapter) out.push('*up');
|
|
1009
|
+
if (engine) out.push('*eng');
|
|
1007
1010
|
return out.join(' ');
|
|
1008
1011
|
}
|
|
1009
1012
|
|
|
@@ -1011,8 +1014,8 @@ export function compressLanes(lanes: readonly string[]): string {
|
|
|
1011
1014
|
export function expandLanes(text: string): string[] {
|
|
1012
1015
|
const out: string[] = [];
|
|
1013
1016
|
for (const token of text.split(' ').filter(Boolean)) {
|
|
1014
|
-
if (token === '*
|
|
1015
|
-
else if (token === '*
|
|
1017
|
+
if (token === '*up') out.push(...ADAPTER_LANES);
|
|
1018
|
+
else if (token === '*eng') out.push(...ENGINE_LANES);
|
|
1016
1019
|
else out.push(token);
|
|
1017
1020
|
}
|
|
1018
1021
|
return out;
|
|
@@ -1026,27 +1029,23 @@ export function failingLanes(result: CellResult): string[] {
|
|
|
1026
1029
|
/**
|
|
1027
1030
|
* Where a cell's failure sits, from which lanes fail:
|
|
1028
1031
|
*
|
|
1029
|
-
* - `core` core's English run differs from upstream's;
|
|
1030
1032
|
* - `semantic-en` semantic's English parse loses it (`en-rt`), so every
|
|
1031
1033
|
* translation inherits the loss;
|
|
1032
|
-
* - `translation` some foreign lanes
|
|
1033
|
-
*
|
|
1034
|
-
* - `
|
|
1034
|
+
* - `translation` some foreign lanes on upstream: the adapter's English
|
|
1035
|
+
* for that language says something else;
|
|
1036
|
+
* - `engine` the new engine differs from upstream on the same text:
|
|
1037
|
+
* its English run, or a language's `/eng` lane without
|
|
1038
|
+
* its `/up` lane.
|
|
1035
1039
|
*
|
|
1036
1040
|
* Several can hold at once; they are joined in that order.
|
|
1037
1041
|
*/
|
|
1038
1042
|
export function familyOf(lanes: readonly string[]): string {
|
|
1039
1043
|
const set = new Set(lanes);
|
|
1040
1044
|
const parts: string[] = [];
|
|
1041
|
-
if (set.has('en')) parts.push('core');
|
|
1042
1045
|
if (set.has('en-rt')) parts.push('semantic-en');
|
|
1043
|
-
else {
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
const both = direct.filter(l => adapter.includes(l));
|
|
1047
|
-
if (both.length) parts.push('translation');
|
|
1048
|
-
if (direct.length > both.length && !set.has('en')) parts.push('direct-path');
|
|
1049
|
-
if (adapter.length > both.length) parts.push('adapter');
|
|
1046
|
+
else if (FOREIGN_LANGUAGES.some(l => set.has(`${l}/up`))) parts.push('translation');
|
|
1047
|
+
if (set.has('eng') || FOREIGN_LANGUAGES.some(l => set.has(`${l}/eng`) !== set.has(`${l}/up`))) {
|
|
1048
|
+
parts.push('engine');
|
|
1050
1049
|
}
|
|
1051
1050
|
return parts.join('+') || 'none';
|
|
1052
1051
|
}
|
|
@@ -1063,26 +1062,13 @@ export const ACCEPTED: ReadonlyArray<{
|
|
|
1063
1062
|
lanes: string;
|
|
1064
1063
|
reason: string;
|
|
1065
1064
|
}> = [
|
|
1066
|
-
{
|
|
1067
|
-
cells: [
|
|
1068
|
-
...['put', 'set', 'while', 'times', 'increment'].map(
|
|
1069
|
-
p => `${p}|the textContent of #a as Int`
|
|
1070
|
-
),
|
|
1071
|
-
// The same difference with a reference owner (PR 108); upstream's
|
|
1072
|
-
// `window as Int` is null, and in a loop bound both read 0 iterations.
|
|
1073
|
-
...['put', 'set', 'increment'].map(p => `${p}|the scrollY of window as Int`),
|
|
1074
|
-
],
|
|
1075
|
-
lanes: 'en *direct',
|
|
1076
|
-
reason:
|
|
1077
|
-
'known difference: core converts the property, upstream the target (core/docs/UPSTREAM-KNOWN-DIFFS.md)',
|
|
1078
|
-
},
|
|
1079
1065
|
{
|
|
1080
1066
|
cells: [
|
|
1081
1067
|
'increment|#a.textContent',
|
|
1082
1068
|
'increment|#a.textContent + 2',
|
|
1083
1069
|
'increment|#a.textContent as Int',
|
|
1084
1070
|
],
|
|
1085
|
-
lanes: 'it it/
|
|
1071
|
+
lanes: 'it/up it/eng',
|
|
1086
1072
|
reason:
|
|
1087
1073
|
'ambiguity: it `di` is both `by` and `of`, so `incrementare i di #a.textContent` also says `increment i of #a.textContent`',
|
|
1088
1074
|
},
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shipped-sources validity gate (see shipped-sources-validity.ts for the why).
|
|
3
|
-
*
|
|
4
|
-
* Compiles every hyperscript source we ship in `examples/` and the doc trees,
|
|
5
|
-
* and ratchets on the recovers-with-errors state (`ok: true` with a non-empty
|
|
6
|
-
* `errors`). Three assertions, matching the canonical-validity gates:
|
|
7
|
-
* 1. sanity — sources were actually found and mostly compile clean;
|
|
8
|
-
* 2. no NEW recovering source appears outside the committed allowlist;
|
|
9
|
-
* 3. no allowlisted key has silently become clean (stale entries must be
|
|
10
|
-
* removed so the list only ever shrinks).
|
|
11
|
-
*
|
|
12
|
-
* To update after an intentional fix: re-run and rewrite
|
|
13
|
-
* `baselines/shipped-sources-validity.json`. Note the allowlist key embeds a
|
|
14
|
-
* hash of the source, so FIXING a source changes its key — the entry goes
|
|
15
|
-
* stale and assertion 3 makes removing it mandatory.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { readFileSync } from 'node:fs';
|
|
19
|
-
import { fileURLToPath } from 'node:url';
|
|
20
|
-
import path from 'node:path';
|
|
21
|
-
import { JSDOM } from 'jsdom';
|
|
22
|
-
import { describe, it, expect, beforeAll } from 'vitest';
|
|
23
|
-
import {
|
|
24
|
-
checkShippedSourcesValidity,
|
|
25
|
-
type ShippedSourcesResult,
|
|
26
|
-
type CompileForValidity,
|
|
27
|
-
} from './shipped-sources-validity';
|
|
28
|
-
|
|
29
|
-
interface AllowlistDoc {
|
|
30
|
-
allowedRecovered: Array<{
|
|
31
|
-
key: string;
|
|
32
|
-
file: string;
|
|
33
|
-
error: string;
|
|
34
|
-
upstream: string;
|
|
35
|
-
reason: string;
|
|
36
|
-
}>;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const baselinePath = path.resolve(
|
|
40
|
-
path.dirname(fileURLToPath(import.meta.url)),
|
|
41
|
-
'../../baselines/shipped-sources-validity.json'
|
|
42
|
-
);
|
|
43
|
-
const allowlist = JSON.parse(readFileSync(baselinePath, 'utf8')) as AllowlistDoc;
|
|
44
|
-
const allowed = new Set(allowlist.allowedRecovered.map(e => e.key));
|
|
45
|
-
|
|
46
|
-
describe('shipped-sources validity gate', () => {
|
|
47
|
-
let result: ShippedSourcesResult;
|
|
48
|
-
|
|
49
|
-
beforeAll(async () => {
|
|
50
|
-
// Import core through its built entry, the same surface a consumer gets.
|
|
51
|
-
const core = (await import('@hyperfixi/core')) as unknown as {
|
|
52
|
-
hyperscript: { compileSync: CompileForValidity };
|
|
53
|
-
};
|
|
54
|
-
const dom = new JSDOM('<!doctype html><html><body></body></html>');
|
|
55
|
-
result = checkShippedSourcesValidity(
|
|
56
|
-
code => core.hyperscript.compileSync(code),
|
|
57
|
-
dom.window.document as unknown as Parameters<typeof checkShippedSourcesValidity>[1]
|
|
58
|
-
);
|
|
59
|
-
}, 120_000);
|
|
60
|
-
|
|
61
|
-
it('finds and compiles the shipped sources (sanity: trees walked, extraction working)', () => {
|
|
62
|
-
// Guards the silent-zero failure mode: a broken walk or extractor would
|
|
63
|
-
// make every other assertion vacuously pass.
|
|
64
|
-
expect(result.checked).toBeGreaterThan(100);
|
|
65
|
-
expect(result.clean).toBeGreaterThan(100);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('has no NEW source that parses with recovered errors outside the allowlist', () => {
|
|
69
|
-
const unexpected = result.findings.filter(f => !allowed.has(f.key));
|
|
70
|
-
expect(
|
|
71
|
-
unexpected,
|
|
72
|
-
unexpected.length
|
|
73
|
-
? `\nNew shipped sources that parse ok:true WITH errors (fix the source, or allowlist with a reason):\n` +
|
|
74
|
-
unexpected
|
|
75
|
-
.map(f => ` [${f.key}]\n "${f.excerpt}"\n -> ${f.error}`)
|
|
76
|
-
.join('\n') +
|
|
77
|
-
`\n\nAsk the real hyperscript.org engine for a second opinion before deciding:\n` +
|
|
78
|
-
`upstream REJECTING means the source is malformed; upstream ACCEPTING means this is a hyperfixi parser defect.\n` +
|
|
79
|
-
`See loadCanonicalParser() in canonical-validity.ts.`
|
|
80
|
-
: ''
|
|
81
|
-
).toEqual([]);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('has no stale allowlist entries (a now-clean source must be removed so the list ratchets down)', () => {
|
|
85
|
-
const stillFailing = new Set(result.findings.map(f => f.key));
|
|
86
|
-
const stale = allowlist.allowedRecovered.map(e => e.key).filter(key => !stillFailing.has(key));
|
|
87
|
-
expect(
|
|
88
|
-
stale,
|
|
89
|
-
stale.length
|
|
90
|
-
? `\nThese allowlisted sources no longer recover-with-errors (fixed, or edited — the key embeds a source hash).\n` +
|
|
91
|
-
`Remove them from baselines/shipped-sources-validity.json:\n ${stale.join('\n ')}`
|
|
92
|
-
: ''
|
|
93
|
-
).toEqual([]);
|
|
94
|
-
});
|
|
95
|
-
});
|