@hyperfixi/testing-framework 3.1.1 → 3.3.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 +186 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5 -1
- package/dist/index.mjs.map +1 -1
- package/dist/runner.js +5 -1
- package/dist/runner.js.map +1 -1
- package/dist/runner.mjs +5 -1
- package/dist/runner.mjs.map +1 -1
- package/package.json +21 -19
- package/src/multilingual/README.md +39 -0
- package/src/multilingual/en-reference-equivalences.test.ts +321 -0
- package/src/multilingual/en-reference-preservation.test.ts +124 -0
- package/src/multilingual/en-reference-preservation.ts +495 -0
- package/src/multilingual/engine-parser-parity.test.ts +67 -0
- package/src/multilingual/pattern-loader.test.ts +43 -0
- package/src/multilingual/pattern-loader.ts +7 -2
- package/src/multilingual/shipped-examples-execution.test.ts +76 -1
- package/src/multilingual/shipped-examples-execution.ts +73 -3
- package/src/multilingual/shipped-sources-engine.test.ts +106 -0
- package/src/multilingual/shipped-sources-validity.ts +65 -0
- package/src/multilingual/validators/execution-validator.test.ts +32 -3
- package/src/multilingual/validators/execution-validator.ts +22 -5
- package/src/multilingual/value-matrix-gate.ts +118 -0
- package/src/multilingual/value-matrix.accepted.test.ts +65 -0
- package/src/multilingual/value-matrix.assign.test.ts +13 -0
- package/src/multilingual/value-matrix.chain-phrases.test.ts +13 -0
- package/src/multilingual/value-matrix.chain.test.ts +13 -0
- package/src/multilingual/value-matrix.count.test.ts +13 -0
- package/src/multilingual/value-matrix.get-phrases.test.ts +13 -0
- package/src/multilingual/value-matrix.get.test.ts +13 -0
- package/src/multilingual/value-matrix.if-phrases.test.ts +13 -0
- package/src/multilingual/value-matrix.if.test.ts +13 -0
- package/src/multilingual/value-matrix.increment.test.ts +13 -0
- package/src/multilingual/value-matrix.isolation.test.ts +57 -0
- package/src/multilingual/value-matrix.names.test.ts +62 -0
- package/src/multilingual/value-matrix.put-phrases.test.ts +13 -0
- package/src/multilingual/value-matrix.put.test.ts +13 -0
- package/src/multilingual/value-matrix.set-phrases.test.ts +13 -0
- package/src/multilingual/value-matrix.set.test.ts +13 -0
- package/src/multilingual/value-matrix.times.test.ts +13 -0
- package/src/multilingual/value-matrix.ts +1208 -0
- package/src/multilingual/value-matrix.while.test.ts +13 -0
- package/src/runner.ts +7 -1
|
@@ -17,6 +17,12 @@
|
|
|
17
17
|
* 3. no allowlisted key has silently converged (stale entries must be
|
|
18
18
|
* removed so the list only ever ratchets down).
|
|
19
19
|
*
|
|
20
|
+
* The same three run for the ENGINE LANE: `@hyperfixi/engine`, the engine meant
|
|
21
|
+
* to replace core's, against the same oracle on every handler upstream accepts
|
|
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.
|
|
25
|
+
*
|
|
20
26
|
* To update after an intentional change: re-run and regenerate
|
|
21
27
|
* `baselines/shipped-examples-execution.json` (the allowlist key embeds a
|
|
22
28
|
* source hash, so FIXING a handler changes its key and assertion 3 forces the
|
|
@@ -54,6 +60,14 @@ interface AllowlistDoc {
|
|
|
54
60
|
excerpt: string;
|
|
55
61
|
reason: string;
|
|
56
62
|
}>;
|
|
63
|
+
/** The engine lane: `@hyperfixi/engine` against upstream. */
|
|
64
|
+
allowedEngineDivergences: Array<{
|
|
65
|
+
key: string;
|
|
66
|
+
file: string;
|
|
67
|
+
event: string;
|
|
68
|
+
excerpt: string;
|
|
69
|
+
reason: string;
|
|
70
|
+
}>;
|
|
57
71
|
}
|
|
58
72
|
|
|
59
73
|
const baselinePath = path.resolve(
|
|
@@ -62,12 +76,17 @@ const baselinePath = path.resolve(
|
|
|
62
76
|
);
|
|
63
77
|
const allowlist = JSON.parse(readFileSync(baselinePath, 'utf8')) as AllowlistDoc;
|
|
64
78
|
const allowed = new Set(allowlist.allowedDivergences.map(e => e.key));
|
|
79
|
+
const allowedOnEngine = new Set(allowlist.allowedEngineDivergences.map(e => e.key));
|
|
80
|
+
|
|
81
|
+
/** One sweep, shared by both lanes' tests. */
|
|
82
|
+
let sweeping: Promise<ExecutionParityResult> | undefined;
|
|
83
|
+
const sweep = (): Promise<ExecutionParityResult> => (sweeping ??= runShippedExamplesExecution());
|
|
65
84
|
|
|
66
85
|
describe('shipped-examples execution gate', () => {
|
|
67
86
|
let result: ExecutionParityResult;
|
|
68
87
|
|
|
69
88
|
beforeAll(async () => {
|
|
70
|
-
result = await
|
|
89
|
+
result = await sweep();
|
|
71
90
|
|
|
72
91
|
// Visibility, not assertions: what the sweep could not compare, and why.
|
|
73
92
|
// A silently shrinking denominator is this gate's own blind spot.
|
|
@@ -84,6 +103,12 @@ describe('shipped-examples execution gate', () => {
|
|
|
84
103
|
for (const [r, n] of [...reasons].sort((a, b) => b[1] - a[1])) {
|
|
85
104
|
console.log(`[shipped-examples-execution] skip ×${n}: ${r}`);
|
|
86
105
|
}
|
|
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
|
+
);
|
|
87
112
|
}, 240_000);
|
|
88
113
|
|
|
89
114
|
it('walks pages and compares handlers (sanity: extraction and both engines working)', () => {
|
|
@@ -142,6 +167,56 @@ describe('shipped-examples execution gate', () => {
|
|
|
142
167
|
});
|
|
143
168
|
});
|
|
144
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
|
+
const realMatches = result.engineCompared.filter(c => c.match && !c.vacuous).length;
|
|
179
|
+
expect(realMatches).toBeGreaterThan(60);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('has no NEW divergence from upstream outside the allowlist', () => {
|
|
183
|
+
const unexpected = result.engineCompared.filter(c => !c.match && !allowedOnEngine.has(c.key));
|
|
184
|
+
expect(
|
|
185
|
+
unexpected,
|
|
186
|
+
unexpected.length
|
|
187
|
+
? `\nShipped handlers whose DOM effect on @hyperfixi/engine DIVERGES from the hyperscript.org ` +
|
|
188
|
+
`engine\n(the engine follows upstream's source: fix the engine, or allowlist with a reason ` +
|
|
189
|
+
`under allowedEngineDivergences):\n` +
|
|
190
|
+
unexpected
|
|
191
|
+
.map(
|
|
192
|
+
f =>
|
|
193
|
+
` [${f.key}]\n` +
|
|
194
|
+
` "${f.excerpt}"\n` +
|
|
195
|
+
` engine : ${JSON.stringify(f.engineEffects).slice(0, 300)}\n` +
|
|
196
|
+
` upstream: ${JSON.stringify(f.upstreamEffects).slice(0, 300)}`
|
|
197
|
+
)
|
|
198
|
+
.join('\n') +
|
|
199
|
+
`\n\nOne source on both, on one page: npx tsx packages/engine/tools/probe.mts '<source>'`
|
|
200
|
+
: ''
|
|
201
|
+
).toEqual([]);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('has no stale allowlist entries', () => {
|
|
205
|
+
const stillDiverging = new Set(result.engineCompared.filter(c => !c.match).map(c => c.key));
|
|
206
|
+
const stale = allowlist.allowedEngineDivergences
|
|
207
|
+
.map(e => e.key)
|
|
208
|
+
.filter(k => !stillDiverging.has(k));
|
|
209
|
+
expect(
|
|
210
|
+
stale,
|
|
211
|
+
stale.length
|
|
212
|
+
? `\nThese handlers no longer diverge on the engine (or were edited: the key embeds a\n` +
|
|
213
|
+
`source hash). Remove them from allowedEngineDivergences in\n` +
|
|
214
|
+
`baselines/shipped-examples-execution.json:\n ${stale.join('\n ')}`
|
|
215
|
+
: ''
|
|
216
|
+
).toEqual([]);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
145
220
|
describe('harness pieces', () => {
|
|
146
221
|
it('extracts the trigger event from the leading on-clause', () => {
|
|
147
222
|
expect(triggerEventOf('on click add .a to me')).toBe('click');
|
|
@@ -35,6 +35,15 @@
|
|
|
35
35
|
* before/after snapshot around it. See runHandlerOnEngine for why isolation is
|
|
36
36
|
* worth its cost.
|
|
37
37
|
*
|
|
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
|
+
*
|
|
38
47
|
* ## Node-only
|
|
39
48
|
* Imports the real `hyperscript.org` build off disk and swaps jsdom globals
|
|
40
49
|
* per handler execution (both engines resolve `document` lazily through
|
|
@@ -144,6 +153,18 @@ export interface ComparedHandler extends ShippedHandler {
|
|
|
144
153
|
excerpt: string;
|
|
145
154
|
}
|
|
146
155
|
|
|
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
|
+
|
|
147
168
|
export interface ExecutionParityResult {
|
|
148
169
|
/** Pages walked. */
|
|
149
170
|
pages: number;
|
|
@@ -153,6 +174,8 @@ export interface ExecutionParityResult {
|
|
|
153
174
|
compared: ComparedHandler[];
|
|
154
175
|
/** Handlers excluded, each with its reason. */
|
|
155
176
|
skipped: SkippedHandler[];
|
|
177
|
+
/** Handlers upstream accepts, executed on upstream and on `@hyperfixi/engine`. */
|
|
178
|
+
engineCompared: EngineComparedHandler[];
|
|
156
179
|
}
|
|
157
180
|
|
|
158
181
|
/** Stable key for one handler execution. */
|
|
@@ -219,6 +242,10 @@ export interface Engines {
|
|
|
219
242
|
hyperfixiInstall(source: string, el: Element): Promise<void>;
|
|
220
243
|
/** Install a handler on an element via the upstream engine. */
|
|
221
244
|
upstreamInstall(el: Element): void;
|
|
245
|
+
/** `@hyperfixi/engine`'s parse errors for a source; [] = it parses. */
|
|
246
|
+
engineErrors(source: string): string[];
|
|
247
|
+
/** Install a handler on an element via `@hyperfixi/engine`. */
|
|
248
|
+
engineInstall(el: Element): void;
|
|
222
249
|
}
|
|
223
250
|
|
|
224
251
|
/**
|
|
@@ -311,7 +338,20 @@ export async function initEngines(): Promise<Engines> {
|
|
|
311
338
|
processNode(el: Node): void;
|
|
312
339
|
};
|
|
313
340
|
|
|
341
|
+
const engine = await import('@hyperfixi/engine');
|
|
342
|
+
engine.register(...engine.everything);
|
|
343
|
+
|
|
314
344
|
return {
|
|
345
|
+
engineErrors(source) {
|
|
346
|
+
try {
|
|
347
|
+
return engine.api.parse(source).errors.map(e => e.message.split('\n')[0] ?? '');
|
|
348
|
+
} catch (e) {
|
|
349
|
+
return ['threw: ' + (e as Error).message.split('\n')[0]];
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
engineInstall(el) {
|
|
353
|
+
engine.api.processNode(el);
|
|
354
|
+
},
|
|
315
355
|
compileClean(source) {
|
|
316
356
|
try {
|
|
317
357
|
const r = core.hyperscript.compileSync(source);
|
|
@@ -425,6 +465,7 @@ export async function runShippedExamplesExecution(opts?: {
|
|
|
425
465
|
|
|
426
466
|
const compared: ComparedHandler[] = [];
|
|
427
467
|
const skipped: SkippedHandler[] = [];
|
|
468
|
+
const engineCompared: EngineComparedHandler[] = [];
|
|
428
469
|
let pages = 0;
|
|
429
470
|
let handlers = 0;
|
|
430
471
|
|
|
@@ -438,6 +479,9 @@ export async function runShippedExamplesExecution(opts?: {
|
|
|
438
479
|
handlers += pageHandlers.length;
|
|
439
480
|
|
|
440
481
|
const eligible: Array<ShippedHandler & { event: string }> = [];
|
|
482
|
+
// The engine lane's denominator: dispatchable, deterministic, and upstream
|
|
483
|
+
// accepts it. Core's verdict is not part of it.
|
|
484
|
+
const oracle: Array<ShippedHandler & { event: string }> = [];
|
|
441
485
|
for (const h of pageHandlers) {
|
|
442
486
|
if (!h.event) {
|
|
443
487
|
skipped.push({ ...h, reason: 'not an `on <event>` handler' });
|
|
@@ -452,6 +496,8 @@ export async function runShippedExamplesExecution(opts?: {
|
|
|
452
496
|
skipped.push({ ...h, reason: disq.reason });
|
|
453
497
|
continue;
|
|
454
498
|
}
|
|
499
|
+
const upstreamErrs = engines.upstreamErrors(h.source);
|
|
500
|
+
if (upstreamErrs.length === 0) oracle.push(h as ShippedHandler & { event: string });
|
|
455
501
|
if (!engines.compileClean(h.source)) {
|
|
456
502
|
skipped.push({
|
|
457
503
|
...h,
|
|
@@ -459,14 +505,18 @@ export async function runShippedExamplesExecution(opts?: {
|
|
|
459
505
|
});
|
|
460
506
|
continue;
|
|
461
507
|
}
|
|
462
|
-
const upstreamErrs = engines.upstreamErrors(h.source);
|
|
463
508
|
if (upstreamErrs.length > 0) {
|
|
464
509
|
skipped.push({ ...h, reason: `upstream rejects it (no oracle): ${upstreamErrs[0]}` });
|
|
465
510
|
continue;
|
|
466
511
|
}
|
|
467
512
|
eligible.push(h as ShippedHandler & { event: string });
|
|
468
513
|
}
|
|
469
|
-
if (eligible.length === 0) continue;
|
|
514
|
+
if (eligible.length === 0 && oracle.length === 0) continue;
|
|
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[]>();
|
|
470
520
|
|
|
471
521
|
// Both engines log runtime errors to the console during dispatch
|
|
472
522
|
// (COMMAND FAILED etc.). That is expected data here — the effect
|
|
@@ -488,6 +538,7 @@ export async function runShippedExamplesExecution(opts?: {
|
|
|
488
538
|
const theirs = await runHandlerOnEngine(html, eligible, h, (_hh, el) =>
|
|
489
539
|
engines.upstreamInstall(el)
|
|
490
540
|
);
|
|
541
|
+
upstreamRun.set(h, theirs);
|
|
491
542
|
compared.push({
|
|
492
543
|
...h,
|
|
493
544
|
key: keyFor(h),
|
|
@@ -498,6 +549,25 @@ export async function runShippedExamplesExecution(opts?: {
|
|
|
498
549
|
excerpt: h.source.replace(/\s+/g, ' ').trim().slice(0, 100),
|
|
499
550
|
});
|
|
500
551
|
}
|
|
552
|
+
for (const h of oracle) {
|
|
553
|
+
const rejected = engines.engineErrors(h.source)[0];
|
|
554
|
+
const theirs =
|
|
555
|
+
(sameHandlers && upstreamRun.get(h)) ||
|
|
556
|
+
(await runHandlerOnEngine(html, oracle, h, (_hh, el) => engines.upstreamInstall(el)));
|
|
557
|
+
const ours =
|
|
558
|
+
rejected !== undefined
|
|
559
|
+
? [`<engine rejects the source: ${rejected}>`]
|
|
560
|
+
: await runHandlerOnEngine(html, oracle, h, (_hh, el) => engines.engineInstall(el));
|
|
561
|
+
engineCompared.push({
|
|
562
|
+
...h,
|
|
563
|
+
key: keyFor(h),
|
|
564
|
+
engineEffects: ours,
|
|
565
|
+
upstreamEffects: theirs,
|
|
566
|
+
match: JSON.stringify(ours) === JSON.stringify(theirs),
|
|
567
|
+
vacuous: ours.length === 0 && theirs.length === 0,
|
|
568
|
+
excerpt: h.source.replace(/\s+/g, ' ').trim().slice(0, 100),
|
|
569
|
+
});
|
|
570
|
+
}
|
|
501
571
|
} finally {
|
|
502
572
|
console.log = saved.log;
|
|
503
573
|
console.warn = saved.warn;
|
|
@@ -507,5 +577,5 @@ export async function runShippedExamplesExecution(opts?: {
|
|
|
507
577
|
}
|
|
508
578
|
}
|
|
509
579
|
|
|
510
|
-
return { pages, handlers, compared, skipped };
|
|
580
|
+
return { pages, handlers, compared, skipped, engineCompared };
|
|
511
581
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shipped sources on `@hyperfixi/engine`: what replacing core's engine would break.
|
|
3
|
+
*
|
|
4
|
+
* `packages/engine` is meant to replace the engine in `packages/core`. Every
|
|
5
|
+
* hyperscript source this repository ships (`examples/` and the doc trees, the
|
|
6
|
+
* validity gate's collection) that core compiles clean is put to the engine's
|
|
7
|
+
* parser. The ones it rejects are listed in
|
|
8
|
+
* `baselines/shipped-sources-engine.json`, each with the reason it is still
|
|
9
|
+
* there. Three assertions, as the sibling gates:
|
|
10
|
+
* 1. sanity: sources were found and the engine reads most of them;
|
|
11
|
+
* 2. no NEW rejected source outside the list. A page or a doc example
|
|
12
|
+
* written in syntax only core has would break when core's engine goes;
|
|
13
|
+
* write upstream's spelling (`packages/engine/README.md` lists the forms
|
|
14
|
+
* that were considered and not kept), or list it with a reason;
|
|
15
|
+
* 3. no stale entry: a listed source the engine now reads, or one that was
|
|
16
|
+
* edited (the key embeds a hash of the source), must be removed, so the
|
|
17
|
+
* list only shrinks. It is empty when core's engine can be replaced
|
|
18
|
+
* without breaking a shipped page.
|
|
19
|
+
*
|
|
20
|
+
* This is a PARSE-level gate. A source can parse on both engines and run
|
|
21
|
+
* differently; the engine lane of `shipped-examples-execution.test.ts` is the
|
|
22
|
+
* gate for that.
|
|
23
|
+
*
|
|
24
|
+
* To see the list by form, and beside upstream's verdict:
|
|
25
|
+
* `npx tsx packages/engine/tools/shipped-sources.mts`.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { readFileSync } from 'node:fs';
|
|
29
|
+
import { fileURLToPath } from 'node:url';
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
import { JSDOM } from 'jsdom';
|
|
32
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
33
|
+
import {
|
|
34
|
+
checkShippedSourcesOnEngine,
|
|
35
|
+
type CompileForValidity,
|
|
36
|
+
type ShippedSourcesOnEngineResult,
|
|
37
|
+
} from './shipped-sources-validity';
|
|
38
|
+
|
|
39
|
+
interface RejectedDoc {
|
|
40
|
+
rejected: Array<{ key: string; file: string; error: string; excerpt: string; reason: string }>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const baselinePath = path.resolve(
|
|
44
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
45
|
+
'../../baselines/shipped-sources-engine.json'
|
|
46
|
+
);
|
|
47
|
+
const listed = JSON.parse(readFileSync(baselinePath, 'utf8')) as RejectedDoc;
|
|
48
|
+
const allowed = new Set(listed.rejected.map(e => e.key));
|
|
49
|
+
|
|
50
|
+
describe('shipped sources on @hyperfixi/engine', () => {
|
|
51
|
+
let result: ShippedSourcesOnEngineResult;
|
|
52
|
+
|
|
53
|
+
beforeAll(async () => {
|
|
54
|
+
const core = (await import('@hyperfixi/core')) as unknown as {
|
|
55
|
+
hyperscript: { compileSync: CompileForValidity };
|
|
56
|
+
};
|
|
57
|
+
const { api, everything, register } = await import('@hyperfixi/engine');
|
|
58
|
+
register(...everything);
|
|
59
|
+
const dom = new JSDOM('<!doctype html><html><body></body></html>');
|
|
60
|
+
result = checkShippedSourcesOnEngine(
|
|
61
|
+
code => core.hyperscript.compileSync(code),
|
|
62
|
+
code => {
|
|
63
|
+
try {
|
|
64
|
+
return api.parse(code).errors.map(error => error.message);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
return ['threw: ' + (e instanceof Error ? e.message : String(e))];
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
dom.window.document as unknown as Parameters<typeof checkShippedSourcesOnEngine>[2]
|
|
70
|
+
);
|
|
71
|
+
}, 120_000);
|
|
72
|
+
|
|
73
|
+
it('finds the shipped sources and the engine reads most of them (sanity)', () => {
|
|
74
|
+
expect(result.coreClean).toBeGreaterThan(200);
|
|
75
|
+
expect(result.engineAccepts).toBeGreaterThan(200);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('has no NEW shipped source the engine rejects outside the list', () => {
|
|
79
|
+
const unexpected = result.rejections.filter(r => !allowed.has(r.key));
|
|
80
|
+
expect(
|
|
81
|
+
unexpected,
|
|
82
|
+
unexpected.length
|
|
83
|
+
? `\nShipped sources core compiles and @hyperfixi/engine rejects (write upstream's spelling,\n` +
|
|
84
|
+
`or list it in baselines/shipped-sources-engine.json with a reason):\n` +
|
|
85
|
+
unexpected
|
|
86
|
+
.map(r => ` [${r.key}]\n "${r.excerpt}"\n -> ${r.error}`)
|
|
87
|
+
.join('\n') +
|
|
88
|
+
`\n\nOne source on the engine and on upstream, side by side:\n` +
|
|
89
|
+
` npx tsx packages/engine/tools/probe.mts '<source>'`
|
|
90
|
+
: ''
|
|
91
|
+
).toEqual([]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('has no stale entries (a source the engine now reads must be removed, so the list only shrinks)', () => {
|
|
95
|
+
const stillRejected = new Set(result.rejections.map(r => r.key));
|
|
96
|
+
const stale = listed.rejected.map(e => e.key).filter(key => !stillRejected.has(key));
|
|
97
|
+
expect(
|
|
98
|
+
stale,
|
|
99
|
+
stale.length
|
|
100
|
+
? `\nThese listed sources are no longer rejected (read by the engine now, or edited: the key\n` +
|
|
101
|
+
`embeds a hash of the source). Remove them from baselines/shipped-sources-engine.json:\n ` +
|
|
102
|
+
stale.join('\n ')
|
|
103
|
+
: ''
|
|
104
|
+
).toEqual([]);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -231,3 +231,68 @@ export function checkShippedSourcesValidity(
|
|
|
231
231
|
|
|
232
232
|
return { checked: sources.length, clean, findings };
|
|
233
233
|
}
|
|
234
|
+
|
|
235
|
+
/** A shipped source `packages/core` compiles clean and `@hyperfixi/engine` rejects. */
|
|
236
|
+
export interface EngineRejection extends ShippedSource {
|
|
237
|
+
/** Stable key: file plus a hash of the source, as the validity gate's. */
|
|
238
|
+
key: string;
|
|
239
|
+
/** The engine's first parse error, first line. */
|
|
240
|
+
error: string;
|
|
241
|
+
excerpt: string;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export interface ShippedSourcesOnEngineResult {
|
|
245
|
+
/** Distinct (file, source) pairs core compiles clean. */
|
|
246
|
+
coreClean: number;
|
|
247
|
+
/** Of those, the ones the engine parses. */
|
|
248
|
+
engineAccepts: number;
|
|
249
|
+
rejections: EngineRejection[];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* What replacing core's engine with `@hyperfixi/engine` would break in the
|
|
254
|
+
* material we ship: every source core compiles clean, put to the engine's
|
|
255
|
+
* parser. `engineErrors` returns the engine's parse errors ([] = it parses).
|
|
256
|
+
*
|
|
257
|
+
* The denominator is core-clean sources on purpose. A source core itself
|
|
258
|
+
* rejects or recovers from is already broken where it is shipped, and is the
|
|
259
|
+
* validity gate's business; the question here is only what would STOP working.
|
|
260
|
+
*/
|
|
261
|
+
export function checkShippedSourcesOnEngine(
|
|
262
|
+
compile: CompileForValidity,
|
|
263
|
+
engineErrors: (code: string) => string[],
|
|
264
|
+
doc: Parameters<typeof extractHyperscriptFromMarkup>[0],
|
|
265
|
+
opts?: { roots?: string[]; repoRoot?: string }
|
|
266
|
+
): ShippedSourcesOnEngineResult {
|
|
267
|
+
const seen = new Set<string>();
|
|
268
|
+
const rejections: EngineRejection[] = [];
|
|
269
|
+
let coreClean = 0;
|
|
270
|
+
let engineAccepts = 0;
|
|
271
|
+
|
|
272
|
+
for (const s of collectShippedSources(doc, opts)) {
|
|
273
|
+
const key = keyFor(s.file, s.source);
|
|
274
|
+
if (seen.has(key)) continue;
|
|
275
|
+
seen.add(key);
|
|
276
|
+
let clean = false;
|
|
277
|
+
try {
|
|
278
|
+
const result = compile(s.source);
|
|
279
|
+
clean = result.ok && (result.errors ?? []).length === 0;
|
|
280
|
+
} catch {
|
|
281
|
+
/* not core-clean */
|
|
282
|
+
}
|
|
283
|
+
if (!clean) continue;
|
|
284
|
+
coreClean++;
|
|
285
|
+
const errors = engineErrors(s.source);
|
|
286
|
+
if (errors.length === 0) {
|
|
287
|
+
engineAccepts++;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
rejections.push({
|
|
291
|
+
...s,
|
|
292
|
+
key,
|
|
293
|
+
error: (errors[0] ?? '').split('\n')[0] ?? '',
|
|
294
|
+
excerpt: s.source.replace(/\s+/g, ' ').trim().slice(0, 100),
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
return { coreClean, engineAccepts, rejections };
|
|
298
|
+
}
|
|
@@ -19,7 +19,7 @@ import { describe, it, expect, beforeAll } from 'vitest';
|
|
|
19
19
|
import { ExecutionValidator, EXECUTION_SUBSET, loadExecutionSubset } from './execution-validator';
|
|
20
20
|
|
|
21
21
|
describe('R2 execution subset (lock)', () => {
|
|
22
|
-
it('contains exactly the
|
|
22
|
+
it('contains exactly the 49 curated patterns', () => {
|
|
23
23
|
// Changing this list recalibrates avgExecutionFidelity for every language.
|
|
24
24
|
// If you expand the subset, regenerate the baseline (--save-baseline) in
|
|
25
25
|
// the SAME PR and update this lock.
|
|
@@ -128,6 +128,10 @@ describe('R2 execution subset (lock)', () => {
|
|
|
128
128
|
// execution-validator.ts.
|
|
129
129
|
'append-content',
|
|
130
130
|
'increment-by-amount',
|
|
131
|
+
// Wave 12 (hxi18n Arc 3): the book's counter — a property counter with
|
|
132
|
+
// a positional owner, which ran and changed nothing in every language
|
|
133
|
+
// until the semantic increment mapper desugared it to `set X to X + 1`.
|
|
134
|
+
'book-counter-increment',
|
|
131
135
|
].sort()
|
|
132
136
|
);
|
|
133
137
|
});
|
|
@@ -231,12 +235,37 @@ describe('R2 execution validator (lock)', () => {
|
|
|
231
235
|
expect(clickOnly.effects).toEqual([]);
|
|
232
236
|
});
|
|
233
237
|
|
|
238
|
+
it('the book counter increments its <output> in en and in the clitic languages', async () => {
|
|
239
|
+
// Hypermedia Systems ch. 9. The ja/ko renders are owner-first
|
|
240
|
+
// (`前 <output/> の textContent`), which neither parsed nor executed before
|
|
241
|
+
// hxi18n Arc 3; a property counter built by the semantic path also wrote
|
|
242
|
+
// nowhere until the increment mapper desugared it to `set X to X + 1`.
|
|
243
|
+
const en = await validator.execute(
|
|
244
|
+
'book-counter-increment',
|
|
245
|
+
'on click increment the textContent of the previous <output/>',
|
|
246
|
+
'en'
|
|
247
|
+
);
|
|
248
|
+
expect(en.error, en.error).toBeUndefined();
|
|
249
|
+
expect(en.effects).toHaveLength(1);
|
|
250
|
+
expect(en.effects[0]).toMatch(/^Δoutput.*text\[6\]$/);
|
|
251
|
+
for (const [lang, code] of [
|
|
252
|
+
['ja', 'クリック を で 前 <output/> の textContent を 増加'],
|
|
253
|
+
['ko', '클릭 할 때 이전 <output/> 의 textContent 을 증가'],
|
|
254
|
+
] as const) {
|
|
255
|
+
const res = await validator.execute('book-counter-increment', code, lang);
|
|
256
|
+
expect(res.error, `${lang}: ${res.error}`).toBeUndefined();
|
|
257
|
+
expect(res.effects, lang).toEqual(en.effects);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
234
261
|
it('wave-6 en references execute with their locked signatures', async () => {
|
|
235
262
|
// The six wave-6 additions. Each en reference must produce a non-empty,
|
|
236
263
|
// deterministic signature against the existing fixture (the foundation every
|
|
237
264
|
// language is scored against). next/closest positionals fall back to `me`
|
|
238
265
|
// when no match exists in the fixture; set *opacity/*transform write inline
|
|
239
|
-
// style; caret-var-on-target
|
|
266
|
+
// style; caret-var-on-target writes `null` into #btn (the undefined
|
|
267
|
+
// `^count`), as upstream's put writes a null value (PR 54; it used to
|
|
268
|
+
// clear the text).
|
|
240
269
|
const cases: ReadonlyArray<[string, string, string[]]> = [
|
|
241
270
|
[
|
|
242
271
|
'next-element',
|
|
@@ -266,7 +295,7 @@ describe('R2 execution validator (lock)', () => {
|
|
|
266
295
|
[
|
|
267
296
|
'caret-var-on-target',
|
|
268
297
|
'on click put ^count on #host into me',
|
|
269
|
-
['Δ#btn cls[] attr[id=btn] style[] text[]'],
|
|
298
|
+
['Δ#btn cls[] attr[id=btn] style[] text[null]'],
|
|
270
299
|
],
|
|
271
300
|
];
|
|
272
301
|
for (const [id, code, expected] of cases) {
|
|
@@ -72,9 +72,11 @@ export const EXECUTION_SUBSET: readonly string[] = [
|
|
|
72
72
|
// patterns whose en reference now parses (the if/else conditional fold) and
|
|
73
73
|
// executes (propertyAccess evaluator; matches/exists/is-empty condition
|
|
74
74
|
// forms). Probed through this validator: each produces a non-empty,
|
|
75
|
-
// deterministic effect signature. `unless-condition`
|
|
76
|
-
// deliberately NOT folded
|
|
77
|
-
// its flat parse
|
|
75
|
+
// deterministic effect signature. `unless-condition` stayed out while it was
|
|
76
|
+
// core's prefix `unless` (deliberately NOT folded: see
|
|
77
|
+
// semantic-parser.tryParseConditionalBlock; its flat parse errored at
|
|
78
|
+
// runtime). The row is `if I do not match …` since 2026-10-01 and has not
|
|
79
|
+
// been probed for this subset.
|
|
78
80
|
'if-condition',
|
|
79
81
|
'if-matches',
|
|
80
82
|
'if-exists',
|
|
@@ -145,8 +147,8 @@ export const EXECUTION_SUBSET: readonly string[] = [
|
|
|
145
147
|
// stale. No fixture/setup/trigger change needed; each en reference produces a clean
|
|
146
148
|
// non-empty signature against the existing fixture (next/closest positionals fall
|
|
147
149
|
// back to `me` consistently across every language; set *opacity/*transform write
|
|
148
|
-
// inline style; caret-var-on-target
|
|
149
|
-
// the same way in every language).
|
|
150
|
+
// inline style; caret-var-on-target writes `null` into #btn, as upstream's put writes
|
|
151
|
+
// a null value — the undefined `^count` resolves the same way in every language).
|
|
150
152
|
'next-element',
|
|
151
153
|
'toggle-aria-expanded',
|
|
152
154
|
'set-opacity',
|
|
@@ -213,6 +215,14 @@ export const EXECUTION_SUBSET: readonly string[] = [
|
|
|
213
215
|
// Fixture adds `#a`/`#b` with distinguishable content (appended last, so
|
|
214
216
|
// existing snapshot indices are preserved).
|
|
215
217
|
'swap-content',
|
|
218
|
+
// Expansion wave 12 (hxi18n Arc 3): `increment the textContent of the previous
|
|
219
|
+
// <output/>` — Hypermedia Systems ch. 9, verbatim. It parsed lossily in every
|
|
220
|
+
// language (the owner dropped) until the of-possessive matcher took positional
|
|
221
|
+
// owners, and even then it ran and changed nothing on the semantic path: the
|
|
222
|
+
// AST handed `increment` a property access, which the command evaluated and
|
|
223
|
+
// wrote nowhere. The semantic mapper now desugars a property counter to core's
|
|
224
|
+
// own `set X to (X + 1)`. PATTERN_SETUP puts an <output> before #btn.
|
|
225
|
+
'book-counter-increment',
|
|
216
226
|
];
|
|
217
227
|
|
|
218
228
|
/**
|
|
@@ -265,6 +275,13 @@ const PATTERN_SETUP: Record<string, (doc: Document) => void> = {
|
|
|
265
275
|
doc.querySelector('.card')!.classList.add('modal');
|
|
266
276
|
doc.body.classList.add('modal-open');
|
|
267
277
|
},
|
|
278
|
+
// `increment the textContent of the previous <output/>` needs an <output>
|
|
279
|
+
// before #btn, as in the book's counter (`<output>0</output><button …>`).
|
|
280
|
+
'book-counter-increment': doc => {
|
|
281
|
+
const out = doc.createElement('output');
|
|
282
|
+
out.textContent = '5';
|
|
283
|
+
doc.getElementById('btn')!.before(out);
|
|
284
|
+
},
|
|
268
285
|
};
|
|
269
286
|
|
|
270
287
|
/**
|