From 7fb6c17dc63f00de13f8985ca198ff418468de2d Mon Sep 17 00:00:00 2001 From: bymyself Date: Wed, 20 May 2026 15:09:16 -0700 Subject: [PATCH] =?UTF-8?q?test(tf):=20convert=20BC=20tests=20to=20axiomEx?= =?UTF-8?q?cluded=20=E2=80=94=20D-ban-runtime-addwidget=20(wave-10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per AXIOMS.md §Axiom-Excluded Test Annotation Policy (new wave-10) and AGENTS.md Rule 11, BC tests asserting on the removed v2 NodeHandle.addWidget / addDOMWidget surface are wrapped with the new `axiomExcluded({...})` helper rather than deleted. The annotated tests continue to run via vitest test.fails — if the v2 surface is ever re-introduced (intentionally or by regression) the expected-failure flips to a real failure and surfaces the policy violation. ## New shared helper - `src/extension-api-v2/__tests__/helpers/axiomExcluded.ts` — thin wrapper around vitest test.fails that attaches structured metadata to `task.meta.annotations`: axiom id, ADR path, rationale, migration paths, optional restoration cross-ref. Reporters and tooling can pivot on the annotation to surface a per-axiom dashboard. ## BC tests converted - `bc-05.v2.test.ts` (8 tests, all excluded — entire file is addDOMWidget-related). Removed `getDOMWidgetElement` import (deleted in foundation commit `d5d5692928`) and added a local stub returning undefined so the remaining test references compile. - `bc-05.migration.test.ts` (6 tests, all excluded — entire file is v1↔v2 addDOMWidget parity). - `bc-11.v2.test.ts` (3 of 11 tests excluded — only the `NodeHandle.addWidget — managed widget list mutation (S2.N16)` describe block; setValue/setHidden/setDisabled/setOption tests stay unchanged because those v2 surfaces remain valid). - `bc-11.migration.test.ts` (4 of 10 tests excluded — only the `node.widgets.push/splice → NodeHandle.addWidget (S2.N16)` describe block; value/option parity tests stay unchanged). ## Harness - `src/extension-api-v2/__tests__/harness/v2Runtime.ts` — comment out the `addWidget` and `addDOMWidget` mock implementations on the NodeHandle stub. With the public surface gone, leaving these mocks would mask the absence in harness-backed tests. Comment block links to D-ban-runtime-addwidget. `widgetCounter` removed (unused). ## Compat-floor doctrine retired - Removed the `// compat-floor: blast_radius 5.45 ≥ 2.0 — MUST pass before v2 ships` header annotation from all 4 converted BC test files. Per the new AXIOMS.md section, axioms and ADRs are the source of truth for v2 surface composition — compat-floor based on consumer counts conflated migration evidence with surface obligation. ## Validation - tsc: no new errors introduced on the touched files (pre-existing TS2305/TS2353/TS2307 errors per AGENTS.md Rule 8 carve-out remain) - BC tests for v1 (bc-05.v1, bc-11.v1) need no changes — bc-05.v1 uses a local synthetic addDOMWidget helper (not the LGraphNode prototype); bc-11.v1 doesn't reference addWidget at all - ext-api/*-v2 stack CI carve-out per AGENTS.md Rule 8 — codecov/patch, e2e, perf, playwright failures don't block forward motion until Phase B rebase Refs: AXIOMS.md A15 + §Axiom-Excluded Test Annotation Policy; AGENTS.md Rule 11; foundation `d5d5692928`; pkg `7c4d713ce4`. --- .../__tests__/bc-05.migration.test.ts | 44 +++++++-- .../__tests__/bc-05.v2.test.ts | 56 ++++++++--- .../__tests__/bc-11.migration.test.ts | 33 +++++-- .../__tests__/bc-11.v2.test.ts | 34 ++++++- .../__tests__/harness/v2Runtime.ts | 51 ++-------- .../__tests__/helpers/axiomExcluded.ts | 94 +++++++++++++++++++ 6 files changed, 239 insertions(+), 73 deletions(-) create mode 100644 src/extension-api-v2/__tests__/helpers/axiomExcluded.ts diff --git a/src/extension-api-v2/__tests__/bc-05.migration.test.ts b/src/extension-api-v2/__tests__/bc-05.migration.test.ts index 75632949cc..7bf09912b4 100644 --- a/src/extension-api-v2/__tests__/bc-05.migration.test.ts +++ b/src/extension-api-v2/__tests__/bc-05.migration.test.ts @@ -1,11 +1,39 @@ // Category: BC.05 — Custom DOM widgets and node sizing // DB cross-ref: S4.W2, S2.N11 // Exemplar: https://github.com/Lightricks/ComfyUI-LTXVideo/blob/main/web/js/sparse_track_editor.js#L218 -// compat-floor: blast_radius 5.45 ≥ 2.0 — MUST pass before v2 ships -// Migration: v1 node.addDOMWidget + node.computeSize → v2 NodeHandle.addDOMWidget + WidgetHandle.setHeight +// +// AXIOM-EXCLUDED (wave-10, D-ban-runtime-addwidget, AXIOMS.md A15): +// This file asserts v1↔v2 parity for runtime DOM widget addition. +// v2 NodeHandle.addDOMWidget is removed per A15 — runtime widget addition +// is forbidden in the new API. All tests are wrapped with +// `axiomExcluded({...})` (vitest test.fails) and continue to run as +// regression alarms. +// +// Migration: v1 `node.addDOMWidget(...)` extensions migrate to one of — +// - Declare in Python INPUT_TYPES (preferred) +// - Boxed widget (BBOX-style) +// - Non-widget UI primitive via defineNode/defineExtension setup() +// +// The "compat-floor blast_radius ≥ 2.0 MUST pass before v2 ships" +// doctrine is retired (AXIOMS.md §Axiom-Excluded Test Annotation Policy). import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { axiomExcluded } from './helpers/axiomExcluded' + +const excluded = axiomExcluded({ + axiom: 'A15', + adr: 'decisions/D-ban-runtime-addwidget.md', + rationale: + 'v2 NodeHandle does not expose addDOMWidget; the v1↔v2 parity scenario this file tests is no longer valid.', + migration: [ + 'Declare in Python INPUT_TYPES', + 'Boxed widget (e.g. BBOX [x,y,w,h])', + 'Non-widget UI primitive via defineNode/defineExtension setup()' + ], + restoration: 'D-ban-runtime-addwidget §Restoration criteria' +}) + // ── Mock world (same pattern as bc-01.migration.test.ts) ────────────────────── // vi.hoisted factory runs before imports — keep handle creation inline. @@ -144,7 +172,7 @@ describe('BC.05 migration — custom DOM widgets and node sizing', () => { }) describe('widget registration parity (S4.W2)', () => { - it('v1 addDOMWidget and v2 addDOMWidget both register a widget with the given name', () => { + excluded('v1 addDOMWidget and v2 addDOMWidget both register a widget with the given name', () => { const el = makeDiv() // v1 pattern @@ -168,7 +196,7 @@ describe('BC.05 migration — custom DOM widgets and node sizing', () => { expect(registeredNames).toEqual(v1Names) }) - it('v1 opts.getHeight() value matches the v2 height option stored in the dispatch command', () => { + excluded('v1 opts.getHeight() value matches the v2 height option stored in the dispatch command', () => { const el = makeDiv(0) // offsetHeight irrelevant const reportedHeight = 200 @@ -201,7 +229,7 @@ describe('BC.05 migration — custom DOM widgets and node sizing', () => { expect(createCmd?.options.__domHeight).toBe(v1Height) }) - it('v2 registers the same number of DOM widgets as v1 for a multi-widget node', () => { + excluded('v2 registers the same number of DOM widgets as v1 for a multi-widget node', () => { // v1 pattern: two addDOMWidget calls const v1Node = createV1Node(3) v1Node.addDOMWidget('widgetA', 'custom', makeDiv(50)) @@ -229,7 +257,7 @@ describe('BC.05 migration — custom DOM widgets and node sizing', () => { }) describe('computeSize elimination (S2.N11)', () => { - it('v2 setHeight produces a SetWidgetOption command; v1 requires a computeSize override for the same effect', () => { + excluded('v2 setHeight produces a SetWidgetOption command; v1 requires a computeSize override for the same effect', () => { const el = makeDiv(100) const newHeight = 400 @@ -264,7 +292,7 @@ describe('BC.05 migration — custom DOM widgets and node sizing', () => { }) describe('cleanup parity', () => { - it('v1 requires manual removal in onRemoved; v2 auto-removes the element via scope disposal', () => { + excluded('v1 requires manual removal in onRemoved; v2 auto-removes the element via scope disposal', () => { const el = makeDiv() document.body.appendChild(el) @@ -297,7 +325,7 @@ describe('BC.05 migration — custom DOM widgets and node sizing', () => { expect(document.body.contains(el)).toBe(false) }) - it('v2 auto-cleanup only removes the element registered via addDOMWidget, not unrelated elements', () => { + excluded('v2 auto-cleanup only removes the element registered via addDOMWidget, not unrelated elements', () => { const registeredEl = makeDiv() const unrelatedEl = makeDiv() document.body.appendChild(registeredEl) diff --git a/src/extension-api-v2/__tests__/bc-05.v2.test.ts b/src/extension-api-v2/__tests__/bc-05.v2.test.ts index 25faaabd7e..0f87a4d30d 100644 --- a/src/extension-api-v2/__tests__/bc-05.v2.test.ts +++ b/src/extension-api-v2/__tests__/bc-05.v2.test.ts @@ -1,11 +1,38 @@ // Category: BC.05 — Custom DOM widgets and node sizing // DB cross-ref: S4.W2, S2.N11 // Exemplar: https://github.com/Lightricks/ComfyUI-LTXVideo/blob/main/web/js/sparse_track_editor.js#L218 -// compat-floor: blast_radius 5.45 ≥ 2.0 — MUST pass before v2 ships -// v2 replacement: NodeHandle.addDOMWidget(opts) — auto-hooks computeSize via WidgetHandle geometry +// +// AXIOM-EXCLUDED (wave-10, D-ban-runtime-addwidget, AXIOMS.md A15): +// v2 NodeHandle.addDOMWidget / addWidget surfaces removed. All tests in +// this file are wrapped with `axiomExcluded({...})` (vitest test.fails) +// and continue to run as regression alarms — if the v2 surface is +// ever re-introduced, these tests flip to FAIL. +// +// Migration paths for original consumers: +// - Declare in Python INPUT_TYPES +// - Boxed widget (e.g. BBOX [x,y,w,h]) +// - Non-widget UI primitive via defineNode/defineExtension setup() +// +// The "compat-floor blast_radius ≥ 2.0 MUST pass before v2 ships" +// doctrine is retired (AXIOMS.md §Axiom-Excluded Test Annotation Policy). import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { axiomExcluded } from './helpers/axiomExcluded' + +const excluded = axiomExcluded({ + axiom: 'A15', + adr: 'decisions/D-ban-runtime-addwidget.md', + rationale: + 'Widgets are schema-declared per A15; v2 NodeHandle does not expose addDOMWidget/addWidget.', + migration: [ + 'Declare in Python INPUT_TYPES', + 'Boxed widget (e.g. BBOX [x,y,w,h])', + 'Non-widget UI primitive via defineNode/defineExtension setup()' + ], + restoration: 'D-ban-runtime-addwidget §Restoration criteria' +}) + // ── Mock world (same pattern as bc-01.v2.test.ts) ──────────────────────────── // vi.hoisted factory runs before imports — keep handle creation inline. @@ -41,12 +68,19 @@ import { _clearExtensionsForTesting, _setDispatchImplForTesting, defineNode, - getDOMWidgetElement, mountExtensionsForNode, unmountExtensionsForNode } from '@/services/extension-api-service' import type { NodeEntityId, WidgetEntityId } from '@/world/entityIds' +// Stub for the removed `getDOMWidgetElement` export. The side table was +// deleted alongside the v2 addDOMWidget shim per D-ban-runtime-addwidget; +// tests that reference it remain (wrapped via axiomExcluded) so the +// resulting assertion failures continue to flag any re-introduction. +const getDOMWidgetElement = ( + _widgetId: WidgetEntityId +): HTMLElement | undefined => undefined + // ── Helpers ─────────────────────────────────────────────────────────────────── function makeNodeId(n: number): NodeEntityId { @@ -98,7 +132,7 @@ describe('BC.05 v2 contract — custom DOM widgets and node sizing', () => { }) describe('NodeHandle.addDOMWidget(opts) — widget registration (S4.W2)', () => { - it('addDOMWidget dispatches a CreateWidget command with type "DOM" and the given name', () => { + excluded('addDOMWidget dispatches a CreateWidget command with type "DOM" and the given name', () => { const el = makeDiv() defineNode({ @@ -120,7 +154,7 @@ describe('BC.05 v2 contract — custom DOM widgets and node sizing', () => { expect(createCmd?.widgetType).toBe('DOM') }) - it('addDOMWidget returns a WidgetHandle with the correct name', () => { + excluded('addDOMWidget returns a WidgetHandle with the correct name', () => { let handleName: string | undefined defineNode({ @@ -141,7 +175,7 @@ describe('BC.05 v2 contract — custom DOM widgets and node sizing', () => { expect(handleName).toBe('preview') }) - it('addDOMWidget stores the DOM element in a side table (not in command options, for serializability)', () => { + excluded('addDOMWidget stores the DOM element in a side table (not in command options, for serializability)', () => { const el = makeDiv() let widgetId: WidgetEntityId | undefined @@ -169,7 +203,7 @@ describe('BC.05 v2 contract — custom DOM widgets and node sizing', () => { expect(getDOMWidgetElement(widgetId!)).toBe(el) }) - it('addDOMWidget uses the provided height option rather than offsetHeight when specified', () => { + excluded('addDOMWidget uses the provided height option rather than offsetHeight when specified', () => { const el = makeDiv(120) // offsetHeight = 120 const customHeight = 250 @@ -195,7 +229,7 @@ describe('BC.05 v2 contract — custom DOM widgets and node sizing', () => { expect(createCmd?.options.__domHeight).toBe(customHeight) }) - it('addDOMWidget falls back to element.offsetHeight when no height option is given', () => { + excluded('addDOMWidget falls back to element.offsetHeight when no height option is given', () => { const el = makeDiv(88) defineNode({ @@ -216,7 +250,7 @@ describe('BC.05 v2 contract — custom DOM widgets and node sizing', () => { expect(createCmd?.options.__domHeight).toBe(88) }) - it('DOM element is removed from the document when the node scope is disposed', () => { + excluded('DOM element is removed from the document when the node scope is disposed', () => { const el = makeDiv() document.body.appendChild(el) expect(document.body.contains(el)).toBe(true) @@ -240,7 +274,7 @@ describe('BC.05 v2 contract — custom DOM widgets and node sizing', () => { }) describe('WidgetHandle geometry — setHeight (replaces S2.N11 computeSize override)', () => { - it('WidgetHandle.setHeight dispatches a SetWidgetOption command with key "__domHeight"', () => { + excluded('WidgetHandle.setHeight dispatches a SetWidgetOption command with key "__domHeight"', () => { defineNode({ name: 'bc05.v2.set-height', nodeCreated(handle) { @@ -266,7 +300,7 @@ describe('BC.05 v2 contract — custom DOM widgets and node sizing', () => { expect(setCmd).toBeDefined() }) - it('multiple addDOMWidget calls each produce independent CreateWidget commands', () => { + excluded('multiple addDOMWidget calls each produce independent CreateWidget commands', () => { defineNode({ name: 'bc05.v2.multi-widget', nodeCreated(handle) { diff --git a/src/extension-api-v2/__tests__/bc-11.migration.test.ts b/src/extension-api-v2/__tests__/bc-11.migration.test.ts index a8470494be..53ddd2b998 100644 --- a/src/extension-api-v2/__tests__/bc-11.migration.test.ts +++ b/src/extension-api-v2/__tests__/bc-11.migration.test.ts @@ -1,11 +1,32 @@ // Category: BC.11 — Widget imperative state writes // DB cross-ref: S4.W4, S4.W5, S2.N16 // Exemplar: https://github.com/r-vage/ComfyUI_Eclipse/blob/main/js/eclipse-set-get.js#L9 -// Migration: v1 direct property mutation (widget.value, widget.options.values, node.widgets.push/splice) -// → v2 WidgetHandle.setValue / setOption / NodeHandle.addWidget +// +// PARTIALLY AXIOM-EXCLUDED (wave-10, D-ban-runtime-addwidget, AXIOMS.md A15): +// - The widget.value→setValue and widget.options→setOption parity blocks +// remain unchanged (these v2 surfaces are valid). +// - The "node.widgets.push/splice → NodeHandle.addWidget" describe block +// is wrapped via `axiomExcluded({...})` (vitest test.fails) because the +// v2 surface no longer exposes addWidget. v1 callers migrate to one of — +// declare in INPUT_TYPES / boxed widget / non-widget UI primitive. import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { axiomExcluded } from './helpers/axiomExcluded' + +const excluded = axiomExcluded({ + axiom: 'A15', + adr: 'decisions/D-ban-runtime-addwidget.md', + rationale: + 'v2 NodeHandle does not expose addWidget; the v1↔v2 parity scenario this describes is no longer valid.', + migration: [ + 'Declare in Python INPUT_TYPES', + 'Boxed widget (e.g. BBOX [x,y,w,h])', + 'Non-widget UI primitive via defineNode/defineExtension setup()' + ], + restoration: 'D-ban-runtime-addwidget §Restoration criteria' +}) + // ── Mock world (same pattern as bc-01.migration.test.ts) ────────────────────── // vi.hoisted factory runs before imports — keep handle creation inline. @@ -250,7 +271,7 @@ describe('BC.11 migration — widget imperative state writes', () => { }) describe('node.widgets.push/splice → NodeHandle.addWidget (S2.N16)', () => { - it('v1 push and v2 addWidget both result in a new widget with the expected name', () => { + excluded('v1 push and v2 addWidget both result in a new widget with the expected name', () => { // v1: push into node.widgets const v1Node = createV1Node() const v1NewWidget = createV1Widget('dynamic_lora', '') @@ -274,7 +295,7 @@ describe('BC.11 migration — widget imperative state writes', () => { expect(v2Names).toContain('dynamic_lora') }) - it('v1 splice by index is position-dependent; v2 addWidget uses name-keyed identity (no drift)', () => { + excluded('v1 splice by index is position-dependent; v2 addWidget uses name-keyed identity (no drift)', () => { // v1: positional splice — inserting before 'cfg' bumps 'cfg' index const v1Node = createV1Node([ createV1Widget('steps', 20), @@ -308,7 +329,7 @@ describe('BC.11 migration — widget imperative state writes', () => { expect(names).toContain('new_widget') }) - it('v2 addWidget returns a WidgetHandle that can immediately call setValue — no index lookup needed', () => { + excluded('v2 addWidget returns a WidgetHandle that can immediately call setValue — no index lookup needed', () => { defineNode({ name: 'bc11.mig.immediate-set', nodeCreated(handle) { @@ -326,7 +347,7 @@ describe('BC.11 migration — widget imperative state writes', () => { expect(setCmd).toBeDefined() }) - it('v1 push requires manual index tracking; v2 addWidget returns handle directly — no index bookkeeping', () => { + excluded('v1 push requires manual index tracking; v2 addWidget returns handle directly — no index bookkeeping', () => { // v1: to get the widget back after push, you track the index const v1Node = createV1Node() v1Node.widgets.push(createV1Widget('added', '')) diff --git a/src/extension-api-v2/__tests__/bc-11.v2.test.ts b/src/extension-api-v2/__tests__/bc-11.v2.test.ts index dc147845be..81781122ff 100644 --- a/src/extension-api-v2/__tests__/bc-11.v2.test.ts +++ b/src/extension-api-v2/__tests__/bc-11.v2.test.ts @@ -1,12 +1,36 @@ // Category: BC.11 — Widget imperative state writes // DB cross-ref: S4.W4, S4.W5, S2.N16 // Exemplar: https://github.com/r-vage/ComfyUI_Eclipse/blob/main/js/eclipse-set-get.js#L9 -// blast_radius: 5.81 — compat-floor: blast_radius ≥ 2.0 — MUST pass before v2 ships -// v2 replacement: WidgetHandle.setValue(v), WidgetHandle.setOption(key,v), NodeHandle.addWidget(opts) +// +// PARTIALLY AXIOM-EXCLUDED (wave-10, D-ban-runtime-addwidget, AXIOMS.md A15): +// - WidgetHandle.setValue / setHidden / setDisabled / setOption tests +// remain unchanged — these surfaces are valid in v2. +// - The "NodeHandle.addWidget" describe block is wrapped via +// `axiomExcluded({...})` (vitest test.fails) because v2 NodeHandle +// no longer exposes `addWidget`. The tests continue to run as +// regression alarms. +// +// The "compat-floor blast_radius ≥ 2.0 MUST pass before v2 ships" +// doctrine is retired (AXIOMS.md §Axiom-Excluded Test Annotation Policy). import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { WidgetHandle } from '@/extension-api/widget' +import { axiomExcluded } from './helpers/axiomExcluded' + +const excluded = axiomExcluded({ + axiom: 'A15', + adr: 'decisions/D-ban-runtime-addwidget.md', + rationale: + 'v2 NodeHandle does not expose addWidget; runtime widget addition is forbidden per A15.', + migration: [ + 'Declare in Python INPUT_TYPES', + 'Boxed widget (e.g. BBOX [x,y,w,h])', + 'Non-widget UI primitive via defineNode/defineExtension setup()' + ], + restoration: 'D-ban-runtime-addwidget §Restoration criteria' +}) + // ── Mock world (same pattern as bc-01.v2.test.ts) ──────────────────────────── // vi.hoisted factory runs before imports — keep handle creation inline. @@ -251,7 +275,7 @@ describe('BC.11 v2 contract — widget imperative state writes', () => { }) describe('NodeHandle.addWidget — managed widget list mutation (S2.N16)', () => { - it('addWidget dispatches a CreateWidget command and returns a handle with the given name', () => { + excluded('addWidget dispatches a CreateWidget command and returns a handle with the given name', () => { let handleName: string | undefined defineNode({ @@ -273,7 +297,7 @@ describe('BC.11 v2 contract — widget imperative state writes', () => { expect(handleName).toBe('steps') }) - it('addWidget for each of two distinct widgets produces two independent CreateWidget commands', () => { + excluded('addWidget for each of two distinct widgets produces two independent CreateWidget commands', () => { defineNode({ name: 'bc11.v2.add-two-widgets', nodeCreated(handle) { @@ -295,7 +319,7 @@ describe('BC.11 v2 contract — widget imperative state writes', () => { expect(createCmds).toHaveLength(2) }) - it('addWidget carries the defaultValue in the CreateWidget command', () => { + excluded('addWidget carries the defaultValue in the CreateWidget command', () => { defineNode({ name: 'bc11.v2.add-widget-default', nodeCreated(handle) { diff --git a/src/extension-api-v2/__tests__/harness/v2Runtime.ts b/src/extension-api-v2/__tests__/harness/v2Runtime.ts index e7b53aae13..30c0f1c21d 100644 --- a/src/extension-api-v2/__tests__/harness/v2Runtime.ts +++ b/src/extension-api-v2/__tests__/harness/v2Runtime.ts @@ -59,7 +59,8 @@ export function createV2Runtime(options: V2RuntimeOptions = {}): V2Runtime { function createHandle(record: NodeRecord): NodeHandle { // Minimal NodeHandle stub — only the fields BC tests touch are real. const widgets: Array<{ name: string; id: string }> = [] - let widgetCounter = 0 + // widgetCounter removed alongside addWidget/addDOMWidget mocks + // per D-ban-runtime-addwidget (wave-10). return { id: record.id as unknown as string, @@ -83,48 +84,12 @@ export function createV2Runtime(options: V2RuntimeOptions = {}): V2Runtime { setProperty: () => {}, widget: (name: string) => widgets.find((w) => w.name === name), widgets: () => widgets, - addWidget: (_type: string, name: string, _defaultValue: unknown) => { - const id = `widget:${record.id}:${++widgetCounter}` - const wh = { - name, - id, - equals(other: { id: string }): boolean { - return this.id === other.id - }, - getValue: () => undefined, - setValue: () => {}, - setOption: () => {}, - setHidden: () => {}, - setDisabled: () => {}, - setHeight: () => {}, - on: () => () => {}, - isSerializeEnabled: () => true, - setSerializeEnabled: () => {} - } - widgets.push(wh) - return wh - }, - addDOMWidget: (opts: { name: string; element: HTMLElement }) => { - const id = `widget:${record.id}:dom:${++widgetCounter}` - const wh = { - name: opts.name, - id, - equals(other: { id: string }): boolean { - return this.id === other.id - }, - getValue: () => undefined, - setValue: () => {}, - setOption: () => {}, - setHidden: () => {}, - setDisabled: () => {}, - setHeight: () => {}, - on: () => () => {}, - isSerializeEnabled: () => true, - setSerializeEnabled: () => {} - } - widgets.push(wh) - return wh - }, + // REMOVED per AXIOMS.md A15 and + // decisions/D-ban-runtime-addwidget.md — v2 NodeHandle does not + // expose addWidget / addDOMWidget. These mocks would mask the + // absence of the public surface in harness-backed tests. + // addWidget: (_type, name, _defaultValue) => { … } + // addDOMWidget: (opts) => { … } inputs: () => [], outputs: () => [], on: () => () => {} diff --git a/src/extension-api-v2/__tests__/helpers/axiomExcluded.ts b/src/extension-api-v2/__tests__/helpers/axiomExcluded.ts new file mode 100644 index 0000000000..e59413d0ac --- /dev/null +++ b/src/extension-api-v2/__tests__/helpers/axiomExcluded.ts @@ -0,0 +1,94 @@ +/** + * Axiom-Excluded Test Annotation Helper + * + * Per the Axiom-Excluded Test Annotation Policy + * (`AXIOMS.md §Axiom-Excluded Test Annotation Policy`, AGENTS.md Rule 11, + * `decisions/D-ban-runtime-addwidget.md §Testing policy`). + * + * When a source-code surface is removed/deferred per an accepted axiom or + * ADR, BC tests that asserted on the absent surface MUST be converted to + * this helper rather than deleted outright. The annotated test continues + * to run; if the surface is ever re-introduced (intentionally or by + * regression) the expected-failure flips to a real failure and surfaces + * the policy violation. + * + * Mechanism: wraps vitest `test.fails(...)` and writes structured + * metadata to `task.meta.annotations` (vitest 1.x+). Reporters and + * tooling can pivot on the annotation to surface a per-axiom dashboard. + * + * @example + * ```ts + * import { axiomExcluded } from './helpers/axiomExcluded' + * + * axiomExcluded({ + * axiom: 'A15', + * adr: 'decisions/D-ban-runtime-addwidget.md', + * rationale: 'Widgets are schema-declared per A15; v2 NodeHandle does not expose addWidget/addDOMWidget.', + * migration: [ + * 'Declare in INPUT_TYPES', + * 'Boxed widget (e.g. BBOX)', + * 'Non-widget UI primitive via bootstrap-hooks' + * ], + * restoration: 'D-ban-runtime-addwidget §Restoration criteria' + * })('addDOMWidget dispatches CreateWidget command', () => { + * // Original test body — asserts the absent surface. + * // Now expected to fail; test.fails converts the throw to a PASS. + * }) + * ``` + * + * @see AXIOMS.md §Axiom-Excluded Test Annotation Policy + * @see AGENTS.md Rule 11 + * @see decisions/D-ban-runtime-addwidget.md + */ + +import { test } from 'vitest' + +export interface AxiomExcludedAnnotation { + /** Short axiom id, e.g. `'A15'` or `'A14:REMOVED'`. */ + axiom: string + /** Workspace ADR path, e.g. `'decisions/D-ban-runtime-addwidget.md'`. */ + adr: string + /** One-sentence summary of why the surface is absent. */ + rationale: string + /** Migration paths the original consumer should take. */ + migration: string[] + /** Optional cross-ref to ADR restoration criteria. */ + restoration?: string +} + +/** + * Returns a vitest test factory that registers expected-failure tests + * with structured axiom-exclusion annotations. + * + * The returned factory has the same call signature as `test.fails`: + * `(name, fn)` or `(name, fn, timeout)`. + */ +export function axiomExcluded(annotation: AxiomExcludedAnnotation) { + return function axiomExcludedTest( + name: string, + fn: () => void | Promise, + timeout?: number + ): void { + // test.fails: passes if fn throws, fails if fn succeeds — gives us the + // "regression alarm" semantics: if the absent surface is restored, the + // body's assertions stop throwing and this test starts failing. + test.fails( + name, + async (context) => { + // Attach the annotation to task.meta so reporters and tooling can + // pivot on it (vitest exposes task.meta on the runner task). + const task = ( + context as unknown as { + task?: { meta?: Record } + } + ).task + if (task) { + task.meta = task.meta ?? {} + ;(task.meta as Record).axiomExcluded = annotation + } + await fn() + }, + timeout + ) + } +}