@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.
Files changed (29) hide show
  1. package/CHANGELOG.md +203 -1
  2. package/package.json +9 -9
  3. package/src/agent-bench/README.md +48 -1
  4. package/src/agent-bench/agent-bench.test.ts +7 -0
  5. package/src/agent-bench/harness.ts +39 -50
  6. package/src/multilingual/README.md +30 -11
  7. package/src/multilingual/bare-render-fidelity.ts +1 -1
  8. package/src/multilingual/cli.ts +5 -3
  9. package/src/multilingual/direct-path-shapes-gate.ts +56 -0
  10. package/src/multilingual/direct-path-shapes.1.test.ts +12 -0
  11. package/src/multilingual/direct-path-shapes.2.test.ts +12 -0
  12. package/src/multilingual/direct-path-shapes.3.test.ts +12 -0
  13. package/src/multilingual/direct-path-shapes.cases.json +2621 -0
  14. package/src/multilingual/direct-path-shapes.ts +459 -0
  15. package/src/multilingual/engine-parser-parity.test.ts +67 -0
  16. package/src/multilingual/orchestrator.ts +2 -1
  17. package/src/multilingual/render-fidelity.ts +1 -1
  18. package/src/multilingual/shipped-examples-execution.test.ts +44 -33
  19. package/src/multilingual/shipped-examples-execution.ts +55 -75
  20. package/src/multilingual/shipped-sources-engine.test.ts +106 -0
  21. package/src/multilingual/shipped-sources-localized.test.ts +87 -0
  22. package/src/multilingual/shipped-sources-validity.ts +127 -74
  23. package/src/multilingual/validators/execution-validator.test.ts +81 -23
  24. package/src/multilingual/validators/execution-validator.ts +140 -94
  25. package/src/multilingual/validators/parse-validator.ts +7 -20
  26. package/src/multilingual/value-matrix.accepted.test.ts +8 -11
  27. package/src/multilingual/value-matrix.isolation.test.ts +7 -6
  28. package/src/multilingual/value-matrix.ts +72 -86
  29. package/src/multilingual/shipped-sources-validity.test.ts +0 -95
@@ -5,33 +5,46 @@
5
5
  * parse that EXECUTES wrongly: the session-5 probe found translations whose
6
6
  * parse looks plausible but whose runtime behavior diverges from the English
7
7
  * reference — a dropped destination role lands the effect on `me` instead of
8
- * `#menu`, a mis-built AST throws `Unknown command: into`, a quoted literal
9
- * keeps its quotes in the DOM. This validator executes a CURATED subset of
10
- * corpus patterns in jsdom (semantic parse → buildAST → runtime.execute →
11
- * dispatch the trigger event) and compares the resulting DOM effects against
12
- * the English reference's effects.
8
+ * `#menu`, a quoted literal keeps its quotes in the DOM. This validator
9
+ * executes a CURATED subset of corpus patterns in jsdom and compares the
10
+ * resulting DOM effects against the English reference's effects.
11
+ *
12
+ * What runs where (since Phase C2 of the engine cutover): the en reference is
13
+ * the English source on upstream `_hyperscript` (the oracle, as for the value
14
+ * matrix and the shipped-examples gate); a translation is its foreign text
15
+ * AS WRITTEN on `@hyperfixi/engine` with `@lokascript/hyperscript-adapter`'s
16
+ * plugin installed and `lang` on the element — the product's multilingual
17
+ * path, where the engine translates the script as it reads it. Until C2 both
18
+ * ran on core's runtime through `parseSemantic → buildAST` (the direct path,
19
+ * which retires with core's parser). Measured before the switch, on a fresh
20
+ * populate: the engine matched upstream on all 49 en references, and all
21
+ * 1,127 translations matched on the engine; core's R2 path differed from
22
+ * upstream on 13 en references (core-only `show`/`toggle @attr`/`with
23
+ * *opacity`/`swap` semantics), which is why the oracle is upstream.
13
24
  *
14
25
  * Effect signature: a before/after diff of every element's classes, attributes,
15
- * inline style, and leaf text. Selectors/classes/attribute names are code (not
16
- * translated), so signatures are directly comparable across languages. A
17
- * pattern scores 1 when its signature EXACTLY matches the en reference's
18
- * (binary — extra effects are as wrong as missing ones). Trapped runtime
19
- * errors are recorded for diagnostics but are NOT part of the match: their
20
- * attribution depends on unhandled-rejection timing (racy), while the effect
21
- * snapshot is synchronous and deterministic. A mis-built AST that damages
22
- * behavior diverges in its effects regardless.
26
+ * inline style, and leaf text, plus (here only) the document order of the
27
+ * id'd elements when it changed — upstream's `swap #a with #b` exchanges the
28
+ * two elements and changes nothing else. Selectors/classes/attribute names are
29
+ * code (not translated), so signatures are directly comparable across
30
+ * languages. A pattern scores 1 when its signature EXACTLY matches the en
31
+ * reference's (binary — extra effects are as wrong as missing ones). Errors
32
+ * the host reports are recorded for diagnostics but are NOT part of the match;
33
+ * a script that damages behavior diverges in its effects regardless.
23
34
  *
24
- * Determinism: a fresh JSDOM document and a fresh Runtime per execution, no
25
- * network, no timers beyond a fixed settle window; the probe measured two full
26
- * sweeps byte-identical. Patterns whose en reference errors or produces no
27
- * effects are excluded (no usable reference — same semantics as R0/R1).
35
+ * Determinism: a fresh JSDOM document per execution, no network, no timers
36
+ * beyond a fixed settle window. Patterns whose en reference errors or produces
37
+ * no effects are excluded (no usable reference — same semantics as R0/R1).
28
38
  */
29
39
 
40
+ import { createRequire } from 'node:module';
41
+ import path from 'node:path';
42
+ import { pathToFileURL } from 'node:url';
30
43
  import { JSDOM } from 'jsdom';
31
- import { parseSemantic, buildAST } from '@lokascript/semantic';
32
44
  import { getAllPatterns, getTranslationsByLanguage } from '@hyperfixi/patterns-reference';
33
45
  import type { LanguageCode } from '../types';
34
46
  import { snapshot, diffSnapshots } from '../effect-signature';
47
+ import { installGlobals } from '../shipped-examples-execution';
35
48
 
36
49
  /**
37
50
  * The curated execution subset: simple, deterministic, fixture-friendly
@@ -72,9 +85,11 @@ export const EXECUTION_SUBSET: readonly string[] = [
72
85
  // patterns whose en reference now parses (the if/else conditional fold) and
73
86
  // executes (propertyAccess evaluator; matches/exists/is-empty condition
74
87
  // forms). Probed through this validator: each produces a non-empty,
75
- // deterministic effect signature. `unless-condition` stays out — `unless` is
76
- // deliberately NOT folded (see semantic-parser.tryParseConditionalBlock), so
77
- // its flat parse still errors at runtime.
88
+ // deterministic effect signature. `unless-condition` stayed out while it was
89
+ // core's prefix `unless` (deliberately NOT folded: see
90
+ // semantic-parser.tryParseConditionalBlock; its flat parse errored at
91
+ // runtime). The row is `if I do not match …` since 2026-10-01 and has not
92
+ // been probed for this subset.
78
93
  'if-condition',
79
94
  'if-matches',
80
95
  'if-exists',
@@ -228,7 +243,7 @@ export const EXECUTION_SUBSET: readonly string[] = [
228
243
  * The SAME fixture (plus the same per-pattern setup) is used for the en
229
244
  * reference and every translation, so signatures are comparable.
230
245
  */
231
- const FIXTURE_HTML = `<!DOCTYPE html><html><body>
246
+ export const FIXTURE_HTML = `<!DOCTYPE html><html><body>
232
247
  <div class="card"><button id="btn">Click</button></div>
233
248
  <div id="item"></div>
234
249
  <div id="menu"></div>
@@ -252,7 +267,7 @@ const FIXTURE_HTML = `<!DOCTYPE html><html><body>
252
267
  </body></html>`;
253
268
 
254
269
  /** Per-pattern fixture preconditions (applied identically for every language). */
255
- const PATTERN_SETUP: Record<string, (doc: Document) => void> = {
270
+ export const PATTERN_SETUP: Record<string, (doc: Document) => void> = {
256
271
  // `remove .highlight from me` needs the class present to have an effect.
257
272
  'remove-class-basic': doc => doc.getElementById('btn')!.classList.add('highlight'),
258
273
  // `show #modal` needs the modal hidden.
@@ -280,6 +295,19 @@ const PATTERN_SETUP: Record<string, (doc: Document) => void> = {
280
295
  out.textContent = '5';
281
296
  doc.getElementById('btn')!.before(out);
282
297
  },
298
+ // `if #modal exists show #modal …` changes nothing on upstream while #modal
299
+ // is visible (core's `show` added a `show` class, which is what the
300
+ // reference used to record). Hidden, the `show` is observable.
301
+ 'if-exists': doc => {
302
+ (doc.getElementById('modal') as HTMLElement).style.display = 'none';
303
+ },
304
+ // `toggle .open on closest .accordion-item` throws on upstream when there is
305
+ // no such ancestor (core fell back to `me`), and the second command never
306
+ // runs. #btn inside an item, as on a real accordion header.
307
+ 'accordion-toggle': doc => doc.querySelector('.card')!.classList.add('accordion-item'),
308
+ // `toggle .open on next .panel` throws on upstream when nothing follows
309
+ // (core fell back to `me`), so the reference errored and was excluded.
310
+ 'toggle-aria-expanded': doc => doc.getElementById('panel')!.classList.add('panel'),
283
311
  };
284
312
 
285
313
  /**
@@ -292,11 +320,11 @@ const PATTERN_SETUP: Record<string, (doc: Document) => void> = {
292
320
  * reference and every translation (it is harness-supplied, not translated), so
293
321
  * effect signatures stay comparable across languages.
294
322
  */
295
- interface PatternTrigger {
323
+ export interface PatternTrigger {
296
324
  readonly event: string;
297
325
  readonly detail?: unknown;
298
326
  }
299
- const PATTERN_TRIGGER: Record<string, PatternTrigger> = {
327
+ export const PATTERN_TRIGGER: Record<string, PatternTrigger> = {
300
328
  // `on success put event.detail.message into #sr-announce set @role to "alert"
301
329
  // on #sr-announce` — a custom event carrying the announcement text. The fixed
302
330
  // message lands in #sr-announce's text; role=alert is set on the same node.
@@ -309,18 +337,27 @@ export interface ExecutionResult {
309
337
  language: LanguageCode;
310
338
  /** Sorted effect lines (see snapshot/diff). Empty on error or no effect. */
311
339
  effects: string[];
312
- /** Parse/build/runtime error, when any. */
340
+ /** Errors the host reported (parse or runtime), when any. */
313
341
  error?: string;
314
342
  /** 1 = exact signature match vs the en reference, 0 = mismatch. Undefined for en itself or when no usable reference exists. */
315
343
  executionFidelity?: number;
316
344
  }
317
345
 
318
- // Effect-signature machinery (serializeElement/snapshot/diffSnapshots) moved to
319
- // ../effect-signature.ts (imported at the top), shared with the
320
- // shipped-examples execution gate so the two can never disagree about what a
321
- // DOM effect is. Notes that used to live on the local copies (leaf-text-only
322
- // rationale; body's own stable key, added in wave 3b when the runtime learned
323
- // to resolve `body`) moved with them.
346
+ // Effect-signature machinery (serializeElement/snapshot/diffSnapshots) lives in
347
+ // ../effect-signature.ts, shared with the shipped-examples execution gate so the
348
+ // two can never disagree about what a DOM effect is.
349
+
350
+ /**
351
+ * Where an execution runs: `engine` is `@hyperfixi/engine` with the
352
+ * multilingual plugin (English or any language); `upstream` is
353
+ * `hyperscript.org`, the oracle, which runs English only.
354
+ */
355
+ export type ExecutionHost = 'engine' | 'upstream';
356
+
357
+ /** What this needs of a host that reads scripts off attributes. */
358
+ interface ScriptHost {
359
+ processNode(element: Element): void;
360
+ }
324
361
 
325
362
  /** How long to let the dispatched handler settle (ms). The subset contains no
326
363
  * waits/transitions/fetches, so this only needs to drain micro/macrotasks. */
@@ -329,42 +366,53 @@ const SETTLE_MS = 20;
329
366
  /** Per-execution hard timeout — a hung execution must not hang the sweep. */
330
367
  const EXECUTION_TIMEOUT_MS = 5000;
331
368
 
369
+ /**
370
+ * Both hosts, loaded once per process: the engine's grammar is a module
371
+ * singleton, and `register` re-runs every module against it.
372
+ */
373
+ let hostsLoading: Promise<Record<ExecutionHost, ScriptHost>> | null = null;
374
+ function loadHosts(): Promise<Record<ExecutionHost, ScriptHost>> {
375
+ hostsLoading ??= (async () => {
376
+ const engine = await import('@hyperfixi/engine');
377
+ engine.register(...engine.everything);
378
+ const { hyperscriptI18n } = await import('@lokascript/hyperscript-adapter');
379
+ engine.api.use(hyperscriptI18n());
380
+ const require = createRequire(import.meta.url);
381
+ const esm = path.join(path.dirname(require.resolve('hyperscript.org')), '_hyperscript.esm.js');
382
+ const upstream = (await import(pathToFileURL(esm).href)).default as ScriptHost;
383
+ return { engine: engine.api, upstream };
384
+ })();
385
+ return hostsLoading;
386
+ }
387
+
388
+ /** The ids of id'd elements in document order — what `swap` changes. */
389
+ function idOrder(document: Document): string {
390
+ return Array.from(document.body.querySelectorAll('[id]'), el => el.id).join(',');
391
+ }
392
+
332
393
  /**
333
394
  * R2 execution validator. `initialize()` MUST complete before `execute()`:
334
- * it bootstraps jsdom globals and only then loads `@hyperfixi/core`, whose
335
- * module evaluation touches `Element`/`document`.
395
+ * it bootstraps jsdom globals and only then loads the hosts, whose module
396
+ * evaluation touches DOM constructors.
336
397
  */
337
398
  export class ExecutionValidator {
338
- private core: {
339
- Runtime: new () => { execute(ast: unknown, ctx: unknown): Promise<unknown> };
340
- createContext: (el: HTMLElement) => unknown;
341
- } | null = null;
399
+ private hosts: Record<ExecutionHost, ScriptHost> | null = null;
342
400
 
343
- /** Errors thrown inside dispatched listeners surface as unhandled rejections
344
- * (the handler is async); trap them into the current execution's sink. */
345
- private listenerErrors: string[] = [];
401
+ /** Errors the hosts report during the current execution: parse and runtime
402
+ * errors go to console.error, and an async handler's rejection to the
403
+ * process. */
404
+ private hostErrors: string[] = [];
346
405
  private trapInstalled = false;
347
406
 
348
407
  async initialize(): Promise<void> {
349
- if (this.core) return;
350
-
351
- // Bootstrap DOM globals BEFORE importing @hyperfixi/core (its dist
352
- // evaluates `Element` at module load). The per-execution fresh document
353
- // replaces these for each run.
354
- this.installGlobals(new JSDOM(FIXTURE_HTML));
355
-
356
- const core = await import('@hyperfixi/core');
357
- this.core = {
358
- Runtime: core.Runtime as unknown as new () => {
359
- execute(ast: unknown, ctx: unknown): Promise<unknown>;
360
- },
361
- createContext: core.createContext as unknown as (el: HTMLElement) => unknown,
362
- };
408
+ if (this.hosts) return;
409
+ installGlobals(new JSDOM(FIXTURE_HTML));
410
+ this.hosts = await loadHosts();
363
411
 
364
412
  if (!this.trapInstalled) {
365
413
  process.on('unhandledRejection', (reason: unknown) => {
366
414
  const msg = reason instanceof Error ? reason.message : String(reason);
367
- this.listenerErrors.push(msg);
415
+ this.hostErrors.push(msg.split('\n')[0] ?? msg);
368
416
  });
369
417
  this.trapInstalled = true;
370
418
  }
@@ -374,25 +422,17 @@ export class ExecutionValidator {
374
422
  return 'ExecutionValidator';
375
423
  }
376
424
 
377
- private installGlobals(dom: JSDOM): void {
378
- const g = globalThis as Record<string, unknown>;
379
- g.window = dom.window;
380
- g.document = dom.window.document;
381
- g.Event = dom.window.Event;
382
- g.CustomEvent = dom.window.CustomEvent;
383
- g.HTMLElement = dom.window.HTMLElement;
384
- g.Element = dom.window.Element;
385
- g.Node = dom.window.Node;
386
- g.MutationObserver = dom.window.MutationObserver;
387
- g.getComputedStyle = dom.window.getComputedStyle;
388
- }
389
-
390
425
  /**
391
- * Execute one pattern translation in a fresh fixture and return its effect
426
+ * Execute one pattern's source in a fresh fixture and return its effect
392
427
  * signature. Never throws; failures come back as `error`.
393
428
  */
394
- async execute(codeExampleId: string, code: string, lang: string): Promise<ExecutionResult> {
395
- const run = this.executeInner(codeExampleId, code, lang);
429
+ async execute(
430
+ codeExampleId: string,
431
+ code: string,
432
+ lang: string,
433
+ host: ExecutionHost = 'engine'
434
+ ): Promise<ExecutionResult> {
435
+ const run = this.executeInner(codeExampleId, code, lang, host);
396
436
  const timeout = new Promise<ExecutionResult>(resolve =>
397
437
  setTimeout(
398
438
  () =>
@@ -411,40 +451,44 @@ export class ExecutionValidator {
411
451
  private async executeInner(
412
452
  codeExampleId: string,
413
453
  code: string,
414
- lang: string
454
+ lang: string,
455
+ host: ExecutionHost
415
456
  ): Promise<ExecutionResult> {
416
- if (!this.core) await this.initialize();
457
+ if (!this.hosts) await this.initialize();
417
458
  const base: ExecutionResult = {
418
459
  codeExampleId,
419
460
  language: lang as LanguageCode,
420
461
  effects: [],
421
462
  };
463
+ if (host === 'upstream' && lang !== 'en') {
464
+ return { ...base, error: 'upstream runs English only' };
465
+ }
422
466
 
423
467
  const dom = new JSDOM(FIXTURE_HTML);
424
- this.installGlobals(dom);
468
+ installGlobals(dom);
425
469
  const document = dom.window.document;
426
470
  PATTERN_SETUP[codeExampleId]?.(document);
427
471
  const btn = document.getElementById('btn')!;
428
- this.listenerErrors = [];
472
+ // The plugin reads the language off the closest `lang`, as on a page; on
473
+ // <html>, outside the snapshot, so it is no part of any signature.
474
+ if (lang !== 'en') document.documentElement.setAttribute('lang', lang);
475
+ btn.setAttribute('_', code);
476
+ this.hostErrors = [];
429
477
 
430
- // The runtime logs every failing command via console — across a full
431
- // multi-language sweep that is pure noise; silence it for the execution.
478
+ // Both hosts log every failing script via console — across a full
479
+ // multi-language sweep that is noise; keep only the errors, as diagnostics.
432
480
  const saved = { log: console.log, warn: console.warn, error: console.error };
433
- console.log = console.warn = console.error = () => {};
481
+ console.log = console.warn = () => {};
482
+ console.error = (...args: unknown[]) => {
483
+ const first = args.map(String).join(' ').split('\n')[0] ?? '';
484
+ this.hostErrors.push(first);
485
+ };
434
486
  try {
435
- const parsed = parseSemantic(code, lang);
436
- if (!parsed.node || parsed.confidence < 0.5) {
437
- return { ...base, error: `parse failed (confidence ${parsed.confidence?.toFixed(2)})` };
438
- }
439
- const built = buildAST(parsed.node);
440
- if (!built.ast) return { ...base, error: 'buildAST returned no AST' };
441
-
442
- const runtime = new this.core!.Runtime();
443
- const ctx = this.core!.createContext(btn as unknown as HTMLElement);
444
- // Installs the event handler (or runs bare commands immediately).
445
- await runtime.execute(built.ast, ctx);
487
+ this.hosts![host].processNode(btn);
488
+ await new Promise(r => setTimeout(r, 0));
446
489
 
447
490
  const before = snapshot(document);
491
+ const orderBefore = idOrder(document);
448
492
  const trigger = PATTERN_TRIGGER[codeExampleId];
449
493
  const triggerEvent =
450
494
  trigger?.detail !== undefined
@@ -452,11 +496,13 @@ export class ExecutionValidator {
452
496
  : new dom.window.Event(trigger?.event ?? 'click', { bubbles: true });
453
497
  btn.dispatchEvent(triggerEvent);
454
498
  await new Promise(r => setTimeout(r, SETTLE_MS));
455
- const after = snapshot(document);
456
499
 
457
- const effects = diffSnapshots(before, after);
458
- if (this.listenerErrors.length > 0) {
459
- return { ...base, effects, error: `runtime: ${this.listenerErrors.join('; ')}` };
500
+ const effects = diffSnapshots(before, snapshot(document));
501
+ const orderAfter = idOrder(document);
502
+ if (orderAfter !== orderBefore) effects.push(`order[${orderAfter}]`);
503
+ effects.sort();
504
+ if (this.hostErrors.length > 0) {
505
+ return { ...base, effects, error: this.hostErrors.join('; ') };
460
506
  }
461
507
  return { ...base, effects };
462
508
  } catch (e: unknown) {
@@ -2,9 +2,8 @@
2
2
  * Parse Validator - Validates hyperscript parsing across languages
3
3
  */
4
4
 
5
- import { MultilingualHyperscript } from '@hyperfixi/core/multilingual';
6
5
  import type { SemanticNode } from '@lokascript/semantic';
7
- import { fillSchemaDefaults } from '@lokascript/semantic';
6
+ import { fillSchemaDefaults, parseSemantic } from '@lokascript/semantic';
8
7
  import type { PatternTranslation, ParseResult, Validator } from '../types';
9
8
  import {
10
9
  collectActions,
@@ -19,21 +18,12 @@ import {
19
18
  * Validates that hyperscript patterns parse correctly using the semantic parser.
20
19
  */
21
20
  export class ParseValidator implements Validator<ParseResult[]> {
22
- private ml: MultilingualHyperscript;
23
- private initialized = false;
24
-
25
- constructor() {
26
- this.ml = new MultilingualHyperscript();
27
- }
28
-
29
21
  /**
30
- * Initialize the validator
22
+ * Initialize the validator. Nothing to load: the semantic parser is a
23
+ * static import (it was reached through core's MultilingualHyperscript,
24
+ * whose `parse` was this same call, until Phase C2).
31
25
  */
32
- async initialize(): Promise<void> {
33
- if (this.initialized) return;
34
- await this.ml.initialize();
35
- this.initialized = true;
36
- }
26
+ async initialize(): Promise<void> {}
37
27
 
38
28
  /**
39
29
  * Get validator name
@@ -65,11 +55,8 @@ export class ParseValidator implements Validator<ParseResult[]> {
65
55
  const startTime = performance.now();
66
56
 
67
57
  try {
68
- // Parse the hyperscript
69
- const semanticNode = (await this.ml.parse(
70
- pattern.hyperscript,
71
- pattern.language
72
- )) as SemanticNode;
58
+ // Parse the hyperscript. Confidence is not filtered here.
59
+ const semanticNode = parseSemantic(pattern.hyperscript, pattern.language).node;
73
60
 
74
61
  if (!semanticNode) {
75
62
  return {
@@ -16,36 +16,33 @@ import {
16
16
  import { loadBaseline } from './value-matrix-gate';
17
17
 
18
18
  describe('value matrix: accepted pairs', () => {
19
- const [known, ambiguity] = ACCEPTED;
19
+ const [ambiguity] = ACCEPTED;
20
20
 
21
21
  it('tags a cell whose failing lanes are all accepted', () => {
22
- expect(acceptedReason('put|the textContent of #a as Int', ['en', 'es', 'ja'])).toBe(
23
- known!.reason
24
- );
25
- expect(acceptedReason('increment|#a.textContent', ['it', 'it/up'])).toBe(ambiguity!.reason);
22
+ expect(acceptedReason('increment|#a.textContent', ['it/up'])).toBe(ambiguity!.reason);
23
+ expect(acceptedReason('increment|#a.textContent', ['it/up', 'it/eng'])).toBe(ambiguity!.reason);
26
24
  });
27
25
 
28
26
  it('leaves it open when one failing lane is not', () => {
29
- expect(acceptedReason('increment|#a.textContent', ['it', 'es'])).toBeUndefined();
30
- expect(acceptedReason('put|the textContent of #a as Int', ['en', 'es/up'])).toBeUndefined();
27
+ expect(acceptedReason('increment|#a.textContent', ['it/up', 'es/up'])).toBeUndefined();
31
28
  });
32
29
 
33
30
  it('a regenerated baseline carries the reason, and counts the pairs', () => {
34
31
  const result = (lanes: Record<string, string>): CellResult => ({
35
32
  id: 'increment|#a.textContent',
36
33
  want: '7',
37
- lanes: { en: '7', es: '7', ...lanes },
34
+ lanes: { eng: '7', 'es/up': '7', ...lanes },
38
35
  });
39
- const kept = baselineFrom([result({ it: '1', 'it/up': '1' })], '');
36
+ const kept = baselineFrom([result({ 'it/up': '1', 'it/eng': '1' })], '');
40
37
  expect(kept.entries['increment|#a.textContent']?.accepted).toBe(ambiguity!.reason);
41
38
  expect(kept.accepted).toBe(2);
42
- const open = baselineFrom([result({ it: '1', es: '1' })], '');
39
+ const open = baselineFrom([result({ 'it/up': '1', 'es/up': '1' })], '');
43
40
  expect(open.entries['increment|#a.textContent']?.accepted).toBeUndefined();
44
41
  expect(open.accepted).toBe(0);
45
42
  });
46
43
 
47
44
  it('accepts only the cells it names', () => {
48
- expect(acceptedReason('put|textContent of #a', ['en'])).toBeUndefined();
45
+ expect(acceptedReason('put|#a.textContent', ['it/up'])).toBeUndefined();
49
46
  });
50
47
 
51
48
  it('names cells the generator makes, and the baseline lists them as accepted', () => {
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * The value matrix runs every (cell, lane) pair in one process, so no run may
3
3
  * see another's writes. Two ways one did:
4
- * - hyperfixi keeps global variables in a Map that every context shares, so
5
- * one lane's `increment n` made every later run read `n` as 7, core's own
6
- * English included;
4
+ * - a run writes a window global (`increment n`); `@hyperfixi/core` also kept
5
+ * global variables in a Map every context shared, so one lane's write made
6
+ * every later run read `n` as 7 (that lane left with core's engine in C4,
7
+ * and the reset still restores every global);
7
8
  * - a lane can remove the body (tr read `increment i by 2 * 2` as
8
9
  * `increment *`), and the next run's reset then threw.
9
10
  *
@@ -41,9 +42,9 @@ describe('value matrix: runs are isolated', () => {
41
42
  cell('writes', 'on click increment n then put n into #out')
42
43
  );
43
44
  const after = await engines.runCell(read);
44
- expect(writes.lanes.en).toBe('7');
45
+ expect(writes.lanes.eng).toBe('7');
45
46
  expect(before.want).toBe('6');
46
- expect(before.lanes.en).toBe('6');
47
+ expect(before.lanes.eng).toBe('6');
47
48
  expect(after.lanes).toEqual(before.lanes);
48
49
  }, 120_000);
49
50
 
@@ -51,7 +52,7 @@ describe('value matrix: runs are isolated', () => {
51
52
  const before = await engines.runCell(read);
52
53
  await engines.runCell(cell('removes', 'on click remove document.body'));
53
54
  const after = await engines.runCell(read);
54
- expect(before.lanes.en).toBe('6');
55
+ expect(before.lanes.eng).toBe('6');
55
56
  expect(after.lanes).toEqual(before.lanes);
56
57
  }, 120_000);
57
58
  });