Add ESLint and Prettier, and close the node test gaps

Tooling:
- Prettier as the single formatting authority (110 columns, single quotes,
  measured against the existing style); Markdown excluded because table-cell
  padding turns the README glossary into 1000-character lines.
- Type-aware ESLint ending in eslint-config-prettier, so a rule always means
  "this is a bug", never "this looks different".
- .editorconfig and an `npm run check` that runs typecheck, lint, format and
  tests in one go.

Findings fixed on the first run: three more duplicate imports, four
String(unknown) calls (parameter types narrowed instead of casts), two dead
initialisers, two redundant casts, a console.log for a real error. Two
warnings were false here and are disabled with a reason in place.

Tests 132 -> 269:
- node-catalog.test.ts covers every node with a run(), with a guard that fails
  when a new node has no row.
- behavior-events.test.ts covers the ten event nodes through a running World,
  checking timing rather than existence.
- Input's state machine (press/release/releaseAll) is reachable without DOM
  events now; the DOM handlers are thin adapters.

Compare fell back to == without a configured operator although the schema
default is <; aligned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-08 10:02:22 +02:00
1 parent f140c2fd77
commit d54a620ebd
113 files changed
+5049 -666

No files matched your search

+53 -11
View File
@@ -1,12 +1,30 @@
import { describe, expect, it } from 'vitest';
import { CommandError } from '../../src/core/commands/Command';
import { AddNodeCommand, DisconnectCommand, RemoveNodeCommand, SetNodeParamCommand } from '../../src/core/commands/graphCommands';
import { AddVariableCommand, RemoveVariableCommand, RenameVariableCommand } from '../../src/core/commands/variableCommands';
import {
AddNodeCommand,
DisconnectCommand,
RemoveNodeCommand,
SetNodeParamCommand,
} from '../../src/core/commands/graphCommands';
import {
AddVariableCommand,
RemoveVariableCommand,
RenameVariableCommand,
} from '../../src/core/commands/variableCommands';
import { GraphInterpreter, allNodeDefs, findNode, portsCompatible } from '../../src/core/graph';
import type { BehaviorAsset } from '../../src/core/model/Asset';
import { findAsset } from '../../src/core/model/Project';
import { deserializeProject, serializeProject } from '../../src/core/store/serialize';
import { type ProjectFixture, fakeContext, graphEditor, makeBehavior, makeProject, makeSpriteScene, place, roundTrip } from '../helpers';
import {
type ProjectFixture,
fakeContext,
graphEditor,
makeBehavior,
makeProject,
makeSpriteScene,
place,
roundTrip,
} from '../helpers';
/** Adds "On key held(ArrowRight) → Move(owner, dx = speed × dt)" to a project, built through commands like the editor would. */
function withMover<F extends ProjectFixture>(fixture: F) {
@@ -75,7 +93,10 @@ describe('GraphInterpreter', () => {
const start = edit.addNode('OnStart');
const self = edit.addNode('Self');
const move = edit.addNode('Move', { dx: 7, dy: -3 });
edit.wire([[start, 'exec', move, 'exec'], [self, 'object', move, 'object']]);
edit.wire([
[start, 'exec', move, 'exec'],
[self, 'object', move, 'object'],
]);
const ctx = fakeContext();
new GraphInterpreter(behavior.graph).fire('OnStart', ctx);
expect(ctx.self.x).toBe(7);
@@ -88,7 +109,11 @@ describe('GraphInterpreter', () => {
const self = edit.addNode('Self');
const clamp = edit.addNode('Clamp', { value: 5 });
const move = edit.addNode('Move', { dx: 4 });
edit.wire([[start, 'exec', move, 'exec'], [self, 'object', move, 'object'], [clamp, 'result', move, 'dy']]);
edit.wire([
[start, 'exec', move, 'exec'],
[self, 'object', move, 'object'],
[clamp, 'result', move, 'dy'],
]);
const ctx = fakeContext();
new GraphInterpreter(behavior.graph).fire('OnStart', ctx);
expect(ctx.self.x).toBe(4); // scale defaulted to 1, not 0
@@ -145,7 +170,10 @@ describe('GraphInterpreter', () => {
let previous = start;
sources.forEach((source, i) => {
const log = edit.addNode('Log');
edit.wire([[previous, 'exec', log, 'exec'], [source, ports[i]!, log, 'message']]);
edit.wire([
[previous, 'exec', log, 'exec'],
[source, ports[i]!, log, 'message'],
]);
previous = log;
});
const ctx = fakeContext();
@@ -198,14 +226,21 @@ describe('GraphInterpreter', () => {
const start = edit.addNode('OnStart');
const a = edit.addNode('Sequence');
const b = edit.addNode('Sequence');
edit.wire([[start, 'exec', a, 'exec'], [a, 'a', b, 'exec'], [b, 'a', a, 'exec']]);
edit.wire([
[start, 'exec', a, 'exec'],
[a, 'a', b, 'exec'],
[b, 'a', a, 'exec'],
]);
expect(() => new GraphInterpreter(behavior.graph).fire('OnStart', fakeContext())).toThrow(/step limit/);
});
});
describe('graph commands', () => {
it('rejects incompatible, self and duplicate connections', () => {
const { edit, nodes: { held, mul, move } } = withMover(makeProject());
const {
edit,
nodes: { held, mul, move },
} = withMover(makeProject());
expect(() => edit.connect(held, 'exec', mul, 'a')).toThrow(/Cannot connect/);
expect(() => edit.connect(mul, 'result', mul, 'a')).toThrow(/itself/);
expect(() => edit.connect(held, 'exec', move, 'exec')).toThrow(/Already/);
@@ -218,7 +253,8 @@ describe('graph commands', () => {
const before = behavior.graph.edges.length;
edit.connect(literal, 'value', nodes.move, 'dx');
expect(behavior.graph.edges.length).toBe(before); // one replaced, one added
const sourceOfDx = () => behavior.graph.edges.find((e) => e.to.node === nodes.move.id && e.to.port === 'dx')!.from.node;
const sourceOfDx = () =>
behavior.graph.edges.find((e) => e.to.node === nodes.move.id && e.to.port === 'dx')!.from.node;
expect(sourceOfDx()).toBe(literal.id);
stack.undo();
expect(sourceOfDx()).toBe(nodes.mul.id);
@@ -288,10 +324,16 @@ describe('behavior serialization', () => {
expect(loaded.graph).toEqual(behavior.graph);
expect(loaded.props).toEqual({ enabled: true, speed: 0 });
const raw = serializeProject(project) as { assets: { id: string; graph?: { nodes: unknown[]; edges: unknown[] } }[] };
const raw = serializeProject(project) as {
assets: { id: string; graph?: { nodes: unknown[]; edges: unknown[] } }[];
};
const rawGraph = raw.assets.find((a) => a.id === behavior.id)!.graph!;
rawGraph.nodes.push({ id: 'zzz', type: 'DoesNotExist', x: 0, y: 0, params: {} });
rawGraph.edges.push({ id: 'e-bad', from: { node: 'zzz', port: 'exec' }, to: { node: behavior.graph.nodes[0]!.id, port: 'exec' } });
rawGraph.edges.push({
id: 'e-bad',
from: { node: 'zzz', port: 'exec' },
to: { node: behavior.graph.nodes[0]!.id, port: 'exec' },
});
const cleaned = findAsset(deserializeProject(raw), behavior.id) as BehaviorAsset;
expect(cleaned.graph.nodes.length).toBe(behavior.graph.nodes.length);
expect(cleaned.graph.edges.length).toBe(behavior.graph.edges.length);