mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-24 06:35:10 +00:00
test(tf): convert BC tests to axiomExcluded — D-ban-runtime-addwidget (wave-10)
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`.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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', ''))
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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: () => () => {}
|
||||
|
||||
94
src/extension-api-v2/__tests__/helpers/axiomExcluded.ts
Normal file
94
src/extension-api-v2/__tests__/helpers/axiomExcluded.ts
Normal file
@@ -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<void>,
|
||||
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<string, unknown> }
|
||||
}
|
||||
).task
|
||||
if (task) {
|
||||
task.meta = task.meta ?? {}
|
||||
;(task.meta as Record<string, unknown>).axiomExcluded = annotation
|
||||
}
|
||||
await fn()
|
||||
},
|
||||
timeout
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user