A behavior has rows of its own, so loading no longer drops Run in editor

`deserializeProject` builds each asset's effective schema in pass 1 and coerces
`props` against it in pass 2. For a behavior that schema was
`[...COMMON_PROPS, ...variables]` -- but `runInEditor` lives in
`ASSET_SCHEMAS.behavior`, not in `COMMON_PROPS`, so `normalizeBag` dropped the
value as undeclared. The flag therefore survived no save: every reload switched
it back off.

The fix is the full schema, `schemaFor('behavior')`, which is what
`schemaForAsset` hands out everywhere else.

Found by a round-trip test comparing the whole project rather than one field. An
existing test had the loss written down as its expectation
(`{ enabled, speed }`), which is why nothing ever reported it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-14 16:13:40 +02:00
1 parent 68c92daaa6
commit 9027a86edd
3 files changed
+15 -4

No files matched your search

+3 -2
View File
@@ -40,7 +40,6 @@ import {
import { type FlatItem, makeFlatItem } from '../properties/listItems';
import {
coerceValue,
COMMON_PROPS,
isValidFieldName,
isValidVariableName,
type PropertyDef,
@@ -76,7 +75,9 @@ export function deserializeProject(raw: unknown, fallbackId = ''): Project {
if (a.type === 'behavior') {
const vars = normalizeVariables(a.variables);
variables.set(a.id, vars);
schemas.set(a.id, [...COMMON_PROPS, ...vars]);
// The full schema, not just the common rows: this is what pass 2 coerces `props` against, and
// a behavior's own `runInEditor` would be dropped as undeclared.
schemas.set(a.id, [...schemaFor('behavior'), ...vars]);
} else if (a.type === 'particles') {
// A source's rows end with its own graph's parameters, so the graph is needed before the
// overrides of the objects placed from it can be coerced.
+1 -1
View File
@@ -475,7 +475,7 @@ describe('behavior serialization', () => {
const loaded = findAsset(roundTrip(project), behavior.id) as BehaviorAsset;
expect(loaded.variables).toEqual(behavior.variables);
expect(loaded.graph).toEqual(behavior.graph);
expect(loaded.props).toEqual({ enabled: true, speed: 0 });
expect(loaded.props).toEqual({ enabled: true, runInEditor: false, speed: 0 });
const raw = serializeProject(project) as {
assets: { id: string; graph?: { nodes: unknown[]; edges: unknown[] } }[];
+11 -1
View File
@@ -1,7 +1,8 @@
import { describe, expect, it } from 'vitest';
import { SetAssetPropertyCommand } from '../../src/core/commands/assetCommands';
import { SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands';
import { deserializeProject } from '../../src/core/store/serialize';
import { makeSpriteScene, place, roundTrip } from '../helpers';
import { makeBehavior, makeSpriteScene, place, roundTrip } from '../helpers';
describe('serialize', () => {
it('round-trips a project', () => {
@@ -11,6 +12,15 @@ describe('serialize', () => {
expect(roundTrip(f.project)).toEqual(f.project);
});
/** A behavior's own rows are not the common ones: coercing against those alone dropped the value. */
it('keeps a behavior running in the editor across a save', () => {
const f = makeSpriteScene();
const behavior = makeBehavior(f, 'B');
f.stack.execute(new SetAssetPropertyCommand(behavior.id, 'runInEditor', true));
const loaded = roundTrip(f.project).assets.find((a) => a.id === behavior.id)!;
expect(loaded.props.runInEditor).toBe(true);
});
it('repairs broken documents', () => {
const project = deserializeProject(
{