@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.
Files changed (28) hide show
  1. package/CHANGELOG.md +199 -1
  2. package/package.json +9 -10
  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/orchestrator.ts +2 -1
  16. package/src/multilingual/render-fidelity.ts +1 -1
  17. package/src/multilingual/shipped-examples-execution.test.ts +32 -96
  18. package/src/multilingual/shipped-examples-execution.ts +31 -121
  19. package/src/multilingual/shipped-sources-engine.test.ts +19 -19
  20. package/src/multilingual/shipped-sources-localized.test.ts +87 -0
  21. package/src/multilingual/shipped-sources-validity.ts +87 -99
  22. package/src/multilingual/validators/execution-validator.test.ts +81 -23
  23. package/src/multilingual/validators/execution-validator.ts +135 -91
  24. package/src/multilingual/validators/parse-validator.ts +7 -20
  25. package/src/multilingual/value-matrix.accepted.test.ts +8 -14
  26. package/src/multilingual/value-matrix.isolation.test.ts +7 -6
  27. package/src/multilingual/value-matrix.ts +32 -84
  28. package/src/multilingual/shipped-sources-validity.test.ts +0 -95
@@ -1,30 +1,24 @@
1
1
  /**
2
- * Shipped-sources validity gate
3
- * -----------------------------
4
- * Every hyperscript source we ship in `examples/` and the doc trees, parsed
5
- * with hyperfixi, must not land in the *recovers-with-errors* state:
6
- * `ok === true` with a non-empty `errors`.
2
+ * Shipped sources: the hyperscript this repository ships to users
3
+ * ----------------------------------------------------------------
4
+ * Collects every hyperscript source in `examples/` and the doc trees, for the
5
+ * gates that read them:
7
6
  *
8
- * That state is the blind spot this gate exists for. The parser is
9
- * deliberately resilient — it recovers from some malformed input and returns a
10
- * usable-but-degraded AST plus diagnostics — so genuinely broken sources
11
- * neither throw nor report `ok: false`. Nothing in the existing suites looks at
12
- * the combination, which is how five malformed examples shipped undetected
13
- * (an `if` with no `end`, a `put` with no target, `{id}` where `${id}` was
14
- * meant, and a `*--css-var` neither engine supports). The same sweep is what
15
- * caught the `send`-vs-`trigger` divergence and the `parseTriggerCommand` hang
16
- * in #780, neither of which was reachable from the existing tests.
7
+ * - `shipped-sources-engine.test.ts`: every English source must parse on
8
+ * `@hyperfixi/engine`, or be listed with a reason (`checkShippedSourcesOnEngine`);
9
+ * - `shipped-sources-localized.test.ts`: every source written in another
10
+ * language must translate to English both engines parse (`collectLocalizedSources`);
11
+ * - the R4-style second opinion, upstream `hyperscript.org`, is
12
+ * `loadCanonicalParser()` in canonical-validity.ts.
17
13
  *
18
- * Deliberately NOT gated: `ok === false`. That is a much larger and mostly
19
- * legitimate class — non-English sources (which need the multilingual path,
20
- * not `compileSync`), plugin syntax whose feature is not installed, and
21
- * intentionally-broken "this does not work" doc snippets. Gating it would
22
- * drown the signal. A source that fails outright is also loud; one that
23
- * silently recovers is not, and that asymmetry is the whole point.
14
+ * (The file is named for the gate it began with: core's "recovers with
15
+ * errors" ratchet, which flagged sources `@hyperfixi/core`'s resilient parser
16
+ * accepted with diagnostics — how five malformed examples were found, and the
17
+ * `send`-vs-`trigger` divergence and `parseTriggerCommand` hang of #780. It left
18
+ * with core's parser in Phase C4; the engine has no recovering mode, so a
19
+ * malformed source is simply rejected, and the engine gate sees it.)
24
20
  *
25
- * Node-only: the second-opinion path needs `loadCanonicalParser()`, which
26
- * imports the real `hyperscript.org` build off disk. Cannot run in a browser
27
- * suite.
21
+ * Node-only: walks the repo and reads git's index.
28
22
  */
29
23
 
30
24
  import fs from 'node:fs';
@@ -32,6 +26,7 @@ import path from 'node:path';
32
26
  import { execFileSync } from 'node:child_process';
33
27
  import { createHash } from 'node:crypto';
34
28
  import { fileURLToPath } from 'node:url';
29
+ import { JSDOM } from 'jsdom';
35
30
  import { extractHyperscriptFromMarkup } from '@hyperfixi/patterns-reference';
36
31
 
37
32
  /** Repo root, from `packages/testing-framework/src/multilingual/`. */
@@ -60,30 +55,6 @@ export interface ShippedSource {
60
55
  source: string;
61
56
  }
62
57
 
63
- export interface ShippedSourceFinding extends ShippedSource {
64
- /** Stable allowlist key: file plus a hash of the source. */
65
- key: string;
66
- /** First hyperfixi diagnostic. */
67
- error: string;
68
- /** Single-line excerpt, for reading the baseline without opening the file. */
69
- excerpt: string;
70
- }
71
-
72
- export interface ShippedSourcesResult {
73
- /** Sources extracted and compiled. */
74
- checked: number;
75
- /** Sources that compiled with no diagnostics. */
76
- clean: number;
77
- /** Sources in the recovers-with-errors state. */
78
- findings: ShippedSourceFinding[];
79
- }
80
-
81
- /** Minimal shape of `hyperscript.compileSync`, injected so this stays testable. */
82
- export type CompileForValidity = (code: string) => {
83
- ok: boolean;
84
- errors?: Array<{ message: string }>;
85
- };
86
-
87
58
  /**
88
59
  * A stable key for an individual snippet.
89
60
  *
@@ -99,7 +70,7 @@ function keyFor(file: string, source: string): string {
99
70
  /**
100
71
  * The files git actually tracks under `repoRoot`, as absolute paths.
101
72
  *
102
- * This gate walks the working TREE, and a working tree is not a clean checkout:
73
+ * These gates walk the working TREE, and a working tree is not a clean checkout:
103
74
  * `examples/vite-plugin-multilingual/` is GITIGNORED, so a local run saw a
104
75
  * source CI could never see. That is unfixable from the allowlist — with an
105
76
  * entry for it the gate failed in CI as a STALE entry, and without one it
@@ -196,45 +167,58 @@ export function collectShippedSources(
196
167
  return out;
197
168
  }
198
169
 
170
+ /** A shipped source written in another language: an `_` under a non-English `lang`. */
171
+ export interface LocalizedSource extends ShippedSource {
172
+ /** The base language of the closest `lang` (`es` for `es-MX`). */
173
+ lang: string;
174
+ }
175
+
199
176
  /**
200
- * Compile every shipped source and report the ones that recovered with errors.
177
+ * The shipped `_` attributes written in a language other than English. The
178
+ * other collectors hand every source to an English parser, which rejects
179
+ * these, so both shipped-sources gates skip them; on a page they run through
180
+ * the multilingual adapter, which translates a script under its element's
181
+ * `lang`. Pages are parsed as documents here, so a `lang` on `<html>` counts.
201
182
  */
202
- export function checkShippedSourcesValidity(
203
- compile: CompileForValidity,
204
- doc: Parameters<typeof extractHyperscriptFromMarkup>[0],
205
- opts?: { roots?: string[]; repoRoot?: string }
206
- ): ShippedSourcesResult {
207
- const sources = collectShippedSources(doc, opts);
208
- const findings: ShippedSourceFinding[] = [];
209
- let clean = 0;
210
-
211
- for (const s of sources) {
212
- let result: ReturnType<CompileForValidity>;
213
- try {
214
- result = compile(s.source);
215
- } catch {
216
- // A throw is the loud failure mode, not this gate's business.
217
- continue;
218
- }
219
- const errors = result.errors ?? [];
220
- if (result.ok && errors.length > 0) {
221
- findings.push({
222
- ...s,
223
- key: keyFor(s.file, s.source),
224
- error: errors[0]?.message ?? 'recovered with errors',
225
- excerpt: s.source.replace(/\s+/g, ' ').trim().slice(0, 100),
226
- });
227
- } else if (result.ok) {
228
- clean++;
183
+ export function collectLocalizedSources(opts?: {
184
+ roots?: string[];
185
+ repoRoot?: string;
186
+ }): LocalizedSource[] {
187
+ const repoRoot = opts?.repoRoot ?? REPO_ROOT;
188
+ const roots = opts?.roots ?? DEFAULT_ROOTS;
189
+ const out: LocalizedSource[] = [];
190
+ const tracked = trackedFiles(repoRoot);
191
+ for (const root of roots) {
192
+ for (const full of walk(path.join(repoRoot, root), [], tracked)) {
193
+ const rel = path.relative(repoRoot, full);
194
+ if (EXCLUDED.some(e => e.match(rel))) continue;
195
+ const text = fs.readFileSync(full, 'utf8');
196
+ const kind: ShippedSource['kind'] = full.endsWith('.html')
197
+ ? 'html-attribute'
198
+ : 'markdown-html-block';
199
+ const parts =
200
+ kind === 'html-attribute'
201
+ ? [text]
202
+ : [...text.matchAll(/```html\n([\s\S]*?)```/g)].map(m => m[1] ?? '');
203
+ for (const html of parts) {
204
+ const { window } = new JSDOM(html);
205
+ const elements: Element[] = Array.from(window.document.querySelectorAll('[_]'));
206
+ for (const el of elements) {
207
+ const source = el.getAttribute('_') ?? '';
208
+ const lang = (el.closest('[lang]')?.getAttribute('lang') ?? '').split('-')[0];
209
+ if (!source.trim() || !lang || lang.toLowerCase() === 'en') continue;
210
+ out.push({ file: rel, kind, source, lang: lang.toLowerCase() });
211
+ }
212
+ window.close();
213
+ }
229
214
  }
230
215
  }
231
-
232
- return { checked: sources.length, clean, findings };
216
+ return out;
233
217
  }
234
218
 
235
- /** A shipped source `packages/core` compiles clean and `@hyperfixi/engine` rejects. */
219
+ /** A shipped English source `@hyperfixi/engine` rejects. */
236
220
  export interface EngineRejection extends ShippedSource {
237
- /** Stable key: file plus a hash of the source, as the validity gate's. */
221
+ /** Stable key: file plus a hash of the source. */
238
222
  key: string;
239
223
  /** The engine's first parse error, first line. */
240
224
  error: string;
@@ -242,46 +226,50 @@ export interface EngineRejection extends ShippedSource {
242
226
  }
243
227
 
244
228
  export interface ShippedSourcesOnEngineResult {
245
- /** Distinct (file, source) pairs core compiles clean. */
246
- coreClean: number;
229
+ /** Distinct (file, source) pairs written in English: the denominator. */
230
+ checked: number;
247
231
  /** Of those, the ones the engine parses. */
248
232
  engineAccepts: number;
233
+ /** Distinct sources skipped because they are written in another language. */
234
+ localized: number;
249
235
  rejections: EngineRejection[];
250
236
  }
251
237
 
252
238
  /**
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).
239
+ * Every shipped source written in English, put to the engine's parser.
240
+ * `engineErrors` returns the engine's parse errors ([] = it parses).
241
+ *
242
+ * A source under a non-English `lang` is not English hyperscript: on a page it
243
+ * runs through the multilingual adapter, and `shipped-sources-localized.test.ts`
244
+ * checks it that way. Every other source must parse, or be listed.
256
245
  *
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.
246
+ * (Until Phase C4 the denominator was the sources `@hyperfixi/core` compiled
247
+ * clean, which also excluded non-English snippets that carried no `lang` and
248
+ * malformed doc examples. Moving off core surfaced fourteen: eleven examples in
249
+ * another language without a `lang`, now marked, and three malformed ones, now
250
+ * fixed.)
260
251
  */
261
252
  export function checkShippedSourcesOnEngine(
262
- compile: CompileForValidity,
263
253
  engineErrors: (code: string) => string[],
264
254
  doc: Parameters<typeof extractHyperscriptFromMarkup>[0],
265
255
  opts?: { roots?: string[]; repoRoot?: string }
266
256
  ): ShippedSourcesOnEngineResult {
257
+ const foreign = new Set(collectLocalizedSources(opts).map(s => keyFor(s.file, s.source)));
267
258
  const seen = new Set<string>();
268
259
  const rejections: EngineRejection[] = [];
269
- let coreClean = 0;
260
+ let checked = 0;
270
261
  let engineAccepts = 0;
262
+ let localized = 0;
271
263
 
272
264
  for (const s of collectShippedSources(doc, opts)) {
273
265
  const key = keyFor(s.file, s.source);
274
266
  if (seen.has(key)) continue;
275
267
  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 */
268
+ if (foreign.has(key)) {
269
+ localized++;
270
+ continue;
282
271
  }
283
- if (!clean) continue;
284
- coreClean++;
272
+ checked++;
285
273
  const errors = engineErrors(s.source);
286
274
  if (errors.length === 0) {
287
275
  engineAccepts++;
@@ -294,5 +282,5 @@ export function checkShippedSourcesOnEngine(
294
282
  excerpt: s.source.replace(/\s+/g, ' ').trim().slice(0, 100),
295
283
  });
296
284
  }
297
- return { coreClean, engineAccepts, rejections };
285
+ return { checked, engineAccepts, localized, rejections };
298
286
  }
@@ -1,14 +1,19 @@
1
1
  /**
2
2
  * R2 — execution smoke validator locks (session 5).
3
3
  *
4
+ * @vitest-environment node
5
+ * The node environment is required: under the suite default (happy-dom) the
6
+ * DOM constructors are happy-dom's, the per-execution jsdom's elements fail
7
+ * the hosts' `instanceof` checks, and every handler installs nothing.
8
+ *
4
9
  * Locks three things:
5
10
  * 1. The curated subset's MEMBERSHIP — expanding/shrinking it recalibrates
6
11
  * every language's avgExecutionFidelity, so it must be a deliberate,
7
12
  * baseline-regenerating change, never an accident.
8
- * 2. The en references EXECUTE: parse → buildAST → runtime.execute installs
9
- * the click handler in jsdom and dispatch produces a non-empty,
10
- * deterministic DOM-effect signature (the foundation every language is
11
- * scored against).
13
+ * 2. The en references EXECUTE: upstream `_hyperscript` (the oracle) and
14
+ * `@hyperfixi/engine` each install the handler in jsdom, dispatch produces
15
+ * a non-empty, deterministic DOM-effect signature, and the two agree (the
16
+ * foundation every language is scored against).
12
17
  * 3. Known cross-language behavior: a faithful translation reproduces the en
13
18
  * signature exactly; a translation that drops the destination role lands
14
19
  * its effect on `me` and MUST diverge (the failure class R2 exists to
@@ -18,6 +23,13 @@
18
23
  import { describe, it, expect, beforeAll } from 'vitest';
19
24
  import { ExecutionValidator, EXECUTION_SUBSET, loadExecutionSubset } from './execution-validator';
20
25
 
26
+ /**
27
+ * Sources loaded off the db for the curated subset: en only, which
28
+ * `db:init:force` writes in CI before this suite runs (translations need a
29
+ * `populate`, which this suite does not have).
30
+ */
31
+ let enSources: Map<string, string>;
32
+
21
33
  describe('R2 execution subset (lock)', () => {
22
34
  it('contains exactly the 49 curated patterns', () => {
23
35
  // Changing this list recalibrates avgExecutionFidelity for every language.
@@ -142,6 +154,43 @@ describe('R2 execution validator (lock)', () => {
142
154
 
143
155
  beforeAll(async () => {
144
156
  await validator.initialize();
157
+ enSources = (await loadExecutionSubset(['en'])).get('en')!;
158
+ });
159
+
160
+ it('every en reference runs on the engine exactly as on upstream, with an effect', async () => {
161
+ // The oracle check behind the reference: the orchestrator scores each
162
+ // translation (on the engine) against upstream's English. An en reference
163
+ // the engine runs differently would fail that pattern in every language
164
+ // at once — this names the engine instead. An empty or erroring reference
165
+ // is excluded from scoring, so it would silently shrink the subset.
166
+ const report: string[] = [];
167
+ for (const id of EXECUTION_SUBSET) {
168
+ const code = enSources.get(id);
169
+ if (!code) continue; // the loadExecutionSubset test below names it
170
+ const up = await validator.execute(id, code, 'en', 'upstream');
171
+ const eng = await validator.execute(id, code, 'en');
172
+ if (up.effects.length === 0)
173
+ report.push(`${id}: no effect on upstream (${up.error ?? 'no error'})`);
174
+ else if (up.error) report.push(`${id}: upstream reports ${up.error}`);
175
+ else if (JSON.stringify(eng.effects) !== JSON.stringify(up.effects)) {
176
+ report.push(
177
+ `${id}:\n upstream ${up.effects.join(' | ')}\n engine ${eng.effects.join(' | ')}`
178
+ );
179
+ }
180
+ }
181
+ expect(report, report.join('\n')).toEqual([]);
182
+ });
183
+
184
+ it('upstream swaps the two elements: the signature records the order', async () => {
185
+ // `swap #a with #b` changes no class, attribute, style or text — only the
186
+ // two elements' places, which the order line carries.
187
+ const res = await validator.execute(
188
+ 'swap-content',
189
+ 'on click swap #a with #b',
190
+ 'en',
191
+ 'upstream'
192
+ );
193
+ expect(res.effects.at(-1)).toMatch(/^order\[.*,b,a\]$/);
145
194
  });
146
195
 
147
196
  it('en reference executes: on click add .highlight to me', async () => {
@@ -206,13 +255,13 @@ describe('R2 execution validator (lock)', () => {
206
255
 
207
256
  it('the announce-screen-reader cell executes via its custom-event trigger', async () => {
208
257
  // First non-click cell: `on success put event.detail.message into
209
- // #sr-announce set @role to "alert" on #sr-announce`. PATTERN_TRIGGER
258
+ // #sr-announce set @role of #sr-announce to "alert"`. PATTERN_TRIGGER
210
259
  // dispatches a `success` CustomEvent carrying detail.message; the handler
211
260
  // writes that text into #sr-announce and sets role=alert on it. Locks both
212
261
  // the per-cell trigger wiring and the two-line effect signature.
213
262
  const res = await validator.execute(
214
263
  'announce-screen-reader',
215
- 'on success put event.detail.message into #sr-announce set @role to "alert" on #sr-announce',
264
+ 'on success put event.detail.message into #sr-announce set @role of #sr-announce to "alert"',
216
265
  'en'
217
266
  );
218
267
  expect(res.error).toBeUndefined();
@@ -261,11 +310,13 @@ describe('R2 execution validator (lock)', () => {
261
310
  it('wave-6 en references execute with their locked signatures', async () => {
262
311
  // The six wave-6 additions. Each en reference must produce a non-empty,
263
312
  // deterministic signature against the existing fixture (the foundation every
264
- // language is scored against). next/closest positionals fall back to `me`
265
- // when no match exists in the fixture; set *opacity/*transform write inline
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).
313
+ // language is scored against). A next/closest positional with no match
314
+ // throws on upstream (core fell back to `me`), so PATTERN_SETUP gives
315
+ // toggle-aria-expanded a following .panel and accordion-toggle an
316
+ // enclosing item; `toggle @attr` writes upstream's `undefined`; set
317
+ // *opacity/*transform write inline style; caret-var-on-target writes
318
+ // `null` into #btn (the undefined `^count`), as upstream's put writes a
319
+ // null value.
269
320
  const cases: ReadonlyArray<[string, string, string[]]> = [
270
321
  [
271
322
  'next-element',
@@ -275,7 +326,10 @@ describe('R2 execution validator (lock)', () => {
275
326
  [
276
327
  'toggle-aria-expanded',
277
328
  'on click toggle @aria-expanded on me toggle .open on next .panel',
278
- ['Δ#btn cls[open] attr[aria-expanded=,id=btn] style[] text[Click]'],
329
+ [
330
+ 'Δ#btn cls[] attr[aria-expanded=undefined,id=btn] style[] text[Click]',
331
+ 'Δ#panel cls[open panel] attr[id=panel] style[] text[]',
332
+ ],
279
333
  ],
280
334
  [
281
335
  'set-opacity',
@@ -290,7 +344,10 @@ describe('R2 execution validator (lock)', () => {
290
344
  [
291
345
  'accordion-toggle',
292
346
  'on click toggle .open on closest .accordion-item toggle @aria-expanded',
293
- ['Δ#btn cls[open] attr[aria-expanded=,id=btn] style[] text[Click]'],
347
+ [
348
+ 'Δ#btn cls[] attr[aria-expanded=undefined,id=btn] style[] text[Click]',
349
+ 'Δdiv[0] cls[accordion-item card open] attr[] style[] text[]',
350
+ ],
294
351
  ],
295
352
  [
296
353
  'caret-var-on-target',
@@ -368,11 +425,10 @@ describe('R2 execution validator (lock)', () => {
368
425
  it('wave-9 en references execute with their locked signatures', async () => {
369
426
  // The three wave-9 additions. Each en reference must produce a non-empty,
370
427
  // deterministic signature against the existing fixture. The `*opacity`
371
- // hide/show STRATEGIES are synchronous (no timer): hide writes display:none
372
- // on #btn (its data-original-display memo is engine bookkeeping, excluded
373
- // from signatures since the shared effect-signature module — see
374
- // ENGINE_ATTRS there); show adds the visibility class on #modal.
375
- // chained-access-possessive-dot writes the parent (.card) display.
428
+ // hide/show STRATEGIES are synchronous (no timer) and, on upstream, write
429
+ // the opacity itself: 0 on #btn, 1 on #modal (core ignored the strategy
430
+ // and toggled display, OPEN_ITEMS C1). chained-access-possessive-dot
431
+ // writes the parent (.card) display.
376
432
  const cases: ReadonlyArray<[string, string, string[]]> = [
377
433
  [
378
434
  'chained-access-possessive-dot',
@@ -382,12 +438,12 @@ describe('R2 execution validator (lock)', () => {
382
438
  [
383
439
  'hide-with-transition',
384
440
  'on click hide me with *opacity',
385
- ['Δ#btn cls[] attr[id=btn] style[display: none;] text[Click]'],
441
+ ['Δ#btn cls[] attr[id=btn] style[opacity: 0;] text[Click]'],
386
442
  ],
387
443
  [
388
444
  'show-with-transition',
389
445
  'on click show #modal with *opacity',
390
- ['Δ#modal cls[show] attr[id=modal] style[] text[]'],
446
+ ['Δ#modal cls[] attr[id=modal] style[opacity: 1;] text[]'],
391
447
  ],
392
448
  ];
393
449
  for (const [id, code, expected] of cases) {
@@ -403,7 +459,9 @@ describe('R2 execution validator (lock)', () => {
403
459
  // pre-existing document-order indices are preserved). The ja translations
404
460
  // lock the exact failure class each fix closed: append's fronted content
405
461
  // literal (was a bogus `event` role → runtime "append requires content")
406
- // and increment's trailing bare amount (was dropped → +1 instead of +10).
462
+ // and increment's bare amount (was dropped → +1 instead of +10). The
463
+ // increment row counts in `#score's textContent` since the examples moved
464
+ // to upstream's spelling (2026-10-02); its ja is the current render.
407
465
  const cases: ReadonlyArray<[string, string, string, string]> = [
408
466
  [
409
467
  'append-content',
@@ -413,8 +471,8 @@ describe('R2 execution validator (lock)', () => {
413
471
  ],
414
472
  [
415
473
  'increment-by-amount',
416
- 'on click increment #score by 10',
417
- '#score を クリック で 増加 10',
474
+ "on click increment #score's textContent by 10",
475
+ 'クリック を で #scoreのtextContent を 10 増加',
418
476
  'text[10]',
419
477
  ],
420
478
  ];