From 7b9f506f4a7973a0070d89be15cbda2e99184da0 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Wed, 30 Sep 2026 04:53:28 +0400 Subject: [PATCH] test: migrate Scene Graph tests into the package and gate new tests/engine files (#790) Scene Graph unit tests move to packages/scene-graph/tests with a package-local assert helper and test type-checking; five Core- and fig-owned tests move to their owners' engine homes. check:test-homes rejects any new test under tests/engine against a reviewed baseline so the migration debt only shrinks. --- .github/workflows/ci.yml | 2 +- bun.lock | 8 + package.json | 4 +- packages/docs/development/testing.md | 2 + packages/scene-graph/package.json | 5 +- .../tests}/basic/count-descendants.test.ts | 2 +- .../scene-graph/tests}/basic/graph.test.ts | 29 +- .../scene-graph/tests}/basic/helpers.ts | 2 +- .../scene-graph/tests}/basic/hit-test.test.ts | 8 +- .../basic/layout-affecting-keys.test.ts | 2 +- .../tests}/basic/text-picture-keys.test.ts | 2 +- .../tests}/basic/update-node.test.ts | 7 +- .../tests}/basic/variables.test.ts | 2 +- .../tests}/clone/node-props.test.ts | 38 +- .../scene-graph/tests}/clone/tree.test.ts | 21 +- .../tests}/components/properties.test.ts | 0 .../scene-graph/tests}/copy.test.ts | 4 +- packages/scene-graph/tests/helpers/assert.ts | 12 + .../{src => tests}/instance-overrides.test.ts | 2 +- .../tests}/instance-sync-duplication.test.ts | 4 +- .../scene-graph/tests}/layout-guides.test.ts | 0 .../tests}/mutation-impact.test.ts | 0 .../tests}/reparent-transform.test.ts | 0 .../tests}/source-metadata.test.ts | 0 .../scene-graph/tests}/undo/batch.test.ts | 0 .../scene-graph/tests}/undo/mutation.test.ts | 7 +- .../binding/instance-override.test.ts | 2 +- .../scene-graph/tests}/vector-network.test.ts | 2 +- .../scene-graph/tests}/world-bounds.test.ts | 0 .../tests}/world-transform.test.ts | 0 packages/scene-graph/tsconfig.test.json | 8 + tests/AGENTS.md | 2 +- .../clipboard}/binding-corruption.test.ts | 0 .../api/stroke}/individual-strokes.test.ts | 0 .../api/variable-binding-extended.test.ts} | 0 .../fig/import}/instance-overrides.test.ts | 0 .../fig}/plugin-data.test.ts | 0 .../variable-binding-validation.test.ts} | 0 tools/AGENTS.md | 2 +- tools/unit-tests/engine-baseline.txt | 592 ++++++++++++++++++ tools/unit-tests/package.json | 3 + tools/unit-tests/src/check-homes.ts | 33 + tools/unit-tests/src/homes.ts | 85 +++ tools/unit-tests/tests/homes.test.ts | 88 +++ 44 files changed, 911 insertions(+), 69 deletions(-) rename {tests/engine/scene-graph => packages/scene-graph/tests}/basic/count-descendants.test.ts (98%) rename {tests/engine/scene-graph => packages/scene-graph/tests}/basic/graph.test.ts (93%) rename {tests/engine/scene-graph => packages/scene-graph/tests}/basic/helpers.ts (81%) rename {tests/engine/scene-graph => packages/scene-graph/tests}/basic/hit-test.test.ts (95%) rename {tests/engine/scene-graph => packages/scene-graph/tests}/basic/layout-affecting-keys.test.ts (97%) rename {tests/engine/scene-graph => packages/scene-graph/tests}/basic/text-picture-keys.test.ts (96%) rename {tests/engine/scene-graph => packages/scene-graph/tests}/basic/update-node.test.ts (97%) rename {tests/engine/scene-graph => packages/scene-graph/tests}/basic/variables.test.ts (99%) rename {tests/engine/scene-graph => packages/scene-graph/tests}/clone/node-props.test.ts (87%) rename {tests/engine/scene-graph => packages/scene-graph/tests}/clone/tree.test.ts (85%) rename {tests/engine/scene-graph => packages/scene-graph/tests}/components/properties.test.ts (100%) rename {tests/engine/scene-graph => packages/scene-graph/tests}/copy.test.ts (98%) create mode 100644 packages/scene-graph/tests/helpers/assert.ts rename packages/scene-graph/{src => tests}/instance-overrides.test.ts (98%) rename {tests/engine/scene-graph => packages/scene-graph/tests}/instance-sync-duplication.test.ts (99%) rename {tests/engine/scene-graph => packages/scene-graph/tests}/layout-guides.test.ts (100%) rename {tests/engine/scene-graph => packages/scene-graph/tests}/mutation-impact.test.ts (100%) rename {tests/engine/scene-graph => packages/scene-graph/tests}/reparent-transform.test.ts (100%) rename {tests/engine/scene-graph => packages/scene-graph/tests}/source-metadata.test.ts (100%) rename {tests/engine/scene-graph => packages/scene-graph/tests}/undo/batch.test.ts (100%) rename {tests/engine/scene-graph => packages/scene-graph/tests}/undo/mutation.test.ts (94%) rename {tests/engine/scene-graph => packages/scene-graph/tests}/variable/binding/instance-override.test.ts (99%) rename {tests/engine/scene-graph => packages/scene-graph/tests}/vector-network.test.ts (96%) rename {tests/engine/scene-graph => packages/scene-graph/tests}/world-bounds.test.ts (100%) rename {tests/engine/scene-graph => packages/scene-graph/tests}/world-transform.test.ts (100%) create mode 100644 packages/scene-graph/tsconfig.test.json rename tests/engine/{scene-graph/clone => editor/clipboard}/binding-corruption.test.ts (100%) rename tests/engine/{scene-graph => figma/api/stroke}/individual-strokes.test.ts (100%) rename tests/engine/{scene-graph/variable/binding/extended.test.ts => figma/api/variable-binding-extended.test.ts} (100%) rename tests/engine/{scene-graph => io/fig/import}/instance-overrides.test.ts (100%) rename tests/engine/{scene-graph => io/fig}/plugin-data.test.ts (100%) rename tests/engine/{scene-graph/variable/binding/validation.test.ts => lint/variable-binding-validation.test.ts} (100%) create mode 100644 tools/unit-tests/engine-baseline.txt create mode 100644 tools/unit-tests/src/check-homes.ts create mode 100644 tools/unit-tests/src/homes.ts create mode 100644 tools/unit-tests/tests/homes.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 651be77c2..aced2176e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -101,7 +101,7 @@ jobs: run: bun run typecheck - name: Enforce architecture and type-shape boundaries - run: bun run check:arch && bun run test:type-shapes + run: bun run check:arch && bun run check:test-homes && bun run test:type-shapes package-quality: name: Package integrity diff --git a/bun.lock b/bun.lock index a4b2ed59f..0961a5782 100644 --- a/bun.lock +++ b/bun.lock @@ -479,6 +479,12 @@ "@open-pencil/vue": "workspace:*", }, }, + "tools/unit-tests": { + "name": "@open-pencil/tool-unit-tests", + "dependencies": { + "@open-pencil/package-artifacts": "workspace:*", + }, + }, "tools/visual-oracles": { "name": "@open-pencil/visual-oracle-tools", "dependencies": { @@ -1370,6 +1376,8 @@ "@open-pencil/tauri-menu-tools": ["@open-pencil/tauri-menu-tools@workspace:tools/tauri-menu"], + "@open-pencil/tool-unit-tests": ["@open-pencil/tool-unit-tests@workspace:tools/unit-tests"], + "@open-pencil/visual-oracle-tools": ["@open-pencil/visual-oracle-tools@workspace:tools/visual-oracles"], "@open-pencil/vue": ["@open-pencil/vue@workspace:packages/vue"], diff --git a/package.json b/package.json index 5fb163c67..9f849181c 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "tools/authoring-reference", "tools/package-quality", "tools/release-packages", + "tools/unit-tests", "tools/tauri-menu", "tools/visual-oracles" ], @@ -42,7 +43,7 @@ "lint:structure": "oxlint -c oxlint.json commitlint.config.ts vite.config.ts vite/ .storybook/ src/ packages/scene-graph/src/ packages/scene-graph/scripts/ packages/core/src/ packages/vue/src/ packages/cli/src/ packages/mcp/src/ packages/harness/src/ packages/harness/tests/ packages/dom-css/src/ packages/dom-css/tests/ packages/dom-css/scripts/ packages/pen/src/ packages/pen/scripts/ packages/kiwi/src/ packages/kiwi/tests/ packages/kiwi/scripts/ packages/fig/src/ packages/fig/tests/ packages/fig/scripts/ tests/ scripts/ tools/", "format": "oxfmt --write .oxfmtrc.json commitlint.config.ts vite.config.ts vite/ .storybook/ src/ packages/scene-graph/src/ packages/scene-graph/scripts/ packages/core/src/ packages/cli/src/ packages/mcp/src/ packages/harness/src/ packages/harness/tests/ packages/vue/src/ packages/dom-css/src/ packages/dom-css/tests/ packages/dom-css/scripts/ packages/pen/src/ packages/pen/scripts/ packages/kiwi/src/ packages/kiwi/tests/ packages/kiwi/scripts/ packages/fig/src/ packages/fig/tests/ packages/fig/scripts/ tests scripts/ tools/", "format:check": "bun run format && status=$(git status --porcelain -uall) && test -z \"$status\" || (echo \"$status\" && exit 1)", - "check": "bun run check:icons && bun run build:packages && bun run lint && tsgo --noEmit && bun run check:vue && bun run check:native-test && bun run check:i18n && bun run check:docs && bun run check:changelog && bun run check:packages && bun run check:deps && bun run check:audit && bun run check:secrets && bun run check:monorepo && bun run check:arch && bun run test:type-shapes && bun run test:tools && bun run test:dupes", + "check": "bun run check:icons && bun run build:packages && bun run lint && tsgo --noEmit && bun run check:vue && bun run check:native-test && bun run check:i18n && bun run check:docs && bun run check:changelog && bun run check:packages && bun run check:deps && bun run check:audit && bun run check:secrets && bun run check:monorepo && bun run check:arch && bun run check:test-homes && bun run test:type-shapes && bun run test:tools && bun run test:dupes", "check:commits": "commitlint", "check:changelog": "bun tools/release-packages/src/check-changelog.ts", "check:deps": "knip --include unlisted,unresolved,binaries", @@ -56,6 +57,7 @@ "check:i18n": "bun tools/i18n/src/check-locales.ts", "check:packages": "bun tools/package-quality/src/cli.ts check", "check:arch": "steiger .", + "check:test-homes": "bun tools/unit-tests/src/check-homes.ts", "check:vue": "vue-tsc --noEmit -p tsconfig.json && vue-tsc --noEmit -p packages/vue/tsconfig.json", "test": "OPENPENCIL_TEST_SERVER=app playwright test --project=openpencil --grep-invert @real-llm", "test:storybook": "OPENPENCIL_TEST_SERVER=storybook playwright test --project=storybook-chromium", diff --git a/packages/docs/development/testing.md b/packages/docs/development/testing.md index 25bbd52b2..04bbb9279 100644 --- a/packages/docs/development/testing.md +++ b/packages/docs/development/testing.md @@ -20,6 +20,8 @@ Core tests using SceneGraph still belong to Core. Central integration is for con **Migration status:** much existing coverage remains under `tests/engine/**`, alongside package-local suites. The table is the agreed destination, not a claim that migration is complete. Until a domain migrates, extend its existing suite rather than create a second home. The repository-wide migration is separate from feature work. +`bun run check:test-homes` keeps the debt shrinking: every test under `tests/engine` must be listed in `tools/unit-tests/engine-baseline.txt`, so a new test fails the check until it lives in its owner's canonical home, and a moved file must be removed from the baseline. Scene Graph tests have migrated to `packages/scene-graph/tests`. + Discovery already covers the destinations: `tools/unit-tests/src/shards.ts` groups tests by owner and lists each owner's canonical home (`packages//tests`, `tests/app`, `tests/integration`) together with the `tests/engine` directories it still owns, so `bun run test:unit` and the CI shards run a file from either place. Moving a domain is therefore a `git mv` plus import fixes; only a new owner or a new top-level home needs a shard entry. Do not move files into a directory the shard map does not list. `test:unit:quick` runs files in Bun worker processes (`--parallel`), which is the fastest local loop. CI shards still run each group in one process; `test:unit:isolated` (`--isolate`, also run nightly) gives every file a fresh global object and is the check to run when a suite passes alone but fails in a shard. Shards share one Bun process, so module-level state (a `fake-indexeddb/auto` import, an IndexedDB connection left open by `createEditorStore()`, a patched global) leaks into later files in the same shard. A package-local run (`bun test tests` inside the package) is a fresh process and will not reproduce that leak. Close what a test opens and restore what it patches. diff --git a/packages/scene-graph/package.json b/packages/scene-graph/package.json index afe6f5710..2a9735766 100644 --- a/packages/scene-graph/package.json +++ b/packages/scene-graph/package.json @@ -192,10 +192,11 @@ }, "scripts": { "build": "bunx tsdown --config tsdown.config.ts", - "check": "bun run typecheck && bun run build && bun run smoke:dist", + "check": "bun run typecheck && bun run test && bun run build && bun run smoke:dist", "prepack": "bun run build", "smoke:dist": "bun scripts/smoke-dist.ts", - "typecheck": "bunx tsc --noEmit -p tsconfig.json" + "test": "bun test tests", + "typecheck": "bunx tsc --noEmit -p tsconfig.test.json" }, "dependencies": { "culori": "^4.0.2", diff --git a/tests/engine/scene-graph/basic/count-descendants.test.ts b/packages/scene-graph/tests/basic/count-descendants.test.ts similarity index 98% rename from tests/engine/scene-graph/basic/count-descendants.test.ts rename to packages/scene-graph/tests/basic/count-descendants.test.ts index d3e540e6a..5b5e53b9a 100644 --- a/tests/engine/scene-graph/basic/count-descendants.test.ts +++ b/packages/scene-graph/tests/basic/count-descendants.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'bun:test' -import { SceneGraph } from '@open-pencil/core' +import { SceneGraph } from '@open-pencil/scene-graph' import { pageId, rect } from './helpers' diff --git a/tests/engine/scene-graph/basic/graph.test.ts b/packages/scene-graph/tests/basic/graph.test.ts similarity index 93% rename from tests/engine/scene-graph/basic/graph.test.ts rename to packages/scene-graph/tests/basic/graph.test.ts index 80b994c1c..2b24ef4ec 100644 --- a/tests/engine/scene-graph/basic/graph.test.ts +++ b/packages/scene-graph/tests/basic/graph.test.ts @@ -1,16 +1,15 @@ import { describe, expect, test } from 'bun:test' -import { generateId, SceneGraph } from '@open-pencil/core' - -import { expectDefined } from '#tests/helpers/assert' +import { generateId, SceneGraph } from '@open-pencil/scene-graph' +import { expectDefined, getNodeOrThrow } from '../helpers/assert' import { pageId, rect } from './helpers' describe('SceneGraph', () => { test('create rectangle', () => { const graph = new SceneGraph() const id = rect(graph, 'Rect', 100, 100, 200, 150) - const node = graph.getNode(id) + const node = getNodeOrThrow(graph, id) expect(node).toBeDefined() expect(expectDefined(node, 'node').type).toBe('RECTANGLE') expect(node.x).toBe(100) @@ -124,7 +123,7 @@ describe('SceneGraph', () => { const page = pageId(graph) const comp = graph.createNode('COMPONENT', page, { name: 'Comp', width: 100, height: 40 }) graph.createNode('RECTANGLE', comp.id, { name: 'BG', width: 100, height: 40 }) - const inst = graph.createInstance(comp.id, page) + const inst = expectDefined(graph.createInstance(comp.id, page), 'instance') expect(inst).toBeDefined() expect(graph.getInstances(comp.id)).toHaveLength(1) @@ -159,12 +158,12 @@ describe('SceneGraph', () => { const graph = new SceneGraph() const page = pageId(graph) const r = rect(graph, 'R', 100, 200) - const node = graph.getNode(r) + const node = getNodeOrThrow(graph, r) expect(node).toBeDefined() expect(expectDefined(node, 'node').parentId).toBe(page) graph.reparentNode(r, page) // Position should not change - const after = graph.getNode(r) + const after = getNodeOrThrow(graph, r) expect(after).toBeDefined() expect(expectDefined(after, 'updated node').x).toBe(100) expect(expectDefined(after, 'updated node').y).toBe(200) @@ -189,7 +188,7 @@ describe('SceneGraph', () => { graph.reparentNode(r, frame) // Now node is inside frame at (200,100), so local coords should be (100, 50) - const after = graph.getNode(r) + const after = getNodeOrThrow(graph, r) expect(after).toBeDefined() expect(expectDefined(after, 'updated node').x).toBe(100) expect(after.y).toBe(50) @@ -258,7 +257,7 @@ describe('SceneGraph', () => { graph.reparentNode(inner, frame) // Inner was at (100,100), frame is at (50,50), so inner's local = (50,50) - const innerNode = graph.getNode(inner) + const innerNode = getNodeOrThrow(graph, inner) expect(innerNode).toBeDefined() expect(expectDefined(innerNode, 'inner node').x).toBe(50) expect(innerNode.y).toBe(50) @@ -339,7 +338,7 @@ describe('SceneGraph', () => { const graph = new SceneGraph() const id = rect(graph, 'R') graph.updateNode(id, { x: 200, name: 'Updated' }) - const node = graph.getNode(id) + const node = getNodeOrThrow(graph, id) expect(node).toBeDefined() expect(expectDefined(node, 'node').x).toBe(200) expect(node.name).toBe('Updated') @@ -353,7 +352,7 @@ describe('SceneGraph', () => { height: 40 }) const child = graph.createNode('RECTANGLE', comp.id, { name: 'BG', width: 100, height: 40 }) - const instance = graph.createInstance(comp.id, pageId(graph)) + const instance = expectDefined(graph.createInstance(comp.id, pageId(graph)), 'instance') expect(instance).toBeDefined() expect(expectDefined(instance, 'instance').type).toBe('INSTANCE') expect(instance.componentId).toBe(comp.id) @@ -371,7 +370,7 @@ describe('SceneGraph', () => { height: 100 }) const label = graph.createNode('TEXT', comp.id, { name: 'Title', text: 'Hello', fontSize: 14 }) - const instance = graph.createInstance(comp.id, pageId(graph)) + const instance = expectDefined(graph.createInstance(comp.id, pageId(graph)), 'instance') expect(instance).toBeDefined() const instLabel = graph.getChildren(expectDefined(instance, 'instance').id)[0] expect(instLabel.text).toBe('Hello') @@ -391,7 +390,7 @@ describe('SceneGraph', () => { height: 100 }) graph.createNode('TEXT', comp.id, { name: 'Title', text: 'Default', fontSize: 14 }) - const instance = graph.createInstance(comp.id, pageId(graph)) + const instance = expectDefined(graph.createInstance(comp.id, pageId(graph)), 'instance') expect(instance).toBeDefined() const instLabel = graph.getChildren(expectDefined(instance, 'instance').id)[0] @@ -421,7 +420,7 @@ describe('SceneGraph', () => { height: 100 }) graph.createNode('RECTANGLE', comp.id, { name: 'BG' }) - const instance = graph.createInstance(comp.id, pageId(graph)) + const instance = expectDefined(graph.createInstance(comp.id, pageId(graph)), 'instance') expect(instance).toBeDefined() expect(graph.getChildren(expectDefined(instance, 'instance').id)).toHaveLength(1) @@ -442,7 +441,7 @@ describe('SceneGraph', () => { height: 40 }) graph.createNode('RECTANGLE', comp.id, { name: 'BG' }) - const instance = graph.createInstance(comp.id, pageId(graph)) + const instance = expectDefined(graph.createInstance(comp.id, pageId(graph)), 'instance') expect(instance).toBeDefined() expect(expectDefined(instance, 'instance').type).toBe('INSTANCE') diff --git a/tests/engine/scene-graph/basic/helpers.ts b/packages/scene-graph/tests/basic/helpers.ts similarity index 81% rename from tests/engine/scene-graph/basic/helpers.ts rename to packages/scene-graph/tests/basic/helpers.ts index e87ec95a2..fd0952b8a 100644 --- a/tests/engine/scene-graph/basic/helpers.ts +++ b/packages/scene-graph/tests/basic/helpers.ts @@ -1,4 +1,4 @@ -import type { SceneGraph } from '@open-pencil/core' +import type { SceneGraph } from '@open-pencil/scene-graph' export function pageId(graph: SceneGraph) { return graph.getPages()[0].id } diff --git a/tests/engine/scene-graph/basic/hit-test.test.ts b/packages/scene-graph/tests/basic/hit-test.test.ts similarity index 95% rename from tests/engine/scene-graph/basic/hit-test.test.ts rename to packages/scene-graph/tests/basic/hit-test.test.ts index ade120e30..6fcee39e4 100644 --- a/tests/engine/scene-graph/basic/hit-test.test.ts +++ b/packages/scene-graph/tests/basic/hit-test.test.ts @@ -1,7 +1,8 @@ import { describe, expect, test } from 'bun:test' -import { SceneGraph } from '@open-pencil/core' +import { SceneGraph } from '@open-pencil/scene-graph' +import { expectDefined } from '../helpers/assert' import { pageId, rect } from './helpers' describe('hitTest', () => { @@ -114,7 +115,10 @@ describe('hitTest', () => { width: 30, height: 30 }) - const instId = graph.createInstance(compId, pageId(graph), { x: 300, y: 0 }).id + const instId = expectDefined( + graph.createInstance(compId, pageId(graph), { x: 300, y: 0 }), + 'instance' + ).id // Hit on instance's child area — returns instance (opaque container) expect(graph.hitTest(320, 20, pageId(graph))?.id).toBe(instId) // Miss on instance's empty area (no fills) diff --git a/tests/engine/scene-graph/basic/layout-affecting-keys.test.ts b/packages/scene-graph/tests/basic/layout-affecting-keys.test.ts similarity index 97% rename from tests/engine/scene-graph/basic/layout-affecting-keys.test.ts rename to packages/scene-graph/tests/basic/layout-affecting-keys.test.ts index 8d8e04a4a..35b501d38 100644 --- a/tests/engine/scene-graph/basic/layout-affecting-keys.test.ts +++ b/packages/scene-graph/tests/basic/layout-affecting-keys.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'bun:test' -import { SceneGraph } from '@open-pencil/core' +import { SceneGraph } from '@open-pencil/scene-graph' describe('LAYOUT_AFFECTING_KEYS membership', () => { test('contains all direct transform properties', () => { diff --git a/tests/engine/scene-graph/basic/text-picture-keys.test.ts b/packages/scene-graph/tests/basic/text-picture-keys.test.ts similarity index 96% rename from tests/engine/scene-graph/basic/text-picture-keys.test.ts rename to packages/scene-graph/tests/basic/text-picture-keys.test.ts index ed5a00816..02698690b 100644 --- a/tests/engine/scene-graph/basic/text-picture-keys.test.ts +++ b/packages/scene-graph/tests/basic/text-picture-keys.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'bun:test' -import { SceneGraph } from '@open-pencil/core' +import { SceneGraph } from '@open-pencil/scene-graph' describe('text rendering invalidation keys', () => { test('TEXT_PICTURE_KEYS contains rendered-picture properties', () => { diff --git a/tests/engine/scene-graph/basic/update-node.test.ts b/packages/scene-graph/tests/basic/update-node.test.ts similarity index 97% rename from tests/engine/scene-graph/basic/update-node.test.ts rename to packages/scene-graph/tests/basic/update-node.test.ts index 1579b3d56..0b2e1b8b0 100644 --- a/tests/engine/scene-graph/basic/update-node.test.ts +++ b/packages/scene-graph/tests/basic/update-node.test.ts @@ -1,9 +1,8 @@ import { describe, expect, test } from 'bun:test' -import { SceneGraph } from '@open-pencil/core' - -import { expectDefined } from '#tests/helpers/assert' +import { SceneGraph } from '@open-pencil/scene-graph' +import { expectDefined } from '../helpers/assert' import { pageId, rect } from './helpers' describe('updateNode', () => { @@ -49,7 +48,7 @@ describe('updateNode', () => { width: 50, height: 50 }) - const inst = graph.createInstance(comp1.id, page, { x: 200, y: 0 }) + const inst = expectDefined(graph.createInstance(comp1.id, page, { x: 200, y: 0 }), 'instance') // Instance should be indexed under comp1 expect(graph.instanceIndex.get(comp1.id)?.has(inst.id)).toBe(true) // Re-point instance to comp2 diff --git a/tests/engine/scene-graph/basic/variables.test.ts b/packages/scene-graph/tests/basic/variables.test.ts similarity index 99% rename from tests/engine/scene-graph/basic/variables.test.ts rename to packages/scene-graph/tests/basic/variables.test.ts index cee455cf1..5987192da 100644 --- a/tests/engine/scene-graph/basic/variables.test.ts +++ b/packages/scene-graph/tests/basic/variables.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'bun:test' -import { SceneGraph } from '@open-pencil/core' +import { SceneGraph } from '@open-pencil/scene-graph' describe('Variables', () => { function pageId(graph: SceneGraph): string { diff --git a/tests/engine/scene-graph/clone/node-props.test.ts b/packages/scene-graph/tests/clone/node-props.test.ts similarity index 87% rename from tests/engine/scene-graph/clone/node-props.test.ts rename to packages/scene-graph/tests/clone/node-props.test.ts index b7039aeb8..0eaa7f881 100644 --- a/tests/engine/scene-graph/clone/node-props.test.ts +++ b/packages/scene-graph/tests/clone/node-props.test.ts @@ -1,9 +1,11 @@ import { describe, expect, test } from 'bun:test' -import { SceneGraph, type SceneNode } from '@open-pencil/core' +import { SceneGraph, type SceneNode } from '@open-pencil/scene-graph' import { getInstanceOverride, setInstanceOverride } from '@open-pencil/scene-graph' import { cloneNodeProps } from '@open-pencil/scene-graph/copy' +import { expectDefined, getNodeOrThrow } from '../helpers/assert' + function pageId(graph: SceneGraph): string { return graph.getPages()[0].id } @@ -22,9 +24,9 @@ describe('cloneNodeProps deep-copies vectorNetwork', () => { regions: [{ windingRule: 'NONZERO', loops: [[0, 1]] }] } }) - const original = graph.getNode(node.id) - const clone = graph.cloneTree(node.id, pageId(graph)) - const clonedNode = graph.getNode(clone.id) + const original = getNodeOrThrow(graph, node.id) + const clone = expectDefined(graph.cloneTree(node.id, pageId(graph)), 'clone') + const clonedNode = getNodeOrThrow(graph, clone.id) expect(clonedNode.vectorNetwork).not.toBe(original.vectorNetwork) if (original.vectorNetwork && clonedNode.vectorNetwork) { @@ -47,9 +49,9 @@ describe('cloneNodeProps deep-copies vectorNetwork', () => { regions: [] } }) - const original = graph.getNode(node.id) - const clone = graph.cloneTree(node.id, pageId(graph)) - const clonedNode = graph.getNode(clone.id) + const original = getNodeOrThrow(graph, node.id) + const clone = expectDefined(graph.cloneTree(node.id, pageId(graph)), 'clone') + const clonedNode = getNodeOrThrow(graph, clone.id) if (clonedNode.vectorNetwork) clonedNode.vectorNetwork.vertices[0].x = 999 expect(original.vectorNetwork?.vertices[0].x).toBe(0) @@ -66,9 +68,9 @@ describe('cloneNodeProps deep-copies textPicture and derivedTextGlyphs', () => { fontFamily: 'Inter', textPicture: new Uint8Array([1, 2, 3, 4]) }) - const original = graph.getNode(node.id) - const clone = graph.cloneTree(node.id, pageId(graph)) - const clonedNode = graph.getNode(clone.id) + const original = getNodeOrThrow(graph, node.id) + const clone = expectDefined(graph.cloneTree(node.id, pageId(graph)), 'clone') + const clonedNode = getNodeOrThrow(graph, clone.id) expect(clonedNode.textPicture).not.toBe(original.textPicture) if (original.textPicture && clonedNode.textPicture) { @@ -89,9 +91,9 @@ describe('cloneNodeProps deep-copies textPicture and derivedTextGlyphs', () => { { commandsBlob: new Uint8Array([30, 40]), x: 10, y: 0, fontSize: 14 } ] }) - const original = graph.getNode(node.id) - const clone = graph.cloneTree(node.id, pageId(graph)) - const clonedNode = graph.getNode(clone.id) + const original = getNodeOrThrow(graph, node.id) + const clone = expectDefined(graph.cloneTree(node.id, pageId(graph)), 'clone') + const clonedNode = getNodeOrThrow(graph, clone.id) expect(clonedNode.derivedTextGlyphs).not.toBe(original.derivedTextGlyphs) if (original.derivedTextGlyphs && clonedNode.derivedTextGlyphs) { @@ -117,16 +119,16 @@ describe('cloneNodeProps deep-copies overrides values', () => { fills: [{ type: 'SOLID', color: { r: 1, g: 0, b: 0, a: 1 }, visible: true, opacity: 1 }] }) - const instance = graph.createInstance(component.id, page) + const instance = expectDefined(graph.createInstance(component.id, page), 'instance') if (!instance) throw new Error('instance failed') const instanceChild = graph.getChildren(instance.id)[0] setInstanceOverride(instance.instanceOverrides, instance.id, instanceChild.id, 'fills', [ { type: 'SOLID', color: { r: 0, g: 0, b: 1, a: 1 }, visible: true, opacity: 1 } ]) - const clone = graph.cloneTree(instance.id, page) + const clone = expectDefined(graph.cloneTree(instance.id, page), 'clone') if (!clone) throw new Error('clone failed') - const clonedInstance = graph.getNode(clone.id) + const clonedInstance = getNodeOrThrow(graph, clone.id) const cloneOverrideVal = getInstanceOverride( clonedInstance.instanceOverrides, clonedInstance.id, @@ -159,6 +161,7 @@ describe('cloneNodeProps fig import mode', () => { format: 'fig', id: 'fig-source', orderKey: 'a', + editedFields: [], fig: { rawSize: { x: 100, y: 40 }, rawTransform: null, @@ -187,7 +190,7 @@ describe('cloneNodeProps coverage guard', () => { const graph = new SceneGraph() const page = pageId(graph) const node = graph.createNode('RECTANGLE', page, { name: 'Rect' }) - const raw = graph.getNode(node.id) + const raw = getNodeOrThrow(graph, node.id) const patch: Partial< Pick< SceneNode, @@ -224,6 +227,7 @@ describe('cloneNodeProps coverage guard', () => { format: 'fig', id: 'source-node', orderKey: 'a', + editedFields: [], fig: { rawSize: { x: 1, y: 2 }, rawTransform: null, diff --git a/tests/engine/scene-graph/clone/tree.test.ts b/packages/scene-graph/tests/clone/tree.test.ts similarity index 85% rename from tests/engine/scene-graph/clone/tree.test.ts rename to packages/scene-graph/tests/clone/tree.test.ts index 2df9a6b36..fb8e6865c 100644 --- a/tests/engine/scene-graph/clone/tree.test.ts +++ b/packages/scene-graph/tests/clone/tree.test.ts @@ -1,6 +1,8 @@ import { describe, expect, test } from 'bun:test' -import { SceneGraph } from '@open-pencil/core' +import { SceneGraph } from '@open-pencil/scene-graph' + +import { expectDefined, getNodeOrThrow } from '../helpers/assert' describe('SceneGraph.cloneTree', () => { test('clone clears source.id from the clone', () => { @@ -15,11 +17,11 @@ describe('SceneGraph.cloneTree', () => { graph.updateNode(rect.id, { source: { ...rect.source, id: '1:42', orderKey: '!', format: 'fig' } }) - const original = graph.getNode(rect.id) + const original = getNodeOrThrow(graph, rect.id) expect(original).toBeDefined() expect(original.source.id).toBe('1:42') - const clone = graph.cloneTree(rect.id, page.id) + const clone = expectDefined(graph.cloneTree(rect.id, page.id), 'clone') expect(clone).not.toBeNull() // Clone must NOT carry the original's Figma GUID expect(clone.source.id).toBeNull() @@ -40,9 +42,9 @@ describe('SceneGraph.cloneTree', () => { source: { ...rect.source, id: '1:99', format: 'fig' } }) - const clone1 = graph.cloneTree(rect.id, page.id) + const clone1 = expectDefined(graph.cloneTree(rect.id, page.id), 'clone') expect(clone1).not.toBeNull() - const clone2 = graph.cloneTree(clone1.id, page.id) + const clone2 = expectDefined(graph.cloneTree(clone1.id, page.id), 'clone') expect(clone2).not.toBeNull() expect(clone2.source.id).toBeNull() }) @@ -62,12 +64,13 @@ describe('SceneGraph.cloneTree', () => { type: 'SOLID', color: { r: 1, g: 0, b: 0, a: 1 }, visible: true, + opacity: 1, blendMode: 'NORMAL' as const } ] }) - const clone = graph.cloneTree(rect.id, page.id) + const clone = expectDefined(graph.cloneTree(rect.id, page.id), 'clone') expect(clone).not.toBeNull() expect(clone.name).toBe('Original') expect(clone.width).toBe(100) @@ -95,7 +98,7 @@ describe('SceneGraph.cloneTree', () => { source: { ...child.source, id: '2:11', format: 'fig' } }) - const clone = graph.cloneTree(frame.id, page.id) + const clone = expectDefined(graph.cloneTree(frame.id, page.id), 'clone') expect(clone).not.toBeNull() expect(clone.source.id).toBeNull() const clonedChild = graph.getChildren(clone.id)[0] @@ -129,10 +132,10 @@ describe('SceneGraph.cloneTree', () => { } }) - const original = graph.getNode(rect.id) + const original = getNodeOrThrow(graph, rect.id) expect(original.source.fig.rawNodeFields).toEqual({ visible: true, opacity: 1 }) - const clone = graph.cloneTree(rect.id, page.id) + const clone = expectDefined(graph.cloneTree(rect.id, page.id), 'clone') expect(clone).not.toBeNull() // Mutate the clone's source.fig (simulating what clearEditedSourceMetadata does) diff --git a/tests/engine/scene-graph/components/properties.test.ts b/packages/scene-graph/tests/components/properties.test.ts similarity index 100% rename from tests/engine/scene-graph/components/properties.test.ts rename to packages/scene-graph/tests/components/properties.test.ts diff --git a/tests/engine/scene-graph/copy.test.ts b/packages/scene-graph/tests/copy.test.ts similarity index 98% rename from tests/engine/scene-graph/copy.test.ts rename to packages/scene-graph/tests/copy.test.ts index 3d3a018ed..760fcfe02 100644 --- a/tests/engine/scene-graph/copy.test.ts +++ b/packages/scene-graph/tests/copy.test.ts @@ -1,6 +1,6 @@ import { describe, test, expect } from 'bun:test' -import type { Fill, Stroke, Effect, StyleRun, GeometryPath } from '@open-pencil/core' +import type { Fill, Stroke, Effect, StyleRun, GeometryPath } from '@open-pencil/scene-graph' import { copyFill, copyFills, @@ -11,7 +11,7 @@ import { scaleGeometryPaths } from '@open-pencil/scene-graph/copy' -import { expectDefined } from '#tests/helpers/assert' +import { expectDefined } from './helpers/assert' describe('copy helpers — mutation isolation', () => { test('copyFill: mutating copy does not affect original', () => { diff --git a/packages/scene-graph/tests/helpers/assert.ts b/packages/scene-graph/tests/helpers/assert.ts new file mode 100644 index 000000000..d4d48ef65 --- /dev/null +++ b/packages/scene-graph/tests/helpers/assert.ts @@ -0,0 +1,12 @@ +import type { SceneGraph, SceneNode } from '@open-pencil/scene-graph' + +export function expectDefined(value: T | null | undefined, label = 'value'): NonNullable { + if (value == null) { + throw new Error(`${label} was expected to be defined`) + } + return value +} + +export function getNodeOrThrow(graph: SceneGraph, id: string): SceneNode { + return expectDefined(graph.getNode(id), `node ${id}`) +} diff --git a/packages/scene-graph/src/instance-overrides.test.ts b/packages/scene-graph/tests/instance-overrides.test.ts similarity index 98% rename from packages/scene-graph/src/instance-overrides.test.ts rename to packages/scene-graph/tests/instance-overrides.test.ts index 7f24ec40a..5a8a68187 100644 --- a/packages/scene-graph/src/instance-overrides.test.ts +++ b/packages/scene-graph/tests/instance-overrides.test.ts @@ -10,7 +10,7 @@ import { hasInstanceOverride, serializeInstanceOverrideState, setInstanceOverride -} from './instance-overrides' +} from '../src/instance-overrides' describe('instance override state', () => { test('stores self and descendant values structurally', () => { diff --git a/tests/engine/scene-graph/instance-sync-duplication.test.ts b/packages/scene-graph/tests/instance-sync-duplication.test.ts similarity index 99% rename from tests/engine/scene-graph/instance-sync-duplication.test.ts rename to packages/scene-graph/tests/instance-sync-duplication.test.ts index abe5157c9..ffe5a4cb8 100644 --- a/tests/engine/scene-graph/instance-sync-duplication.test.ts +++ b/packages/scene-graph/tests/instance-sync-duplication.test.ts @@ -144,7 +144,7 @@ describe('instance synchronization child deduplication', () => { const page = graph.addPage('Page') const comp = graph.createNode('COMPONENT', page.id, { name: 'Menu' }) - const _item1 = graph.createNode('FRAME', comp.id, { name: 'Item 1' }) + graph.createNode('FRAME', comp.id, { name: 'Item 1' }) const inst = graph.createNode('INSTANCE', page.id, { name: 'Menu Instance', @@ -156,7 +156,7 @@ describe('instance synchronization child deduplication', () => { }) // Now add a second child to the component - const _item2 = graph.createNode('FRAME', comp.id, { name: 'Item 2', height: 32 }) + graph.createNode('FRAME', comp.id, { name: 'Item 2', height: 32 }) graph.syncInstances(comp.id) expect(inst.childIds.length).toBe(2) diff --git a/tests/engine/scene-graph/layout-guides.test.ts b/packages/scene-graph/tests/layout-guides.test.ts similarity index 100% rename from tests/engine/scene-graph/layout-guides.test.ts rename to packages/scene-graph/tests/layout-guides.test.ts diff --git a/tests/engine/scene-graph/mutation-impact.test.ts b/packages/scene-graph/tests/mutation-impact.test.ts similarity index 100% rename from tests/engine/scene-graph/mutation-impact.test.ts rename to packages/scene-graph/tests/mutation-impact.test.ts diff --git a/tests/engine/scene-graph/reparent-transform.test.ts b/packages/scene-graph/tests/reparent-transform.test.ts similarity index 100% rename from tests/engine/scene-graph/reparent-transform.test.ts rename to packages/scene-graph/tests/reparent-transform.test.ts diff --git a/tests/engine/scene-graph/source-metadata.test.ts b/packages/scene-graph/tests/source-metadata.test.ts similarity index 100% rename from tests/engine/scene-graph/source-metadata.test.ts rename to packages/scene-graph/tests/source-metadata.test.ts diff --git a/tests/engine/scene-graph/undo/batch.test.ts b/packages/scene-graph/tests/undo/batch.test.ts similarity index 100% rename from tests/engine/scene-graph/undo/batch.test.ts rename to packages/scene-graph/tests/undo/batch.test.ts diff --git a/tests/engine/scene-graph/undo/mutation.test.ts b/packages/scene-graph/tests/undo/mutation.test.ts similarity index 94% rename from tests/engine/scene-graph/undo/mutation.test.ts rename to packages/scene-graph/tests/undo/mutation.test.ts index 34e729e16..491b122c6 100644 --- a/tests/engine/scene-graph/undo/mutation.test.ts +++ b/packages/scene-graph/tests/undo/mutation.test.ts @@ -1,9 +1,8 @@ import { describe, test, expect } from 'bun:test' -import { SceneGraph, UndoManager } from '@open-pencil/core' -import type { JSONObject } from '@open-pencil/scene-graph/primitives' +import { SceneGraph, UndoManager, type SceneNode } from '@open-pencil/scene-graph' -import { getNodeOrThrow } from '#tests/helpers/assert' +import { getNodeOrThrow } from '../helpers/assert' // --------------------------------------------------------------------------- // SceneGraph + UndoManager — integration (updateNodeWithUndo pattern) @@ -22,7 +21,7 @@ describe('SceneGraph + UndoManager — updateNode undo integration', () => { ) { const node = getNodeOrThrow(graph, id) const previous = Object.fromEntries( - (Object.keys(changes) as string[]).map((k) => [k, (node as JSONObject)[k]]) + (Object.keys(changes) as Array).map((k) => [k, node[k]]) ) graph.updateNode(id, changes) undo.push({ diff --git a/tests/engine/scene-graph/variable/binding/instance-override.test.ts b/packages/scene-graph/tests/variable/binding/instance-override.test.ts similarity index 99% rename from tests/engine/scene-graph/variable/binding/instance-override.test.ts rename to packages/scene-graph/tests/variable/binding/instance-override.test.ts index 46e07a4f4..c9292f040 100644 --- a/tests/engine/scene-graph/variable/binding/instance-override.test.ts +++ b/packages/scene-graph/tests/variable/binding/instance-override.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'bun:test' -import { SceneGraph } from '@open-pencil/core' +import { SceneGraph } from '@open-pencil/scene-graph' import { getInstanceOverride } from '@open-pencil/scene-graph' function pageId(graph: SceneGraph): string { diff --git a/tests/engine/scene-graph/vector-network.test.ts b/packages/scene-graph/tests/vector-network.test.ts similarity index 96% rename from tests/engine/scene-graph/vector-network.test.ts rename to packages/scene-graph/tests/vector-network.test.ts index 073a125a0..8897d03c8 100644 --- a/tests/engine/scene-graph/vector-network.test.ts +++ b/packages/scene-graph/tests/vector-network.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from 'bun:test' import { mergeVectorNetworks, type VectorNetwork } from '@open-pencil/scene-graph' -import { expectDefined } from '#tests/helpers/assert' +import { expectDefined } from './helpers/assert' function lineNetwork(x: number): VectorNetwork { return { diff --git a/tests/engine/scene-graph/world-bounds.test.ts b/packages/scene-graph/tests/world-bounds.test.ts similarity index 100% rename from tests/engine/scene-graph/world-bounds.test.ts rename to packages/scene-graph/tests/world-bounds.test.ts diff --git a/tests/engine/scene-graph/world-transform.test.ts b/packages/scene-graph/tests/world-transform.test.ts similarity index 100% rename from tests/engine/scene-graph/world-transform.test.ts rename to packages/scene-graph/tests/world-transform.test.ts diff --git a/packages/scene-graph/tsconfig.test.json b/packages/scene-graph/tsconfig.test.json new file mode 100644 index 000000000..cccf5a099 --- /dev/null +++ b/packages/scene-graph/tsconfig.test.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "emitDeclarationOnly": false + }, + "include": ["src", "tests"] +} diff --git a/tests/AGENTS.md b/tests/AGENTS.md index c8adcd763..e20790629 100644 --- a/tests/AGENTS.md +++ b/tests/AGENTS.md @@ -5,7 +5,7 @@ ## Placement - Package-local tests mirror source domains under `packages//tests/`; central app tests mirror `src/app/**` under `tests/app/`; `tests/integration/` requires a genuinely cross-owner contract; E2E under `tests/e2e/` follows user workflows; native and Figma acceptance are explicit exceptions. -- Existing `tests/engine/**` domains migrate together with runner discovery: `tools/unit-tests/src/shards.ts` lists each owner's canonical home and its current `tests/engine` directories, so a move is a `git mv` plus imports. Do not create competing homes or undiscovered suites. +- Existing `tests/engine/**` domains migrate together with runner discovery: `tools/unit-tests/src/shards.ts` lists each owner's canonical home and its current `tests/engine` directories, so a move is a `git mv` plus imports. `bun run check:test-homes` rejects any test added under `tests/engine` and any stale entry in `tools/unit-tests/engine-baseline.txt`; new tests go to the canonical home, and a moved file is removed from the baseline (`--write` regenerates it). Scene Graph has migrated to `packages/scene-graph/tests`. - Owner-local helpers and fixtures stay local; only genuinely shared support goes under central `tests/helpers//` and `tests/fixtures/`. MCP transport tests: `tests/engine/mcp/{server,stdio,transport}` with `tests/helpers/mcp`. - Never commit temporary, diagnostic, or profile specs; keep them in ignored `scratch/`. diff --git a/tests/engine/scene-graph/clone/binding-corruption.test.ts b/tests/engine/editor/clipboard/binding-corruption.test.ts similarity index 100% rename from tests/engine/scene-graph/clone/binding-corruption.test.ts rename to tests/engine/editor/clipboard/binding-corruption.test.ts diff --git a/tests/engine/scene-graph/individual-strokes.test.ts b/tests/engine/figma/api/stroke/individual-strokes.test.ts similarity index 100% rename from tests/engine/scene-graph/individual-strokes.test.ts rename to tests/engine/figma/api/stroke/individual-strokes.test.ts diff --git a/tests/engine/scene-graph/variable/binding/extended.test.ts b/tests/engine/figma/api/variable-binding-extended.test.ts similarity index 100% rename from tests/engine/scene-graph/variable/binding/extended.test.ts rename to tests/engine/figma/api/variable-binding-extended.test.ts diff --git a/tests/engine/scene-graph/instance-overrides.test.ts b/tests/engine/io/fig/import/instance-overrides.test.ts similarity index 100% rename from tests/engine/scene-graph/instance-overrides.test.ts rename to tests/engine/io/fig/import/instance-overrides.test.ts diff --git a/tests/engine/scene-graph/plugin-data.test.ts b/tests/engine/io/fig/plugin-data.test.ts similarity index 100% rename from tests/engine/scene-graph/plugin-data.test.ts rename to tests/engine/io/fig/plugin-data.test.ts diff --git a/tests/engine/scene-graph/variable/binding/validation.test.ts b/tests/engine/lint/variable-binding-validation.test.ts similarity index 100% rename from tests/engine/scene-graph/variable/binding/validation.test.ts rename to tests/engine/lint/variable-binding-validation.test.ts diff --git a/tools/AGENTS.md b/tools/AGENTS.md index a046aa45f..33e5eba22 100644 --- a/tools/AGENTS.md +++ b/tools/AGENTS.md @@ -3,7 +3,7 @@ Private tooling lives under `tools//{src,tests}` with kebab-case domains and focused tests; `bun run test:tools` runs every tool package with a `test` script. `scripts/` may contain only tiny compatibility entrypoints. - Resolve the workspace with `resolveWorkspaceRoot` from `@open-pencil/package-artifacts`, not parent-directory traversal. Use domain aliases for cross-directory tooling imports, including tests; keep sibling imports relative. -- `tools/architecture` (Steiger rules) backs `check:arch`; `tools/docs` backs `check:docs`, including the guide-map check; `tools/i18n`, `tools/release-packages`, `tools/unit-tests`, `tools/ci`, and `tools/brand` own their namesake commands. +- `tools/architecture` (Steiger rules) backs `check:arch`; `tools/docs` backs `check:docs`, including the guide-map check; `tools/unit-tests` backs the unit shards and `check:test-homes` with its `engine-baseline.txt`; `tools/i18n`, `tools/release-packages`, `tools/ci`, and `tools/brand` own their namesake commands. ## CI diff --git a/tools/unit-tests/engine-baseline.txt b/tools/unit-tests/engine-baseline.txt new file mode 100644 index 000000000..3a35c757b --- /dev/null +++ b/tools/unit-tests/engine-baseline.txt @@ -0,0 +1,592 @@ +# Unit tests still under tests/engine, the migration debt from +# packages/docs/development/testing.md. New tests go to the owner's canonical +# home; remove a line here when its file moves. Regenerate with +# `bun tools/unit-tests/src/check-homes.ts --write`. +tests/engine/acp/permission.test.ts +tests/engine/acp/transport.test.ts +tests/engine/app/ai/attachment/image.test.ts +tests/engine/app/ai/catalog.test.ts +tests/engine/app/ai/context.test.ts +tests/engine/app/ai/failure.test.ts +tests/engine/app/ai/harness/transport.test.ts +tests/engine/app/ai/markdown.test.ts +tests/engine/app/ai/model-roles.test.ts +tests/engine/app/ai/model-settings.test.ts +tests/engine/app/ai/model.test.ts +tests/engine/app/ai/profile-selection.test.ts +tests/engine/app/ai/vision.test.ts +tests/engine/app/automation/child-ready.test.ts +tests/engine/app/automation/mcp-runtime.test.ts +tests/engine/app/automation/mcp-vite-plugin.test.ts +tests/engine/app/automation/portless-route.test.ts +tests/engine/app/cache.test.ts +tests/engine/app/chat/history/continuation.test.ts +tests/engine/app/chat/history/controller.test.ts +tests/engine/app/chat/history/idb.test.ts +tests/engine/app/chat/step-limit.test.ts +tests/engine/app/chat/tool-access.test.ts +tests/engine/app/chat/tool-state.test.ts +tests/engine/app/clipboard/component-dependencies.test.ts +tests/engine/app/clipboard/dependency-bindings.test.ts +tests/engine/app/clipboard/figma-images.test.ts +tests/engine/app/clipboard/html.test.ts +tests/engine/app/clipboard/keyboard.test.ts +tests/engine/app/clipboard/memory.test.ts +tests/engine/app/clipboard/notifications.test.ts +tests/engine/app/clipboard/references.test.ts +tests/engine/app/clipboard/styles.test.ts +tests/engine/app/clipboard/variables.test.ts +tests/engine/app/code/sandbox.test.ts +tests/engine/app/demo.test.ts +tests/engine/app/demo/viewport.test.ts +tests/engine/app/diagnostics/events.test.ts +tests/engine/app/diagnostics/recorder.test.ts +tests/engine/app/diagnostics/settings.test.ts +tests/engine/app/diagnostics/subscriptions.test.ts +tests/engine/app/diagnostics/transport.test.ts +tests/engine/app/document/autosave/create.test.ts +tests/engine/app/document/close/controller.test.ts +tests/engine/app/document/close/exit.test.ts +tests/engine/app/document/close/native.test.ts +tests/engine/app/document/io/browser.test.ts +tests/engine/app/document/io/changes.test.ts +tests/engine/app/document/io/deep-link.test.ts +tests/engine/app/document/io/imported-document.test.ts +tests/engine/app/document/io/pending-open.test.ts +tests/engine/app/document/io/source-identity.test.ts +tests/engine/app/document/io/web-link.test.ts +tests/engine/app/document/recovery/controller.test.ts +tests/engine/app/document/recovery/store.test.ts +tests/engine/app/editor/fonts/browser-fetch.test.ts +tests/engine/app/editor/panes.test.ts +tests/engine/app/editor/preparation/controller.test.ts +tests/engine/app/editor/preparation/multi-tab.test.ts +tests/engine/app/editor/selection/focus.test.ts +tests/engine/app/editor/store-path.test.ts +tests/engine/app/editor/variables.test.ts +tests/engine/app/editor/vectorize/providers.test.ts +tests/engine/app/external-link.test.ts +tests/engine/app/font-loading.test.ts +tests/engine/app/integrations/mcp-connections.test.ts +tests/engine/app/integrations/pi-mcp.test.ts +tests/engine/app/integrations/storage/cancellation.test.ts +tests/engine/app/integrations/storage/cors.test.ts +tests/engine/app/integrations/storage/list-objects.test.ts +tests/engine/app/integrations/storage/namespace.test.ts +tests/engine/app/integrations/storage/region.test.ts +tests/engine/app/integrations/storage/registry.test.ts +tests/engine/app/recent-files/store.test.ts +tests/engine/app/runtime/config.test.ts +tests/engine/app/settings/ai-tool-access.test.ts +tests/engine/app/settings/credential-migration.test.ts +tests/engine/app/settings/credentials.test.ts +tests/engine/app/settings/credentials/access-check.test.ts +tests/engine/app/settings/font.test.ts +tests/engine/app/settings/mcp-tool-access.test.ts +tests/engine/app/settings/mcp-workflow.test.ts +tests/engine/app/settings/model-schema.test.ts +tests/engine/app/settings/motion.test.ts +tests/engine/app/settings/navigation.test.ts +tests/engine/app/settings/storage-workflow.test.ts +tests/engine/app/settings/validation.test.ts +tests/engine/app/settings/vectorize-workflow.test.ts +tests/engine/app/shell/menu/file-batch.test.ts +tests/engine/app/shell/menu/schema.test.ts +tests/engine/app/shell/menu/shortcut.test.ts +tests/engine/app/shell/toast.test.ts +tests/engine/app/storage/cache-eviction.test.ts +tests/engine/app/storage/format-bytes.test.ts +tests/engine/app/storage/local-store.test.ts +tests/engine/app/storage/persist.test.ts +tests/engine/app/storage/reconcile.test.ts +tests/engine/app/storage/sync-outbox.test.ts +tests/engine/app/storage/unique-name.test.ts +tests/engine/app/tabs/open-file-dedup.test.ts +tests/engine/app/tauri/http.test.ts +tests/engine/app/usage/summarize.test.ts +tests/engine/app/vector-edit-transforms.test.ts +tests/engine/cli/app-client.test.ts +tests/engine/cli/eval.test.ts +tests/engine/cli/export/font-policy.test.ts +tests/engine/cli/export/index.test.ts +tests/engine/cli/export/storybook.test.ts +tests/engine/cli/fonts.test.ts +tests/engine/cli/headless.test.ts +tests/engine/cli/import.test.ts +tests/engine/cli/overlaps.test.ts +tests/engine/clipboard/figma/bounds.test.ts +tests/engine/clipboard/figma/codec.test.ts +tests/engine/clipboard/figma/html.test.ts +tests/engine/clipboard/figma/images.test.ts +tests/engine/clipboard/fixtures/gold-preview-roundtrip.test.ts +tests/engine/clipboard/openpencil/green-50.test.ts +tests/engine/clipboard/openpencil/images.test.ts +tests/engine/collab/room-id.test.ts +tests/engine/collab/yjs-sync.test.ts +tests/engine/color/basic.test.ts +tests/engine/color/color-model.test.ts +tests/engine/color/document-space-conversion.test.ts +tests/engine/color/okhcl/basic.test.ts +tests/engine/color/okhcl/document-color-space.test.ts +tests/engine/color/okhcl/toggle-conversion.test.ts +tests/engine/color/okhcl/ui-logic.test.ts +tests/engine/color/solid-commit.test.ts +tests/engine/core/cache/resource.test.ts +tests/engine/core/editor/text/path-edit.test.ts +tests/engine/core/editor/text/start-editing-gate.test.ts +tests/engine/core/text/path-layout.test.ts +tests/engine/dom-css/conversion.test.ts +tests/engine/dom-css/jsx.test.ts +tests/engine/dom-css/runtime.test.ts +tests/engine/dom-css/tailwind.test.ts +tests/engine/editor/clipboard/binding-corruption.test.ts +tests/engine/editor/clipboard/delete.test.ts +tests/engine/editor/clipboard/figma-images.test.ts +tests/engine/editor/clipboard/file-placement.test.ts +tests/engine/editor/clipboard/import-nodes/components.test.ts +tests/engine/editor/clipboard/import-nodes/filtering.test.ts +tests/engine/editor/clipboard/import-nodes/hierarchy.test.ts +tests/engine/editor/clipboard/import-nodes/instance-linkage.test.ts +tests/engine/editor/clipboard/import-nodes/paste-scope.test.ts +tests/engine/editor/clipboard/import-nodes/styling.test.ts +tests/engine/editor/clipboard/import-nodes/text/metrics.test.ts +tests/engine/editor/clipboard/import-nodes/undo/redo.test.ts +tests/engine/editor/clipboard/paste-target.test.ts +tests/engine/editor/clipboard/paste-to-replace.test.ts +tests/engine/editor/components/library-capabilities.test.ts +tests/engine/editor/components/properties.test.ts +tests/engine/editor/components/sync-layout-scope.test.ts +tests/engine/editor/components/variants.test.ts +tests/engine/editor/effects-and-resize.test.ts +tests/engine/editor/frame-presets.test.ts +tests/engine/editor/graph/events-integration.test.ts +tests/engine/editor/graph/events.test.ts +tests/engine/editor/graph/replacement.test.ts +tests/engine/editor/guides.test.ts +tests/engine/editor/history/atomic-tools.test.ts +tests/engine/editor/layout/mutation-impact.test.ts +tests/engine/editor/navigation-lifecycle.test.ts +tests/engine/editor/node-preview.test.ts +tests/engine/editor/nudge.test.ts +tests/engine/editor/opacity.test.ts +tests/engine/editor/pages/loading.test.ts +tests/engine/editor/preview-events.test.ts +tests/engine/editor/selection/alignment.test.ts +tests/engine/editor/selection/distribution.test.ts +tests/engine/editor/selection/flip.test.ts +tests/engine/editor/selection/hit-test.test.ts +tests/engine/editor/selection/properties.test.ts +tests/engine/editor/selection/semantics.test.ts +tests/engine/editor/structure/boolean.test.ts +tests/engine/editor/structure/flatten.test.ts +tests/engine/editor/structure/frame-selection.test.ts +tests/engine/editor/structure/state.test.ts +tests/engine/editor/structure/z-order.test.ts +tests/engine/editor/text-auto-resize.test.ts +tests/engine/editor/undo/basic.test.ts +tests/engine/editor/undo/batch-idle.test.ts +tests/engine/editor/undo/create-instance.test.ts +tests/engine/editor/undo/create-shape.test.ts +tests/engine/editor/undo/group-resize.test.ts +tests/engine/editor/undo/history-events.test.ts +tests/engine/editor/undo/redo-sequence.test.ts +tests/engine/editor/undo/variable-bindings.test.ts +tests/engine/editor/vectorize.test.ts +tests/engine/editor/view-state.test.ts +tests/engine/figma/api/absolute/position.test.ts +tests/engine/figma/api/auto-layout/basic.test.ts +tests/engine/figma/api/auto-layout/extras.test.ts +tests/engine/figma/api/clone.test.ts +tests/engine/figma/api/components.test.ts +tests/engine/figma/api/corner/radius.test.ts +tests/engine/figma/api/create/combine-as-variants.test.ts +tests/engine/figma/api/create/component-from-node.test.ts +tests/engine/figma/api/create/expose-instance-swap.test.ts +tests/engine/figma/api/create/instance-fill-override.test.ts +tests/engine/figma/api/document/pages.test.ts +tests/engine/figma/api/expanded.test.ts +tests/engine/figma/api/find/all-with-criteria.test.ts +tests/engine/figma/api/flatten-renderer.test.ts +tests/engine/figma/api/frozen/arrays.test.ts +tests/engine/figma/api/grouping-compat.test.ts +tests/engine/figma/api/grouping.test.ts +tests/engine/figma/api/images.test.ts +tests/engine/figma/api/internals/not-exposed.test.ts +tests/engine/figma/api/layout/plugin-compat.test.ts +tests/engine/figma/api/layout/sizing.test.ts +tests/engine/figma/api/mask.test.ts +tests/engine/figma/api/min-max/dimensions.test.ts +tests/engine/figma/api/node/creation.test.ts +tests/engine/figma/api/property/access.test.ts +tests/engine/figma/api/selection.test.ts +tests/engine/figma/api/serialization.test.ts +tests/engine/figma/api/stroke/details.test.ts +tests/engine/figma/api/stroke/individual-strokes.test.ts +tests/engine/figma/api/stubs.test.ts +tests/engine/figma/api/text/basic.test.ts +tests/engine/figma/api/text/extras.test.ts +tests/engine/figma/api/traversal.test.ts +tests/engine/figma/api/tree/operations.test.ts +tests/engine/figma/api/variable-binding-extended.test.ts +tests/engine/figma/api/variables.test.ts +tests/engine/figma/api/vector/paths.test.ts +tests/engine/geometry/angles.test.ts +tests/engine/geometry/arrow-caps.test.ts +tests/engine/geometry/bounds.test.ts +tests/engine/geometry/clip-polygon.test.ts +tests/engine/geometry/matrix.test.ts +tests/engine/geometry/rotation.test.ts +tests/engine/geometry/scene.test.ts +tests/engine/geometry/text-visual-bounds.test.ts +tests/engine/geometry/visual-bounds.test.ts +tests/engine/hit-test/basic.test.ts +tests/engine/hit-test/scope.test.ts +tests/engine/icons/api.test.ts +tests/engine/icons/iconify.test.ts +tests/engine/icons/svg.test.ts +tests/engine/io/dom-css-formats.test.ts +tests/engine/io/export-formats.test.ts +tests/engine/io/fig/export/auto-layout.test.ts +tests/engine/io/fig/export/boolean-operation.test.ts +tests/engine/io/fig/export/component-properties.test.ts +tests/engine/io/fig/export/corner-smoothing.test.ts +tests/engine/io/fig/export/font-feature-encoding.test.ts +tests/engine/io/fig/export/font-variations.test.ts +tests/engine/io/fig/export/guid-collision.test.ts +tests/engine/io/fig/export/images.test.ts +tests/engine/io/fig/export/lazy-population.test.ts +tests/engine/io/fig/export/masks.test.ts +tests/engine/io/fig/export/paint-schema-fields.test.ts +tests/engine/io/fig/export/parse-failures.test.ts +tests/engine/io/fig/export/path-text-reflow-export.test.ts +tests/engine/io/fig/export/rotation-transform.test.ts +tests/engine/io/fig/export/shared-styles.test.ts +tests/engine/io/fig/export/stroke-geometry.test.ts +tests/engine/io/fig/export/text.test.ts +tests/engine/io/fig/export/variables.test.ts +tests/engine/io/fig/export/worker.test.ts +tests/engine/io/fig/heavy/component-metadata.test.ts +tests/engine/io/fig/heavy/fixtures.test.ts +tests/engine/io/fig/import/basic.test.ts +tests/engine/io/fig/import/boolean-operation.test.ts +tests/engine/io/fig/import/cancellation.test.ts +tests/engine/io/fig/import/component-props.test.ts +tests/engine/io/fig/import/derived-symbol-data.test.ts +tests/engine/io/fig/import/edge-cases.test.ts +tests/engine/io/fig/import/field-protection.test.ts +tests/engine/io/fig/import/font-variations.test.ts +tests/engine/io/fig/import/group-reclassify.test.ts +tests/engine/io/fig/import/instance-overrides.test.ts +tests/engine/io/fig/import/instance-regressions.test.ts +tests/engine/io/fig/import/lazy-pages.test.ts +tests/engine/io/fig/import/legacy/arc/data.test.ts +tests/engine/io/fig/import/legacy/auto-layout/basic.test.ts +tests/engine/io/fig/import/legacy/blend/mode.test.ts +tests/engine/io/fig/import/legacy/clips/content.test.ts +tests/engine/io/fig/import/legacy/component/sets.test.ts +tests/engine/io/fig/import/legacy/constraints.test.ts +tests/engine/io/fig/import/legacy/corners.test.ts +tests/engine/io/fig/import/legacy/effects.test.ts +tests/engine/io/fig/import/legacy/fills.test.ts +tests/engine/io/fig/import/legacy/image/fills.test.ts +tests/engine/io/fig/import/legacy/independent/strokes.test.ts +tests/engine/io/fig/import/legacy/multiple/fills.test.ts +tests/engine/io/fig/import/legacy/node/types.test.ts +tests/engine/io/fig/import/legacy/shared-styles.test.ts +tests/engine/io/fig/import/legacy/text.test.ts +tests/engine/io/fig/import/legacy/transforms.test.ts +tests/engine/io/fig/import/legacy/variables.test.ts +tests/engine/io/fig/import/mask-oracle.test.ts +tests/engine/io/fig/import/masks.test.ts +tests/engine/io/fig/import/paint-schema-fields.test.ts +tests/engine/io/fig/import/pattern-paint-oracle.test.ts +tests/engine/io/fig/import/population-delta.test.ts +tests/engine/io/fig/import/property-integrity.test.ts +tests/engine/io/fig/import/raw-field-coverage.test.ts +tests/engine/io/fig/import/raw-metadata-oracle.test.ts +tests/engine/io/fig/import/rich-text-oracle.test.ts +tests/engine/io/fig/import/scaled-instance-strokes.test.ts +tests/engine/io/fig/import/schema-coverage.test.ts +tests/engine/io/fig/import/session-ownership.test.ts +tests/engine/io/fig/import/strokes.test.ts +tests/engine/io/fig/import/style-refs.test.ts +tests/engine/io/fig/import/text-path.test.ts +tests/engine/io/fig/import/text-sizing.test.ts +tests/engine/io/fig/instance/override-resave.test.ts +tests/engine/io/fig/instance/override-roundtrip.test.ts +tests/engine/io/fig/instance/swap-roundtrip.test.ts +tests/engine/io/fig/instance/text-marker-roundtrip.test.ts +tests/engine/io/fig/page-manifest.test.ts +tests/engine/io/fig/plugin-data.test.ts +tests/engine/io/fig/roundtrip/basic.test.ts +tests/engine/io/fig/roundtrip/exhaustive.test.ts +tests/engine/io/fig/roundtrip/export-settings.test.ts +tests/engine/io/fig/roundtrip/glyph-blob.test.ts +tests/engine/io/fig/roundtrip/group.test.ts +tests/engine/io/fig/roundtrip/source-metadata.test.ts +tests/engine/io/fig/roundtrip/variables.test.ts +tests/engine/io/fig/roundtrip/verifiers.test.ts +tests/engine/io/fig/thumbnail-page.test.ts +tests/engine/io/fig/thumbnail-range.test.ts +tests/engine/io/pptx/export/fidelity.test.ts +tests/engine/io/pptx/export/render.test.ts +tests/engine/io/subgraph.test.ts +tests/engine/io/svg/export/builder.test.ts +tests/engine/io/svg/export/paths.test.ts +tests/engine/io/svg/export/render.test.ts +tests/engine/io/svg/import.test.ts +tests/engine/io/svg/path-parse.test.ts +tests/engine/kiwi/instance-overrides/sync-clones.test.ts +tests/engine/kiwi/schema-runtime.test.ts +tests/engine/kiwi/serialize-fixes/auto-layout/transforms.test.ts +tests/engine/kiwi/serialize-fixes/borders/take-space.test.ts +tests/engine/kiwi/serialize-fixes/component-metadata.test.ts +tests/engine/kiwi/serialize-fixes/directionality.test.ts +tests/engine/kiwi/serialize-fixes/font/family.test.ts +tests/engine/kiwi/serialize-fixes/frame/mask.test.ts +tests/engine/kiwi/serialize-fixes/integration.test.ts +tests/engine/kiwi/serialize-fixes/line/height.test.ts +tests/engine/layout/absolute-position/cache.test.ts +tests/engine/layout/auto-layout/absolute/children-in-yoga-tree.test.ts +tests/engine/layout/auto-layout/absolute/positioning.test.ts +tests/engine/layout/auto-layout/alignment/counter-axis.test.ts +tests/engine/layout/auto-layout/alignment/primary-axis.test.ts +tests/engine/layout/auto-layout/counter-axis/align-content.test.ts +tests/engine/layout/auto-layout/counter-axis/spacing-no-wrap.test.ts +tests/engine/layout/auto-layout/edge-cases.test.ts +tests/engine/layout/auto-layout/fill/flex-basis.test.ts +tests/engine/layout/auto-layout/gap/mapping-correctness.test.ts +tests/engine/layout/auto-layout/hidden-children/basic.test.ts +tests/engine/layout/auto-layout/hidden-children/preserve-dimensions.test.ts +tests/engine/layout/auto-layout/horizontal/basic.test.ts +tests/engine/layout/auto-layout/imported-derived-layout/effective-generated-text.test.ts +tests/engine/layout/auto-layout/layout-align-self/extended-values.test.ts +tests/engine/layout/auto-layout/layout/imported-derived.test.ts +tests/engine/layout/auto-layout/layout/mode-none.test.ts +tests/engine/layout/auto-layout/min-max/constraints.test.ts +tests/engine/layout/auto-layout/mixed/sizing.test.ts +tests/engine/layout/auto-layout/nested/basic.test.ts +tests/engine/layout/auto-layout/nested/fill-children.test.ts +tests/engine/layout/auto-layout/self/alignment.test.ts +tests/engine/layout/auto-layout/sizing/modes.test.ts +tests/engine/layout/auto-layout/text/measurement.test.ts +tests/engine/layout/auto-layout/vertical/basic.test.ts +tests/engine/layout/auto-layout/wrap/layout.test.ts +tests/engine/layout/grid-layout.test.ts +tests/engine/library/discovery.test.ts +tests/engine/library/hash-normalization.test.ts +tests/engine/library/instance-updates.test.ts +tests/engine/library/materialize.test.ts +tests/engine/library/metadata.test.ts +tests/engine/library/preview.test.ts +tests/engine/library/routed-catalog.test.ts +tests/engine/library/selective.test.ts +tests/engine/library/snapshot.test.ts +tests/engine/library/source-publication.test.ts +tests/engine/library/storage-catalog.test.ts +tests/engine/library/update-groups.test.ts +tests/engine/library/update.test.ts +tests/engine/lint/basic.test.ts +tests/engine/lint/variable-binding-validation.test.ts +tests/engine/mcp/auth.test.ts +tests/engine/mcp/browser-rpc.test.ts +tests/engine/mcp/path-scoping.test.ts +tests/engine/mcp/result.test.ts +tests/engine/mcp/server/cleanup.test.ts +tests/engine/mcp/server/index.test.ts +tests/engine/mcp/server/security.test.ts +tests/engine/mcp/server/transport.test.ts +tests/engine/mcp/server/watchdog.test.ts +tests/engine/mcp/stdio/auth.test.ts +tests/engine/mcp/stdio/index.test.ts +tests/engine/mcp/stdio/reconnect.test.ts +tests/engine/mcp/test-support/discovery-isolation.test.ts +tests/engine/mcp/tools.test.ts +tests/engine/mcp/transport/paths.test.ts +tests/engine/mcp/webmcp-settings.test.ts +tests/engine/mcp/webmcp.test.ts +tests/engine/pen/file.test.ts +tests/engine/pen/layout.test.ts +tests/engine/pen/var-padding.test.ts +tests/engine/pen/variables.test.ts +tests/engine/profiler/capture-stack.test.ts +tests/engine/profiler/draw-call-counter.test.ts +tests/engine/profiler/frame-stats.test.ts +tests/engine/profiler/gpu-timer.test.ts +tests/engine/profiler/phase-timer.test.ts +tests/engine/profiler/speedscope.test.ts +tests/engine/random/basic.test.ts +tests/engine/render/canvas/arrow-heads-draw.test.ts +tests/engine/render/canvas/blend.test.ts +tests/engine/render/canvas/boolean.test.ts +tests/engine/render/canvas/cache.test.ts +tests/engine/render/canvas/chunks/cache.test.ts +tests/engine/render/canvas/chunks/context.test.ts +tests/engine/render/canvas/chunks/gold-preview.test.ts +tests/engine/render/canvas/chunks/index.test.ts +tests/engine/render/canvas/chunks/record.test.ts +tests/engine/render/canvas/component-label-text.test.ts +tests/engine/render/canvas/corner-smoothing.test.ts +tests/engine/render/canvas/culling.test.ts +tests/engine/render/canvas/derived-text.test.ts +tests/engine/render/canvas/diamond-gradient.test.ts +tests/engine/render/canvas/effect-raster-cache.test.ts +tests/engine/render/canvas/effect-raster-integration.test.ts +tests/engine/render/canvas/effects/edge-cases.test.ts +tests/engine/render/canvas/effects/inner-shadow.test.ts +tests/engine/render/canvas/effects/ordering.test.ts +tests/engine/render/canvas/effects/path-shapes.test.ts +tests/engine/render/canvas/effects/spread.test.ts +tests/engine/render/canvas/effects/text-shadow.test.ts +tests/engine/render/canvas/effects/types.test.ts +tests/engine/render/canvas/font-readiness.test.ts +tests/engine/render/canvas/gradient.test.ts +tests/engine/render/canvas/guides/draw.test.ts +tests/engine/render/canvas/guides/geometry.test.ts +tests/engine/render/canvas/guides/redlines.test.ts +tests/engine/render/canvas/image-fill.test.ts +tests/engine/render/canvas/label-cache.test.ts +tests/engine/render/canvas/labels/color.test.ts +tests/engine/render/canvas/labels/fonts.test.ts +tests/engine/render/canvas/labels/layout.test.ts +tests/engine/render/canvas/labels/paragraph-cache.test.ts +tests/engine/render/canvas/labels/paragraph-reuse.test.ts +tests/engine/render/canvas/labels/transform.test.ts +tests/engine/render/canvas/layout-grids.test.ts +tests/engine/render/canvas/line.test.ts +tests/engine/render/canvas/masks.test.ts +tests/engine/render/canvas/measurement.test.ts +tests/engine/render/canvas/normalize-dash-pattern.test.ts +tests/engine/render/canvas/painted-text.test.ts +tests/engine/render/canvas/picture-invalidation.test.ts +tests/engine/render/canvas/raster-export.test.ts +tests/engine/render/canvas/renderer-lifecycle.test.ts +tests/engine/render/canvas/retained-backing-pixels.test.ts +tests/engine/render/canvas/retained-backing.test.ts +tests/engine/render/canvas/schema-fill-fallback.test.ts +tests/engine/render/canvas/surface-replacement.test.ts +tests/engine/render/canvas/text-font-variations.test.ts +tests/engine/render/canvas/text-preparation-cache.test.ts +tests/engine/render/canvas/text.test.ts +tests/engine/render/canvas/tiles/cache.test.ts +tests/engine/render/canvas/tiles/geometry.test.ts +tests/engine/render/canvas/tiles/gold-preview.test.ts +tests/engine/render/canvas/tiles/planner.test.ts +tests/engine/render/canvas/tiles/render.test.ts +tests/engine/render/canvas/tiles/scheduler.test.ts +tests/engine/render/canvas/variable-modes.test.ts +tests/engine/render/canvas/visual/boolean.test.ts +tests/engine/render/jsx/builders.test.ts +tests/engine/render/jsx/component/properties.test.ts +tests/engine/render/jsx/component/property-references.test.ts +tests/engine/render/jsx/component/set-properties.test.ts +tests/engine/render/jsx/export.test.ts +tests/engine/render/jsx/gaps.test.ts +tests/engine/render/jsx/grid-layout.test.ts +tests/engine/render/jsx/instance-sizing.test.ts +tests/engine/render/jsx/reference.test.ts +tests/engine/render/jsx/render-tree.test.ts +tests/engine/render/jsx/scalar-variables.test.ts +tests/engine/render/jsx/schema.test.ts +tests/engine/render/jsx/text-props.test.ts +tests/engine/snap/basic.test.ts +tests/engine/tauri/acp-transport.test.ts +tests/engine/tauri/automation-files.test.ts +tests/engine/tauri/command.test.ts +tests/engine/tauri/document-io.test.ts +tests/engine/tauri/fig-export.test.ts +tests/engine/tauri/file-actions.test.ts +tests/engine/tauri/font-cache.test.ts +tests/engine/tauri/fonts.test.ts +tests/engine/tauri/harness-process.test.ts +tests/engine/tauri/http.test.ts +tests/engine/tauri/mcp-spawn.test.ts +tests/engine/tauri/mock-cleanup.test.ts +tests/engine/tauri/processes.test.ts +tests/engine/text/coverage.test.ts +tests/engine/text/derived-text/clipboard.test.ts +tests/engine/text/edit-undo.test.ts +tests/engine/text/editor.test.ts +tests/engine/text/fonts/coverage.test.ts +tests/engine/text/fonts/fallbacks.test.ts +tests/engine/text/fonts/lifecycle.test.ts +tests/engine/text/fonts/loading.test.ts +tests/engine/text/fonts/normalize-export.test.ts +tests/engine/text/fonts/status.test.ts +tests/engine/text/fonts/variations.test.ts +tests/engine/text/fonts/web-fonts.test.ts +tests/engine/text/opentype/outlines.test.ts +tests/engine/text/resolver.test.ts +tests/engine/text/rtl-auto-layout-input.test.ts +tests/engine/text/style-runs.test.ts +tests/engine/text/style-runs/mutation.test.ts +tests/engine/tools/ai-adapter.test.ts +tests/engine/tools/analyze/overlaps/bounds.test.ts +tests/engine/tools/analyze/overlaps/index.test.ts +tests/engine/tools/analyze/overlaps/page-scope.test.ts +tests/engine/tools/calc/expression.test.ts +tests/engine/tools/calc/tool.test.ts +tests/engine/tools/cli.test.ts +tests/engine/tools/create.test.ts +tests/engine/tools/describe.test.ts +tests/engine/tools/eval-wrap.test.ts +tests/engine/tools/export-image.test.ts +tests/engine/tools/input.test.ts +tests/engine/tools/library-components.test.ts +tests/engine/tools/modify.test.ts +tests/engine/tools/modify/image-fill.test.ts +tests/engine/tools/read.test.ts +tests/engine/tools/registry.test.ts +tests/engine/tools/stock-photo/apply.test.ts +tests/engine/tools/stock-photo/credentials.test.ts +tests/engine/tools/stock-photo/requests.test.ts +tests/engine/tools/structure.test.ts +tests/engine/tools/xpath/basic.test.ts +tests/engine/vector/blob/mutation.test.ts +tests/engine/vector/blob/roundtrip.test.ts +tests/engine/vector/bounds.test.ts +tests/engine/vector/centerline.test.ts +tests/engine/vector/fill-geometry-regen.test.ts +tests/engine/vector/normalize.test.ts +tests/engine/vector/path-rendering.test.ts +tests/engine/vector/validate.test.ts +tests/engine/vector/vectorize.test.ts +tests/engine/vue/canvas/color-space.test.ts +tests/engine/vue/canvas/render-loop.test.ts +tests/engine/vue/controls/appearance.test.ts +tests/engine/vue/controls/binding-mode-edit.test.ts +tests/engine/vue/controls/binding-modes.test.ts +tests/engine/vue/controls/color-binding-provider.test.ts +tests/engine/vue/controls/component-properties.test.ts +tests/engine/vue/controls/constraints.test.ts +tests/engine/vue/controls/node-preview.test.ts +tests/engine/vue/controls/number-expression.test.ts +tests/engine/vue/controls/property-preview.test.ts +tests/engine/vue/controls/shared-style.test.ts +tests/engine/vue/controls/stroke.test.ts +tests/engine/vue/controls/typography-depth.test.ts +tests/engine/vue/document-workspace.test.ts +tests/engine/vue/editor/commands/shortcut.test.ts +tests/engine/vue/editor/menu-model/canvas.test.ts +tests/engine/vue/editor/selected-nodes.test.ts +tests/engine/vue/fill/model.test.ts +tests/engine/vue/i18n/locale.test.ts +tests/engine/vue/input/auto-layout-hover.test.ts +tests/engine/vue/input/draw.test.ts +tests/engine/vue/input/drop-files.test.ts +tests/engine/vue/input/explicit-snap-targets.test.ts +tests/engine/vue/input/geometry.test.ts +tests/engine/vue/input/guides.test.ts +tests/engine/vue/input/move-threshold.test.ts +tests/engine/vue/input/node-edit-snap.test.ts +tests/engine/vue/input/resize/geometry.test.ts +tests/engine/vue/input/resize/path-text/basic.test.ts +tests/engine/vue/input/resize/path-text/reflow.test.ts +tests/engine/vue/input/resize/snapping.test.ts +tests/engine/vue/input/snap-guides.test.ts +tests/engine/vue/layer-tree/model.test.ts +tests/engine/vue/layout/hug-contents.test.ts +tests/engine/vue/lifecycle/retention.test.ts +tests/engine/vue/renderer.test.ts +tests/engine/vue/shared/wheel.test.ts +tests/engine/vue/testing/test-id.test.ts diff --git a/tools/unit-tests/package.json b/tools/unit-tests/package.json index 672a61158..c2091939b 100644 --- a/tools/unit-tests/package.json +++ b/tools/unit-tests/package.json @@ -4,5 +4,8 @@ "type": "module", "scripts": { "test": "bun test tests" + }, + "dependencies": { + "@open-pencil/package-artifacts": "workspace:*" } } diff --git a/tools/unit-tests/src/check-homes.ts b/tools/unit-tests/src/check-homes.ts new file mode 100644 index 000000000..65257dee2 --- /dev/null +++ b/tools/unit-tests/src/check-homes.ts @@ -0,0 +1,33 @@ +#!/usr/bin/env bun +/** + * Fails when a unit test is added under `tests/engine` instead of its owner's canonical home, + * or when the baseline lists a file that has moved. + * + * bun tools/unit-tests/src/check-homes.ts # check + * bun tools/unit-tests/src/check-homes.ts --write # rewrite the baseline from the current tree + */ +import { writeFile } from 'node:fs/promises' +import { resolve } from 'node:path' + +import { resolveWorkspaceRoot } from '@open-pencil/package-artifacts' + +import { ENGINE_BASELINE_PATH, checkTestHomes, formatBaseline, listEngineTests } from './homes' + +const repoRoot = await resolveWorkspaceRoot(process.cwd()) + +if (process.argv.includes('--write')) { + const files = await listEngineTests(repoRoot) + await writeFile(resolve(repoRoot, ENGINE_BASELINE_PATH), formatBaseline(files)) + console.log(`Wrote ${files.length} tests/engine files to ${ENGINE_BASELINE_PATH}.`) + process.exit(0) +} + +const result = await checkTestHomes({ repoRoot }) +if (result.errors.length > 0) { + console.error('Test placement check failed:') + for (const error of result.errors) console.error(`- ${error}`) + process.exit(1) +} +console.log( + `Test placement check passed; ${result.remaining} tests/engine files remain to migrate.` +) diff --git a/tools/unit-tests/src/homes.ts b/tools/unit-tests/src/homes.ts new file mode 100644 index 000000000..ebadde2d1 --- /dev/null +++ b/tools/unit-tests/src/homes.ts @@ -0,0 +1,85 @@ +import { existsSync, readFileSync } from 'node:fs' +import { resolve } from 'node:path' + +import { UNIT_TEST_GROUPS } from './shards' + +export const ENGINE_TESTS_ROOT = 'tests/engine' +export const ENGINE_BASELINE_PATH = 'tools/unit-tests/engine-baseline.txt' + +export interface TestHomesOptions { + repoRoot: string + baselinePath?: string + /** Defaults to the discovered `tests/engine` files; tests inject a listing. */ + engineTests?: readonly string[] +} + +export interface TestHomesResult { + errors: string[] + /** `tests/engine` files that the baseline still allows. */ + remaining: number +} + +/** Canonical home for a `tests/engine//...` file, from the shard map. */ +export function canonicalHomeFor(engineTestPath: string): string | undefined { + for (const paths of Object.values(UNIT_TEST_GROUPS)) { + const engineDirs: readonly string[] = paths.filter((path) => + path.startsWith(`${ENGINE_TESTS_ROOT}/`) + ) + if (!engineDirs.some((dir) => engineTestPath.startsWith(`${dir}/`))) continue + return paths.find((path) => !path.startsWith(`${ENGINE_TESTS_ROOT}/`)) + } + return undefined +} + +export function parseBaseline(text: string): string[] { + return text + .split('\n') + .map((line) => line.trim()) + .filter((line) => line.length > 0 && !line.startsWith('#')) +} + +export function formatBaseline(files: readonly string[]): string { + const header = [ + '# Unit tests still under tests/engine, the migration debt from', + "# packages/docs/development/testing.md. New tests go to the owner's canonical", + '# home; remove a line here when its file moves. Regenerate with', + '# `bun tools/unit-tests/src/check-homes.ts --write`.' + ] + return [...header, ...[...files].sort()].join('\n') + '\n' +} + +/** Every test file under `tests/engine`, discovered or not, so an unsharded file cannot hide. */ +export async function listEngineTests(repoRoot: string): Promise { + const files = await Array.fromAsync( + new Bun.Glob(`${ENGINE_TESTS_ROOT}/**/*.test.ts`).scan({ cwd: repoRoot }) + ) + return files.map((file) => file.split('\\').join('/')).sort() +} + +/** + * Every `tests/engine` test file must be in the reviewed baseline, and every baseline entry must + * still exist, so the debt only shrinks: a new test goes to its owner's canonical home from day one. + */ +export async function checkTestHomes(options: TestHomesOptions): Promise { + const baselinePath = resolve(options.repoRoot, options.baselinePath ?? ENGINE_BASELINE_PATH) + if (!existsSync(baselinePath)) { + return { errors: [`Missing engine test baseline at ${baselinePath}`], remaining: 0 } + } + const baseline = new Set(parseBaseline(readFileSync(baselinePath, 'utf8'))) + const engineTests = options.engineTests ?? (await listEngineTests(options.repoRoot)) + const present = new Set(engineTests) + const errors: string[] = [] + + for (const file of engineTests) { + if (baseline.has(file)) continue + const home = canonicalHomeFor(file) + errors.push( + `${file} is a new test under ${ENGINE_TESTS_ROOT}; put it under ${home ?? "the owner's canonical home"} instead` + ) + } + for (const file of baseline) { + if (!present.has(file)) errors.push(`${file} no longer exists; remove it from the baseline`) + } + + return { errors, remaining: [...baseline].filter((file) => present.has(file)).length } +} diff --git a/tools/unit-tests/tests/homes.test.ts b/tools/unit-tests/tests/homes.test.ts new file mode 100644 index 000000000..927446581 --- /dev/null +++ b/tools/unit-tests/tests/homes.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, test } from 'bun:test' +import { mkdir, mkdtemp, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' + +import { + canonicalHomeFor, + checkTestHomes, + formatBaseline, + listEngineTests, + parseBaseline +} from '../src/homes' + +const REPO_ROOT = fileURLToPath(new URL('../../..', import.meta.url)) + +async function repoWithBaseline(files: readonly string[]) { + const repoRoot = await mkdtemp(join(tmpdir(), 'open-pencil-test-homes-')) + await mkdir(join(repoRoot, 'tools/unit-tests'), { recursive: true }) + await writeFile(join(repoRoot, 'tools/unit-tests/engine-baseline.txt'), formatBaseline(files)) + return repoRoot +} + +describe('canonicalHomeFor', () => { + test("maps an engine directory to its owner's canonical home from the shard map", () => { + expect(canonicalHomeFor('tests/engine/scene-graph/x.test.ts')).toBe( + 'packages/scene-graph/tests' + ) + expect(canonicalHomeFor('tests/engine/editor/a/b.test.ts')).toBe('packages/core/tests') + expect(canonicalHomeFor('tests/engine/app/x.test.ts')).toBe('tests/app') + expect(canonicalHomeFor('tests/engine/unknown/x.test.ts')).toBeUndefined() + }) +}) + +describe('baseline format', () => { + test('round-trips sorted entries and ignores comments and blank lines', () => { + const text = formatBaseline(['tests/engine/b.test.ts', 'tests/engine/a.test.ts']) + expect(text.startsWith('#')).toBe(true) + expect(parseBaseline(text)).toEqual(['tests/engine/a.test.ts', 'tests/engine/b.test.ts']) + }) +}) + +describe('checkTestHomes', () => { + test('passes when the tree and the baseline agree', async () => { + const files = ['tests/engine/io/a.test.ts'] + const repoRoot = await repoWithBaseline(files) + + const result = await checkTestHomes({ repoRoot, engineTests: files }) + + expect(result).toEqual({ errors: [], remaining: 1 }) + }) + + test('rejects a new engine test and points at the canonical home', async () => { + const repoRoot = await repoWithBaseline([]) + + const result = await checkTestHomes({ + repoRoot, + engineTests: ['tests/engine/scene-graph/new.test.ts'] + }) + + expect(result.errors).toEqual([ + 'tests/engine/scene-graph/new.test.ts is a new test under tests/engine; put it under packages/scene-graph/tests instead' + ]) + }) + + test('rejects stale baseline entries so the debt only shrinks', async () => { + const repoRoot = await repoWithBaseline(['tests/engine/io/moved.test.ts']) + + const result = await checkTestHomes({ repoRoot, engineTests: [] }) + + expect(result.errors).toEqual([ + 'tests/engine/io/moved.test.ts no longer exists; remove it from the baseline' + ]) + }) + + test('fails without a baseline file', async () => { + const repoRoot = await mkdtemp(join(tmpdir(), 'open-pencil-test-homes-empty-')) + + expect((await checkTestHomes({ repoRoot })).errors).toHaveLength(1) + }) + + test('the committed baseline matches the repository', async () => { + const result = await checkTestHomes({ repoRoot: REPO_ROOT }) + + expect(result.errors).toEqual([]) + expect(result.remaining).toBe((await listEngineTests(REPO_ROOT)).length) + }) +})