Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions packages/mcp/src/operations/scene-operations.test.ts
Original file line number Diff line number Diff line change
@@ -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 })
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test leaves SQLite handle open

Low Severity

The afterEach hook removes the temporary database directory without closing the SqliteSceneStore instance. This can leave SQLite database files locked, causing fs.rm to fail (e.g., EBUSY on Windows) and potentially leaking file handles. Other tests correctly close the store before cleanup.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ef80fa3. Configure here.


// 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')
})
})
12 changes: 6 additions & 6 deletions packages/mcp/src/operations/scene-operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,17 +296,17 @@ class SceneOperationsFacade implements SceneOperations {
}

async appendSceneEvent(options: SceneEventAppendOptions): Promise<SceneEvent | null> {
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<SceneEvent[]> {
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 {
Expand Down