From ef80fa355c064a268ea3a297cff7e9ed4defb5d0 Mon Sep 17 00:00:00 2001 From: Venkatesh Kone Date: Sat, 11 Jul 2026 18:14:05 +0530 Subject: [PATCH] fix(mcp): keep store receiver bound in scene event operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit appendSceneEvent and listSceneEvents detached the store methods before invoking them (const append = this.requireStore().appendSceneEvent; append(options)), so `this` was undefined inside store implementations that rely on it — SqliteSceneStore.withWriteTransaction in particular. Every save_scene, create_from_template, and live-sync mutation routed through the facade failed with "undefined is not an object (evaluating 'this.withWriteTransaction')" even though the SQLite write succeeded. Invoke the methods on the store instance instead, and add facade-level regression tests covering both the bound-receiver path and the stores-without-scene-events fallback. Co-Authored-By: Claude Fable 5 --- .../src/operations/scene-operations.test.ts | 78 +++++++++++++++++++ .../mcp/src/operations/scene-operations.ts | 12 +-- 2 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 packages/mcp/src/operations/scene-operations.test.ts diff --git a/packages/mcp/src/operations/scene-operations.test.ts b/packages/mcp/src/operations/scene-operations.test.ts new file mode 100644 index 000000000..634f694ef --- /dev/null +++ b/packages/mcp/src/operations/scene-operations.test.ts @@ -0,0 +1,78 @@ +import { afterEach, beforeEach, describe, expect, test } from 'bun:test' +import * as fs from 'node:fs/promises' +import * as os from 'node:os' +import * as path from 'node:path' +import type { SceneGraph } from '@pascal-app/core/clone-scene-graph' +import { SqliteSceneStore } from '../storage/sqlite-scene-store' +import { createSceneOperations } from './scene-operations' + +function makeGraph(): SceneGraph { + return { + nodes: { + site_abc: { + object: 'node', + id: 'site_abc', + type: 'site', + parentId: null, + visible: true, + metadata: {}, + }, + } as SceneGraph['nodes'], + rootNodeIds: ['site_abc'] as SceneGraph['rootNodeIds'], + } +} + +describe('SceneOperationsFacade scene events', () => { + let rootDir: string + let store: SqliteSceneStore + + beforeEach(async () => { + rootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'pascal-scene-ops-test-')) + store = new SqliteSceneStore({ databasePath: path.join(rootDir, 'pascal.db') }) + }) + + afterEach(async () => { + await fs.rm(rootDir, { recursive: true, force: true }) + }) + + // Regression: appendSceneEvent/listSceneEvents were detached from the store + // before invocation, so `this` was undefined inside store methods that rely + // on it (e.g. SqliteSceneStore.withWriteTransaction). + test('appendSceneEvent and listSceneEvents preserve the store receiver', async () => { + const operations = createSceneOperations({ store }) + const graph = makeGraph() + const meta = await store.save({ id: 'live', name: 'Live', graph }) + + const appended = await operations.appendSceneEvent({ + sceneId: meta.id, + version: meta.version, + kind: 'save_scene', + graph, + }) + expect(appended?.sceneId).toBe(meta.id) + + const events = await operations.listSceneEvents(meta.id) + expect(events.map((event) => event.kind)).toEqual(['save_scene']) + }) + + test('appendSceneEvent returns null and listSceneEvents throws when the store lacks scene events', async () => { + const operations = createSceneOperations({ + store: { + ...store, + backend: 'sqlite', + appendSceneEvent: undefined, + listSceneEvents: undefined, + } as never, + }) + + expect( + await operations.appendSceneEvent({ + sceneId: 'live', + version: 1, + kind: 'save_scene', + graph: makeGraph(), + }), + ).toBeNull() + await expect(operations.listSceneEvents('live')).rejects.toThrow('scene_events_unavailable') + }) +}) diff --git a/packages/mcp/src/operations/scene-operations.ts b/packages/mcp/src/operations/scene-operations.ts index fcf3ba3ba..5b74f0b68 100644 --- a/packages/mcp/src/operations/scene-operations.ts +++ b/packages/mcp/src/operations/scene-operations.ts @@ -296,17 +296,17 @@ class SceneOperationsFacade implements SceneOperations { } async appendSceneEvent(options: SceneEventAppendOptions): Promise { - const append = this.requireStore().appendSceneEvent - if (!append) return null - return append(options) + const store = this.requireStore() + if (!store.appendSceneEvent) return null + return store.appendSceneEvent(options) } async listSceneEvents(id: string, options?: SceneEventListOptions): Promise { - const list = this.requireStore().listSceneEvents - if (!list) { + const store = this.requireStore() + if (!store.listSceneEvents) { throw new Error('scene_events_unavailable') } - return list(id, options) + return store.listSceneEvents(id, options) } private requireBridge(): SceneBridge {