@hyperfixi/testing-framework 3.3.0 → 4.0.1
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 +199 -1
- package/package.json +9 -10
- 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/orchestrator.ts +2 -1
- package/src/multilingual/render-fidelity.ts +1 -1
- package/src/multilingual/shipped-examples-execution.test.ts +32 -96
- package/src/multilingual/shipped-examples-execution.ts +31 -121
- package/src/multilingual/shipped-sources-engine.test.ts +19 -19
- package/src/multilingual/shipped-sources-localized.test.ts +87 -0
- package/src/multilingual/shipped-sources-validity.ts +87 -99
- package/src/multilingual/validators/execution-validator.test.ts +81 -23
- package/src/multilingual/validators/execution-validator.ts +135 -91
- package/src/multilingual/validators/parse-validator.ts +7 -20
- package/src/multilingual/value-matrix.accepted.test.ts +8 -14
- package/src/multilingual/value-matrix.isolation.test.ts +7 -6
- package/src/multilingual/value-matrix.ts +32 -84
- package/src/multilingual/shipped-sources-validity.test.ts +0 -95
|
@@ -3,25 +3,25 @@
|
|
|
3
3
|
* why and the execution model).
|
|
4
4
|
*
|
|
5
5
|
* Executes every eligible `_="…"` handler shipped in `examples/**` on BOTH
|
|
6
|
-
* hyperfixi and the real `hyperscript.org` engine, in jsdom, and
|
|
7
|
-
* divergence of their DOM effect signatures. Upstream is the
|
|
8
|
-
* oracle — the same role R4 gives it for validity. This is the gate
|
|
9
|
-
* have caught the #785 defect (a conditional body running
|
|
10
|
-
* shipped page) on BEHAVIOR: every parse-level gate stayed
|
|
11
|
-
* shipped.
|
|
6
|
+
* `@hyperfixi/engine` and the real `hyperscript.org` engine, in jsdom, and
|
|
7
|
+
* ratchets on divergence of their DOM effect signatures. Upstream is the
|
|
8
|
+
* behavioral oracle — the same role R4 gives it for validity. This is the gate
|
|
9
|
+
* that would have caught the #785 defect (a conditional body running
|
|
10
|
+
* unconditionally on a shipped page) on BEHAVIOR: every parse-level gate stayed
|
|
11
|
+
* green while it shipped. It is also the gate for a handler that parses on both
|
|
12
|
+
* engines and runs differently, which the parse-level list in
|
|
13
|
+
* shipped-sources-engine.test.ts cannot see.
|
|
12
14
|
*
|
|
13
15
|
* Assertions, matching the shipped-sources gate:
|
|
14
16
|
* 1. sanity — pages walked, handlers extracted, comparisons actually ran
|
|
15
17
|
* (guards the silent-zero failure mode);
|
|
16
|
-
* 2. no NEW divergence appears outside the committed allowlist
|
|
18
|
+
* 2. no NEW divergence appears outside the committed allowlist
|
|
19
|
+
* (`allowedEngineDivergences`);
|
|
17
20
|
* 3. no allowlisted key has silently converged (stale entries must be
|
|
18
21
|
* removed so the list only ever ratchets down).
|
|
19
22
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* (`allowedEngineDivergences` in the same baseline). It is the gate for a
|
|
23
|
-
* handler that parses on both engines and runs differently, which the
|
|
24
|
-
* parse-level list in shipped-sources-engine.test.ts cannot see.
|
|
23
|
+
* (Until Phase C4 the same three also ran for a lane on `@hyperfixi/core`'s
|
|
24
|
+
* runtime, against its own allowlist. It left with core's engine.)
|
|
25
25
|
*
|
|
26
26
|
* To update after an intentional change: re-run and regenerate
|
|
27
27
|
* `baselines/shipped-examples-execution.json` (the allowlist key embeds a
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
* (happy-dom) the DOM constructors already exist on globalThis, the harness's
|
|
38
38
|
* globals bootstrap refuses to overwrite what it does not own, and both
|
|
39
39
|
* engines then bind happy-dom's constructors — every instanceof against a
|
|
40
|
-
* jsdom element fails and every
|
|
41
|
-
* (measured: 0 real matches under happy-dom vs 74 under node).
|
|
40
|
+
* jsdom element fails and every signature comes back empty
|
|
41
|
+
* (measured on core's lane: 0 real matches under happy-dom vs 74 under node).
|
|
42
42
|
*/
|
|
43
43
|
|
|
44
44
|
import { readFileSync } from 'node:fs';
|
|
@@ -53,14 +53,7 @@ import {
|
|
|
53
53
|
} from './shipped-examples-execution';
|
|
54
54
|
|
|
55
55
|
interface AllowlistDoc {
|
|
56
|
-
|
|
57
|
-
key: string;
|
|
58
|
-
file: string;
|
|
59
|
-
event: string;
|
|
60
|
-
excerpt: string;
|
|
61
|
-
reason: string;
|
|
62
|
-
}>;
|
|
63
|
-
/** The engine lane: `@hyperfixi/engine` against upstream. */
|
|
56
|
+
/** `@hyperfixi/engine` against upstream. */
|
|
64
57
|
allowedEngineDivergences: Array<{
|
|
65
58
|
key: string;
|
|
66
59
|
file: string;
|
|
@@ -75,18 +68,13 @@ const baselinePath = path.resolve(
|
|
|
75
68
|
'../../baselines/shipped-examples-execution.json'
|
|
76
69
|
);
|
|
77
70
|
const allowlist = JSON.parse(readFileSync(baselinePath, 'utf8')) as AllowlistDoc;
|
|
78
|
-
const allowed = new Set(allowlist.allowedDivergences.map(e => e.key));
|
|
79
71
|
const allowedOnEngine = new Set(allowlist.allowedEngineDivergences.map(e => e.key));
|
|
80
72
|
|
|
81
|
-
|
|
82
|
-
let sweeping: Promise<ExecutionParityResult> | undefined;
|
|
83
|
-
const sweep = (): Promise<ExecutionParityResult> => (sweeping ??= runShippedExamplesExecution());
|
|
84
|
-
|
|
85
|
-
describe('shipped-examples execution gate', () => {
|
|
73
|
+
describe('shipped-examples execution gate: @hyperfixi/engine against upstream', () => {
|
|
86
74
|
let result: ExecutionParityResult;
|
|
87
75
|
|
|
88
76
|
beforeAll(async () => {
|
|
89
|
-
result = await
|
|
77
|
+
result = await runShippedExamplesExecution();
|
|
90
78
|
|
|
91
79
|
// Visibility, not assertions: what the sweep could not compare, and why.
|
|
92
80
|
// A silently shrinking denominator is this gate's own blind spot.
|
|
@@ -95,26 +83,21 @@ describe('shipped-examples execution gate', () => {
|
|
|
95
83
|
const r = s.reason.split(':')[0] ?? s.reason;
|
|
96
84
|
reasons.set(r, (reasons.get(r) ?? 0) + 1);
|
|
97
85
|
}
|
|
98
|
-
const
|
|
86
|
+
const onEngine = result.engineCompared;
|
|
99
87
|
console.log(
|
|
100
88
|
`[shipped-examples-execution] pages=${result.pages} handlers=${result.handlers} ` +
|
|
101
|
-
`compared=${
|
|
89
|
+
`compared=${onEngine.length} (vacuous=${onEngine.filter(c => c.vacuous).length}) ` +
|
|
90
|
+
`diverging=${onEngine.filter(c => !c.match).length} skipped=${result.skipped.length}`
|
|
102
91
|
);
|
|
103
92
|
for (const [r, n] of [...reasons].sort((a, b) => b[1] - a[1])) {
|
|
104
93
|
console.log(`[shipped-examples-execution] skip ×${n}: ${r}`);
|
|
105
94
|
}
|
|
106
|
-
const onEngine = result.engineCompared;
|
|
107
|
-
console.log(
|
|
108
|
-
`[shipped-examples-execution] engine lane: compared=${onEngine.length} ` +
|
|
109
|
-
`(vacuous=${onEngine.filter(c => c.vacuous).length}) ` +
|
|
110
|
-
`diverging=${onEngine.filter(c => !c.match).length}`
|
|
111
|
-
);
|
|
112
95
|
}, 240_000);
|
|
113
96
|
|
|
114
97
|
it('walks pages and compares handlers (sanity: extraction and both engines working)', () => {
|
|
115
|
-
// Floors well below current values
|
|
116
|
-
//
|
|
117
|
-
//
|
|
98
|
+
// Floors well below current values but far above zero: a broken walk,
|
|
99
|
+
// extractor, or engine bootstrap fails loudly here instead of making
|
|
100
|
+
// assertions 2-3 vacuously pass.
|
|
118
101
|
//
|
|
119
102
|
// Calibrated against the git-TRACKED corpus — the sweep ignores untracked
|
|
120
103
|
// examples/ dirs, so these numbers are the same on every machine and in
|
|
@@ -122,59 +105,9 @@ describe('shipped-examples execution gate', () => {
|
|
|
122
105
|
// gitignored dirs present and failed every clean checkout — #862.)
|
|
123
106
|
expect(result.pages).toBeGreaterThan(35);
|
|
124
107
|
expect(result.handlers).toBeGreaterThan(200);
|
|
125
|
-
expect(result.
|
|
108
|
+
expect(result.engineCompared.length).toBeGreaterThan(90);
|
|
126
109
|
// Vacuous (empty-vs-empty) pairs are NOT parity evidence — the floor is on
|
|
127
110
|
// real, non-empty signature matches.
|
|
128
|
-
const realMatches = result.compared.filter(c => c.match && !c.vacuous).length;
|
|
129
|
-
expect(realMatches).toBeGreaterThan(40);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('has no NEW divergence from upstream outside the allowlist', () => {
|
|
133
|
-
const unexpected = result.compared.filter(c => !c.match && !allowed.has(c.key));
|
|
134
|
-
expect(
|
|
135
|
-
unexpected,
|
|
136
|
-
unexpected.length
|
|
137
|
-
? `\nShipped handlers whose DOM effect DIVERGES from the hyperscript.org engine ` +
|
|
138
|
-
`(fix the behavior, or allowlist with a family reason):\n` +
|
|
139
|
-
unexpected
|
|
140
|
-
.map(
|
|
141
|
-
f =>
|
|
142
|
-
` [${f.key}]\n` +
|
|
143
|
-
` "${f.excerpt}"\n` +
|
|
144
|
-
` hyperfixi: ${JSON.stringify(f.hyperfixiEffects).slice(0, 300)}\n` +
|
|
145
|
-
` upstream : ${JSON.stringify(f.upstreamEffects).slice(0, 300)}`
|
|
146
|
-
)
|
|
147
|
-
.join('\n') +
|
|
148
|
-
`\n\nTriage guidance: an EMPTY hyperfixi signature with a non-empty upstream one usually\n` +
|
|
149
|
-
`means hyperfixi silently dropped behavior (the #785 class). The reverse often means a\n` +
|
|
150
|
-
`deliberate hyperfixi extension or a jsdom limitation on the upstream side — check the\n` +
|
|
151
|
-
`existing family reasons in baselines/shipped-examples-execution.json before adding a new one.`
|
|
152
|
-
: ''
|
|
153
|
-
).toEqual([]);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('has no stale allowlist entries (a now-converged handler must be removed so the list ratchets down)', () => {
|
|
157
|
-
const stillDiverging = new Set(result.compared.filter(c => !c.match).map(c => c.key));
|
|
158
|
-
const stale = allowlist.allowedDivergences.map(e => e.key).filter(k => !stillDiverging.has(k));
|
|
159
|
-
expect(
|
|
160
|
-
stale,
|
|
161
|
-
stale.length
|
|
162
|
-
? `\nThese allowlisted handlers no longer diverge (fixed, or edited — the key embeds a\n` +
|
|
163
|
-
`source hash; or no longer eligible, in which case the coverage loss should be deliberate).\n` +
|
|
164
|
-
`Remove them from baselines/shipped-examples-execution.json:\n ${stale.join('\n ')}`
|
|
165
|
-
: ''
|
|
166
|
-
).toEqual([]);
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
describe('shipped-examples execution gate: @hyperfixi/engine against upstream', () => {
|
|
171
|
-
let result: ExecutionParityResult;
|
|
172
|
-
beforeAll(async () => {
|
|
173
|
-
result = await sweep();
|
|
174
|
-
}, 240_000);
|
|
175
|
-
|
|
176
|
-
it('compares handlers on the engine (sanity: the lane ran and mostly agrees)', () => {
|
|
177
|
-
expect(result.engineCompared.length).toBeGreaterThan(90);
|
|
178
111
|
const realMatches = result.engineCompared.filter(c => c.match && !c.vacuous).length;
|
|
179
112
|
expect(realMatches).toBeGreaterThan(60);
|
|
180
113
|
});
|
|
@@ -196,12 +129,14 @@ describe('shipped-examples execution gate: @hyperfixi/engine against upstream',
|
|
|
196
129
|
` upstream: ${JSON.stringify(f.upstreamEffects).slice(0, 300)}`
|
|
197
130
|
)
|
|
198
131
|
.join('\n') +
|
|
199
|
-
`\n\
|
|
132
|
+
`\n\nTriage guidance: an EMPTY engine signature with a non-empty upstream one usually\n` +
|
|
133
|
+
`means the engine silently dropped behavior (the #785 class).\n` +
|
|
134
|
+
`One source on both, on one page: npx tsx packages/engine/tools/probe.mts '<source>'`
|
|
200
135
|
: ''
|
|
201
136
|
).toEqual([]);
|
|
202
137
|
});
|
|
203
138
|
|
|
204
|
-
it('has no stale allowlist entries', () => {
|
|
139
|
+
it('has no stale allowlist entries (a now-converged handler must be removed so the list ratchets down)', () => {
|
|
205
140
|
const stillDiverging = new Set(result.engineCompared.filter(c => !c.match).map(c => c.key));
|
|
206
141
|
const stale = allowlist.allowedEngineDivergences
|
|
207
142
|
.map(e => e.key)
|
|
@@ -209,9 +144,10 @@ describe('shipped-examples execution gate: @hyperfixi/engine against upstream',
|
|
|
209
144
|
expect(
|
|
210
145
|
stale,
|
|
211
146
|
stale.length
|
|
212
|
-
? `\nThese handlers no longer diverge on the engine (or
|
|
213
|
-
`source hash
|
|
214
|
-
`baselines/shipped-examples-execution.json:\n
|
|
147
|
+
? `\nThese handlers no longer diverge on the engine (fixed, or edited — the key embeds a\n` +
|
|
148
|
+
`source hash; or no longer eligible, in which case the coverage loss should be deliberate).\n` +
|
|
149
|
+
`Remove them from allowedEngineDivergences in baselines/shipped-examples-execution.json:\n ` +
|
|
150
|
+
stale.join('\n ')
|
|
215
151
|
: ''
|
|
216
152
|
).toEqual([]);
|
|
217
153
|
});
|
|
@@ -9,24 +9,27 @@
|
|
|
9
9
|
*
|
|
10
10
|
* This gate executes the handlers we actually ship. For each `_="…"` attribute
|
|
11
11
|
* in `examples/**`, the full page is loaded into two jsdom instances — one
|
|
12
|
-
* processed by hyperfixi
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
12
|
+
* processed by `@hyperfixi/engine`, one by the real `hyperscript.org` engine
|
|
13
|
+
* (`processNode` on both) — the handler's trigger event is dispatched, and the
|
|
14
|
+
* resulting DOM effect signatures (../effect-signature.ts, shared with the R2
|
|
15
|
+
* execution ratchet) are diffed against each other. Upstream plays the role R4
|
|
16
|
+
* gives it for validity: the oracle. A divergence means the engine's runtime
|
|
17
|
+
* behavior differs from upstream's ON A SHIPPED PAGE — exactly the #785/#786
|
|
18
|
+
* failure mode, caught on behavior instead of by luck. The parse-level list of
|
|
19
|
+
* what the engine rejects (shipped-sources-engine.test.ts) cannot see a handler
|
|
20
|
+
* that parses on both and runs differently; this can. A handler upstream
|
|
21
|
+
* accepts and the engine rejects is compared too, and diverges.
|
|
22
|
+
*
|
|
23
|
+
* (Until Phase C4 a second lane ran each handler on `@hyperfixi/core`'s runtime
|
|
24
|
+
* against the same oracle. It left with core's engine; its ten allowlisted
|
|
25
|
+
* divergences were core's.)
|
|
21
26
|
*
|
|
22
27
|
* ## Fair denominator (mirrors R4)
|
|
23
|
-
* A handler is only compared when
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* timer / navigation constructs. Every exclusion is recorded with a reason —
|
|
29
|
-
* the skip list is part of the result, never silent (`no silent caps`).
|
|
28
|
+
* A handler is only compared when upstream parses it (an engine-only form, such
|
|
29
|
+
* as `new X()`, has no oracle). It must also be deterministically executable in
|
|
30
|
+
* jsdom: triggered by a dispatchable event, free of network / timer /
|
|
31
|
+
* navigation constructs. Every exclusion is recorded with a reason — the skip
|
|
32
|
+
* list is part of the result, never silent (`no silent caps`).
|
|
30
33
|
*
|
|
31
34
|
* ## Execution model
|
|
32
35
|
* Per handler and engine: a FRESH jsdom of the whole page, every eligible
|
|
@@ -35,15 +38,6 @@
|
|
|
35
38
|
* before/after snapshot around it. See runHandlerOnEngine for why isolation is
|
|
36
39
|
* worth its cost.
|
|
37
40
|
*
|
|
38
|
-
* ## The engine lane
|
|
39
|
-
* `@hyperfixi/engine` is meant to replace core's engine, so the same sweep
|
|
40
|
-
* runs it against the same oracle: every handler upstream accepts (whether or
|
|
41
|
-
* not core compiles it clean) is executed on upstream and on the engine, and
|
|
42
|
-
* the two effect signatures are compared (`engineCompared`). The parse-level
|
|
43
|
-
* list of what the engine rejects (shipped-sources-engine.test.ts) cannot see
|
|
44
|
-
* a handler that parses on both and runs differently; this can. A handler
|
|
45
|
-
* upstream accepts and the engine rejects is compared too, and diverges.
|
|
46
|
-
*
|
|
47
41
|
* ## Node-only
|
|
48
42
|
* Imports the real `hyperscript.org` build off disk and swaps jsdom globals
|
|
49
43
|
* per handler execution (both engines resolve `document` lazily through
|
|
@@ -135,12 +129,12 @@ export interface SkippedHandler extends ShippedHandler {
|
|
|
135
129
|
reason: string;
|
|
136
130
|
}
|
|
137
131
|
|
|
138
|
-
/** One
|
|
139
|
-
export interface
|
|
132
|
+
/** One handler run on upstream and on `@hyperfixi/engine`; signatures either match or not. */
|
|
133
|
+
export interface EngineComparedHandler extends ShippedHandler {
|
|
140
134
|
/** Stable key: file + source hash + event. Fixing a source changes its key. */
|
|
141
135
|
key: string;
|
|
142
136
|
event: string;
|
|
143
|
-
|
|
137
|
+
engineEffects: string[];
|
|
144
138
|
upstreamEffects: string[];
|
|
145
139
|
match: boolean;
|
|
146
140
|
/**
|
|
@@ -153,25 +147,11 @@ export interface ComparedHandler extends ShippedHandler {
|
|
|
153
147
|
excerpt: string;
|
|
154
148
|
}
|
|
155
149
|
|
|
156
|
-
/** One handler run on upstream and on `@hyperfixi/engine`. */
|
|
157
|
-
export interface EngineComparedHandler extends ShippedHandler {
|
|
158
|
-
key: string;
|
|
159
|
-
event: string;
|
|
160
|
-
engineEffects: string[];
|
|
161
|
-
upstreamEffects: string[];
|
|
162
|
-
match: boolean;
|
|
163
|
-
/** Both signatures empty: not evidence of parity (see ComparedHandler.vacuous). */
|
|
164
|
-
vacuous: boolean;
|
|
165
|
-
excerpt: string;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
150
|
export interface ExecutionParityResult {
|
|
169
151
|
/** Pages walked. */
|
|
170
152
|
pages: number;
|
|
171
153
|
/** Handlers found (before eligibility). */
|
|
172
154
|
handlers: number;
|
|
173
|
-
/** Handlers executed on both engines. */
|
|
174
|
-
compared: ComparedHandler[];
|
|
175
155
|
/** Handlers excluded, each with its reason. */
|
|
176
156
|
skipped: SkippedHandler[];
|
|
177
157
|
/** Handlers upstream accepts, executed on upstream and on `@hyperfixi/engine`. */
|
|
@@ -234,12 +214,8 @@ export function extractHandlers(file: string, html: string): ShippedHandler[] {
|
|
|
234
214
|
|
|
235
215
|
/** Minimal engine surfaces, injected by `initEngines`. */
|
|
236
216
|
export interface Engines {
|
|
237
|
-
/**
|
|
238
|
-
compileClean(source: string): boolean;
|
|
239
|
-
/** upstream parse check (fair-denominator side 2). Returns error strings; [] = valid. */
|
|
217
|
+
/** upstream parse check (the fair denominator). Returns error strings; [] = valid. */
|
|
240
218
|
upstreamErrors(source: string): string[];
|
|
241
|
-
/** Install a handler on an element via hyperfixi (the public eval surface). */
|
|
242
|
-
hyperfixiInstall(source: string, el: Element): Promise<void>;
|
|
243
219
|
/** Install a handler on an element via the upstream engine. */
|
|
244
220
|
upstreamInstall(el: Element): void;
|
|
245
221
|
/** `@hyperfixi/engine`'s parse errors for a source; [] = it parses. */
|
|
@@ -315,22 +291,6 @@ export async function initEngines(): Promise<Engines> {
|
|
|
315
291
|
installRejectionTrap();
|
|
316
292
|
installGlobals(new JSDOM('<!doctype html><html><body></body></html>'));
|
|
317
293
|
|
|
318
|
-
// Install goes through parse + a FRESH Runtime per handler with the element
|
|
319
|
-
// as context — the same shape the browser attribute processor uses, and the
|
|
320
|
-
// same shape the R2 validator proved stable across per-page document swaps.
|
|
321
|
-
// (The api singleton `hyperscript.eval` binds document-dependent state at
|
|
322
|
-
// first use, which is correct in a browser — document identity never changes
|
|
323
|
-
// within a realm — but silently resolves later PAGES' selectors against the
|
|
324
|
-
// first page here. Measured: a two-page probe no-opped page 2's toggle.)
|
|
325
|
-
const core = (await import('@hyperfixi/core')) as unknown as {
|
|
326
|
-
hyperscript: {
|
|
327
|
-
compileSync(code: string): { ok: boolean; errors?: Array<{ message: string }> };
|
|
328
|
-
};
|
|
329
|
-
parse(code: string): { success: boolean; node?: unknown };
|
|
330
|
-
Runtime: new () => { execute(ast: unknown, ctx: unknown): Promise<unknown> };
|
|
331
|
-
createContext(el: HTMLElement): unknown;
|
|
332
|
-
};
|
|
333
|
-
|
|
334
294
|
const require = createRequire(import.meta.url);
|
|
335
295
|
const esm = path.join(path.dirname(require.resolve('hyperscript.org')), '_hyperscript.esm.js');
|
|
336
296
|
const hs = (await import(pathToFileURL(esm).href)).default as {
|
|
@@ -352,14 +312,6 @@ export async function initEngines(): Promise<Engines> {
|
|
|
352
312
|
engineInstall(el) {
|
|
353
313
|
engine.api.processNode(el);
|
|
354
314
|
},
|
|
355
|
-
compileClean(source) {
|
|
356
|
-
try {
|
|
357
|
-
const r = core.hyperscript.compileSync(source);
|
|
358
|
-
return r.ok && (r.errors ?? []).length === 0;
|
|
359
|
-
} catch {
|
|
360
|
-
return false;
|
|
361
|
-
}
|
|
362
|
-
},
|
|
363
315
|
upstreamErrors(source) {
|
|
364
316
|
try {
|
|
365
317
|
return (hs.parse(source)?.errors ?? []).map(e => e.message);
|
|
@@ -367,14 +319,6 @@ export async function initEngines(): Promise<Engines> {
|
|
|
367
319
|
return ['threw: ' + (e as Error).message.split('\n')[0]];
|
|
368
320
|
}
|
|
369
321
|
},
|
|
370
|
-
async hyperfixiInstall(source, el) {
|
|
371
|
-
const parsed = core.parse(source);
|
|
372
|
-
if (!parsed.success || !parsed.node) {
|
|
373
|
-
throw new Error('parse failed at install (eligibility should have caught this)');
|
|
374
|
-
}
|
|
375
|
-
const runtime = new core.Runtime();
|
|
376
|
-
await runtime.execute(parsed.node, core.createContext(el as HTMLElement));
|
|
377
|
-
},
|
|
378
322
|
upstreamInstall(el) {
|
|
379
323
|
hs.processNode(el);
|
|
380
324
|
},
|
|
@@ -452,7 +396,7 @@ async function runHandlerOnEngine(
|
|
|
452
396
|
/**
|
|
453
397
|
* The sweep: walk the git-tracked `examples/**` corpus, extract handlers,
|
|
454
398
|
* apply the fair-denominator filters (each skip reasoned), and execute every
|
|
455
|
-
* eligible handler on
|
|
399
|
+
* eligible handler on upstream and on the engine.
|
|
456
400
|
*/
|
|
457
401
|
export async function runShippedExamplesExecution(opts?: {
|
|
458
402
|
roots?: string[];
|
|
@@ -463,7 +407,6 @@ export async function runShippedExamplesExecution(opts?: {
|
|
|
463
407
|
const roots = opts?.roots ?? DEFAULT_ROOTS;
|
|
464
408
|
const engines = opts?.engines ?? (await initEngines());
|
|
465
409
|
|
|
466
|
-
const compared: ComparedHandler[] = [];
|
|
467
410
|
const skipped: SkippedHandler[] = [];
|
|
468
411
|
const engineCompared: EngineComparedHandler[] = [];
|
|
469
412
|
let pages = 0;
|
|
@@ -478,9 +421,7 @@ export async function runShippedExamplesExecution(opts?: {
|
|
|
478
421
|
pages++;
|
|
479
422
|
handlers += pageHandlers.length;
|
|
480
423
|
|
|
481
|
-
|
|
482
|
-
// The engine lane's denominator: dispatchable, deterministic, and upstream
|
|
483
|
-
// accepts it. Core's verdict is not part of it.
|
|
424
|
+
// The denominator: dispatchable, deterministic, and upstream accepts it.
|
|
484
425
|
const oracle: Array<ShippedHandler & { event: string }> = [];
|
|
485
426
|
for (const h of pageHandlers) {
|
|
486
427
|
if (!h.event) {
|
|
@@ -497,26 +438,13 @@ export async function runShippedExamplesExecution(opts?: {
|
|
|
497
438
|
continue;
|
|
498
439
|
}
|
|
499
440
|
const upstreamErrs = engines.upstreamErrors(h.source);
|
|
500
|
-
if (upstreamErrs.length === 0) oracle.push(h as ShippedHandler & { event: string });
|
|
501
|
-
if (!engines.compileClean(h.source)) {
|
|
502
|
-
skipped.push({
|
|
503
|
-
...h,
|
|
504
|
-
reason: 'hyperfixi does not compile it clean (shipped-sources gate territory)',
|
|
505
|
-
});
|
|
506
|
-
continue;
|
|
507
|
-
}
|
|
508
441
|
if (upstreamErrs.length > 0) {
|
|
509
442
|
skipped.push({ ...h, reason: `upstream rejects it (no oracle): ${upstreamErrs[0]}` });
|
|
510
443
|
continue;
|
|
511
444
|
}
|
|
512
|
-
|
|
445
|
+
oracle.push(h as ShippedHandler & { event: string });
|
|
513
446
|
}
|
|
514
|
-
if (
|
|
515
|
-
// Upstream's signatures from the core lane serve the engine lane too when
|
|
516
|
-
// the two lanes install the same handlers on the page.
|
|
517
|
-
const sameHandlers =
|
|
518
|
-
eligible.length === oracle.length && eligible.every((h, i) => h === oracle[i]);
|
|
519
|
-
const upstreamRun = new Map<ShippedHandler, string[]>();
|
|
447
|
+
if (oracle.length === 0) continue;
|
|
520
448
|
|
|
521
449
|
// Both engines log runtime errors to the console during dispatch
|
|
522
450
|
// (COMMAND FAILED etc.). That is expected data here — the effect
|
|
@@ -531,29 +459,11 @@ export async function runShippedExamplesExecution(opts?: {
|
|
|
531
459
|
const noop = () => {};
|
|
532
460
|
console.log = console.warn = console.error = console.debug = noop;
|
|
533
461
|
try {
|
|
534
|
-
for (const h of eligible) {
|
|
535
|
-
const ours = await runHandlerOnEngine(html, eligible, h, (hh, el) =>
|
|
536
|
-
engines.hyperfixiInstall(hh.source, el)
|
|
537
|
-
);
|
|
538
|
-
const theirs = await runHandlerOnEngine(html, eligible, h, (_hh, el) =>
|
|
539
|
-
engines.upstreamInstall(el)
|
|
540
|
-
);
|
|
541
|
-
upstreamRun.set(h, theirs);
|
|
542
|
-
compared.push({
|
|
543
|
-
...h,
|
|
544
|
-
key: keyFor(h),
|
|
545
|
-
hyperfixiEffects: ours,
|
|
546
|
-
upstreamEffects: theirs,
|
|
547
|
-
match: JSON.stringify(ours) === JSON.stringify(theirs),
|
|
548
|
-
vacuous: ours.length === 0 && theirs.length === 0,
|
|
549
|
-
excerpt: h.source.replace(/\s+/g, ' ').trim().slice(0, 100),
|
|
550
|
-
});
|
|
551
|
-
}
|
|
552
462
|
for (const h of oracle) {
|
|
553
463
|
const rejected = engines.engineErrors(h.source)[0];
|
|
554
|
-
const theirs =
|
|
555
|
-
|
|
556
|
-
|
|
464
|
+
const theirs = await runHandlerOnEngine(html, oracle, h, (_hh, el) =>
|
|
465
|
+
engines.upstreamInstall(el)
|
|
466
|
+
);
|
|
557
467
|
const ours =
|
|
558
468
|
rejected !== undefined
|
|
559
469
|
? [`<engine rejects the source: ${rejected}>`]
|
|
@@ -577,5 +487,5 @@ export async function runShippedExamplesExecution(opts?: {
|
|
|
577
487
|
}
|
|
578
488
|
}
|
|
579
489
|
|
|
580
|
-
return { pages, handlers,
|
|
490
|
+
return { pages, handlers, skipped, engineCompared };
|
|
581
491
|
}
|
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shipped sources on `@hyperfixi/engine`:
|
|
2
|
+
* Shipped sources on `@hyperfixi/engine`: every English source must parse.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* parser. The ones it rejects are listed in
|
|
4
|
+
* Every hyperscript source this repository ships in English (`examples/` and
|
|
5
|
+
* the doc trees; a source under a non-English `lang` is the localized gate's)
|
|
6
|
+
* is put to the engine's parser. The ones it rejects are listed in
|
|
8
7
|
* `baselines/shipped-sources-engine.json`, each with the reason it is still
|
|
9
8
|
* there. Three assertions, as the sibling gates:
|
|
10
9
|
* 1. sanity: sources were found and the engine reads most of them;
|
|
11
10
|
* 2. no NEW rejected source outside the list. A page or a doc example
|
|
12
|
-
* written in syntax
|
|
13
|
-
* write upstream's spelling (`packages/engine/README.md`
|
|
14
|
-
* that were considered and not kept), or list it with a
|
|
11
|
+
* written in syntax the engine lacks (core 3.x's own forms among it)
|
|
12
|
+
* does not run; write upstream's spelling (`packages/engine/README.md`
|
|
13
|
+
* lists the forms that were considered and not kept), or list it with a
|
|
14
|
+
* reason;
|
|
15
15
|
* 3. no stale entry: a listed source the engine now reads, or one that was
|
|
16
16
|
* edited (the key embeds a hash of the source), must be removed, so the
|
|
17
|
-
* list only shrinks. It
|
|
18
|
-
*
|
|
17
|
+
* list only shrinks. It has been empty since the history pages moved to
|
|
18
|
+
* `call history.pushState` (Phase B1).
|
|
19
|
+
*
|
|
20
|
+
* Until Phase C4 the denominator was the sources `@hyperfixi/core` compiled
|
|
21
|
+
* clean (see `checkShippedSourcesOnEngine`).
|
|
19
22
|
*
|
|
20
23
|
* This is a PARSE-level gate. A source can parse on both engines and run
|
|
21
24
|
* differently; the engine lane of `shipped-examples-execution.test.ts` is the
|
|
@@ -32,7 +35,6 @@ import { JSDOM } from 'jsdom';
|
|
|
32
35
|
import { describe, it, expect, beforeAll } from 'vitest';
|
|
33
36
|
import {
|
|
34
37
|
checkShippedSourcesOnEngine,
|
|
35
|
-
type CompileForValidity,
|
|
36
38
|
type ShippedSourcesOnEngineResult,
|
|
37
39
|
} from './shipped-sources-validity';
|
|
38
40
|
|
|
@@ -51,14 +53,10 @@ describe('shipped sources on @hyperfixi/engine', () => {
|
|
|
51
53
|
let result: ShippedSourcesOnEngineResult;
|
|
52
54
|
|
|
53
55
|
beforeAll(async () => {
|
|
54
|
-
const core = (await import('@hyperfixi/core')) as unknown as {
|
|
55
|
-
hyperscript: { compileSync: CompileForValidity };
|
|
56
|
-
};
|
|
57
56
|
const { api, everything, register } = await import('@hyperfixi/engine');
|
|
58
57
|
register(...everything);
|
|
59
58
|
const dom = new JSDOM('<!doctype html><html><body></body></html>');
|
|
60
59
|
result = checkShippedSourcesOnEngine(
|
|
61
|
-
code => core.hyperscript.compileSync(code),
|
|
62
60
|
code => {
|
|
63
61
|
try {
|
|
64
62
|
return api.parse(code).errors.map(error => error.message);
|
|
@@ -66,13 +64,15 @@ describe('shipped sources on @hyperfixi/engine', () => {
|
|
|
66
64
|
return ['threw: ' + (e instanceof Error ? e.message : String(e))];
|
|
67
65
|
}
|
|
68
66
|
},
|
|
69
|
-
dom.window.document as unknown as Parameters<typeof checkShippedSourcesOnEngine>[
|
|
67
|
+
dom.window.document as unknown as Parameters<typeof checkShippedSourcesOnEngine>[1]
|
|
70
68
|
);
|
|
71
69
|
}, 120_000);
|
|
72
70
|
|
|
73
71
|
it('finds the shipped sources and the engine reads most of them (sanity)', () => {
|
|
74
|
-
expect(result.
|
|
75
|
-
expect(result.engineAccepts).toBeGreaterThan(
|
|
72
|
+
expect(result.checked).toBeGreaterThan(300);
|
|
73
|
+
expect(result.engineAccepts).toBeGreaterThan(300);
|
|
74
|
+
// Written in another language: the localized gate's, not this one's.
|
|
75
|
+
expect(result.localized).toBeGreaterThan(10);
|
|
76
76
|
});
|
|
77
77
|
|
|
78
78
|
it('has no NEW shipped source the engine rejects outside the list', () => {
|
|
@@ -80,7 +80,7 @@ describe('shipped sources on @hyperfixi/engine', () => {
|
|
|
80
80
|
expect(
|
|
81
81
|
unexpected,
|
|
82
82
|
unexpected.length
|
|
83
|
-
? `\nShipped sources
|
|
83
|
+
? `\nShipped sources @hyperfixi/engine rejects (write upstream's spelling,\n` +
|
|
84
84
|
`or list it in baselines/shipped-sources-engine.json with a reason):\n` +
|
|
85
85
|
unexpected
|
|
86
86
|
.map(r => ` [${r.key}]\n "${r.excerpt}"\n -> ${r.error}`)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shipped sources written in another language, through the multilingual path.
|
|
3
|
+
*
|
|
4
|
+
* Both shipped-sources gates hand every `_` to an English parser, which
|
|
5
|
+
* rejects a script written in Spanish, so they skip it — and nothing else
|
|
6
|
+
* looked. On a page such a script runs through `@lokascript/hyperscript-
|
|
7
|
+
* adapter`, which translates it under its element's `lang`. This gate does
|
|
8
|
+
* the same and requires English that upstream `_hyperscript` and
|
|
9
|
+
* `@hyperfixi/engine` both parse (Phase C2c; fifteen attributes today: the
|
|
10
|
+
* `live` blocks of examples/hx-v4-i18n/live-multilang.html, the example in
|
|
11
|
+
* docs/BROWSER_BUNDLES.md's "Hyperscript in another language", and the
|
|
12
|
+
* multilingual section of packages/core/docs/EXAMPLES.md).
|
|
13
|
+
*
|
|
14
|
+
* @vitest-environment node
|
|
15
|
+
* Required: the hosts and the collector need real jsdom documents.
|
|
16
|
+
*/
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
import { pathToFileURL } from 'node:url';
|
|
19
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
20
|
+
import { JSDOM } from 'jsdom';
|
|
21
|
+
import { preprocess } from '@lokascript/hyperscript-adapter';
|
|
22
|
+
import { collectLocalizedSources, type LocalizedSource } from './shipped-sources-validity';
|
|
23
|
+
import { installGlobals } from './shipped-examples-execution';
|
|
24
|
+
|
|
25
|
+
interface ParseHost {
|
|
26
|
+
parse(source: string): { errors?: Array<{ message: string }> } | undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('shipped sources written in another language', () => {
|
|
30
|
+
const sources: LocalizedSource[] = collectLocalizedSources();
|
|
31
|
+
let upstream: ParseHost;
|
|
32
|
+
let engine: ParseHost;
|
|
33
|
+
|
|
34
|
+
beforeAll(async () => {
|
|
35
|
+
installGlobals(new JSDOM('<!doctype html><html><body></body></html>'));
|
|
36
|
+
const require = createRequire(import.meta.url);
|
|
37
|
+
const esm = require.resolve('hyperscript.org').replace(/[^/\\]+$/, '_hyperscript.esm.js');
|
|
38
|
+
upstream = (await import(pathToFileURL(esm).href)).default as ParseHost;
|
|
39
|
+
const engineModule = await import('@hyperfixi/engine');
|
|
40
|
+
engineModule.register(...engineModule.everything);
|
|
41
|
+
engine = engineModule.api;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('finds them (a page that moves them changes this count on purpose)', () => {
|
|
45
|
+
expect(sources.map(s => `${s.file} [${s.lang}]`)).toEqual([
|
|
46
|
+
'examples/hx-v4-i18n/live-multilang.html [es]',
|
|
47
|
+
'examples/hx-v4-i18n/live-multilang.html [ja]',
|
|
48
|
+
'examples/hx-v4-i18n/live-multilang.html [ar]',
|
|
49
|
+
// The engine + adapter example in "Hyperscript in another language" (C-R3).
|
|
50
|
+
'docs/BROWSER_BUNDLES.md [ja]',
|
|
51
|
+
// "Multilingual Examples". Marked with `lang` in C4b: core's English parser had
|
|
52
|
+
// kept them out of the engine gate; the engine gate now reads everything else.
|
|
53
|
+
'packages/core/docs/EXAMPLES.md [es]',
|
|
54
|
+
'packages/core/docs/EXAMPLES.md [ja]',
|
|
55
|
+
'packages/core/docs/EXAMPLES.md [ar]',
|
|
56
|
+
'packages/core/docs/EXAMPLES.md [es]',
|
|
57
|
+
'packages/core/docs/EXAMPLES.md [ja]',
|
|
58
|
+
'packages/core/docs/EXAMPLES.md [ko]',
|
|
59
|
+
'packages/core/docs/EXAMPLES.md [es]',
|
|
60
|
+
'packages/core/docs/EXAMPLES.md [fr]',
|
|
61
|
+
'packages/core/docs/EXAMPLES.md [de]',
|
|
62
|
+
'packages/core/docs/EXAMPLES.md [es]',
|
|
63
|
+
'packages/core/docs/EXAMPLES.md [pt]',
|
|
64
|
+
]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('each translates to English upstream and the engine both parse', () => {
|
|
68
|
+
const failures: string[] = [];
|
|
69
|
+
for (const s of sources) {
|
|
70
|
+
let english: string;
|
|
71
|
+
try {
|
|
72
|
+
english = preprocess(s.source, s.lang);
|
|
73
|
+
} catch (e) {
|
|
74
|
+
failures.push(`${s.file} [${s.lang}] threw: ${(e as Error).message}`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const first = (host: ParseHost) => host.parse(english)?.errors?.[0]?.message.split('\n')[0];
|
|
78
|
+
const problem =
|
|
79
|
+
english === s.source
|
|
80
|
+
? 'not translated'
|
|
81
|
+
: (first(upstream) && `upstream: ${first(upstream)}`) ||
|
|
82
|
+
(first(engine) && `engine: ${first(engine)}`);
|
|
83
|
+
if (problem) failures.push(`${s.file} [${s.lang}] ${problem}\n ${s.source}\n → ${english}`);
|
|
84
|
+
}
|
|
85
|
+
expect(failures).toEqual([]);
|
|
86
|
+
});
|
|
87
|
+
});
|