150 lines
4.8 KiB
TypeScript
150 lines
4.8 KiB
TypeScript
import { beforeEach, describe, expect, test } from 'bun:test'
|
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
|
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'
|
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
import type { SceneGraph } from '@pascal-app/core/clone-scene-graph'
|
|
import { SceneBridge } from '../bridge/scene-bridge'
|
|
import { createSceneOperations } from '../operations'
|
|
import type { SceneMeta, SceneStore } from '../storage/types'
|
|
import { registerCreateWall } from './create-wall'
|
|
|
|
describe('create_wall', () => {
|
|
let client: Client
|
|
let bridge: SceneBridge
|
|
|
|
beforeEach(async () => {
|
|
bridge = new SceneBridge()
|
|
bridge.setScene({}, [])
|
|
bridge.loadDefault()
|
|
const server = new McpServer({ name: 'test', version: '0.0.0' })
|
|
registerCreateWall(server, bridge)
|
|
const [srvT, cliT] = InMemoryTransport.createLinkedPair()
|
|
client = new Client({ name: 'test-client', version: '0.0.0' })
|
|
await Promise.all([server.connect(srvT), client.connect(cliT)])
|
|
})
|
|
|
|
test('creates a wall with custom thickness', async () => {
|
|
const level = Object.values(bridge.getNodes()).find((n) => n.type === 'level')!
|
|
const result = await client.callTool({
|
|
name: 'create_wall',
|
|
arguments: {
|
|
levelId: level.id,
|
|
start: [0, 0],
|
|
end: [4, 0],
|
|
thickness: 0.15,
|
|
},
|
|
})
|
|
expect(result.isError).toBeFalsy()
|
|
const parsed = JSON.parse((result.content as Array<{ type: string; text: string }>)[0]!.text)
|
|
expect(parsed.wallId).toMatch(/^wall_/)
|
|
const created = bridge.getNode(parsed.wallId)
|
|
expect(created).not.toBeNull()
|
|
expect((created as { thickness?: number }).thickness).toBe(0.15)
|
|
})
|
|
|
|
test('publishes a live scene snapshot when bound to a saved scene', async () => {
|
|
const now = new Date().toISOString()
|
|
const savedMeta: SceneMeta = {
|
|
id: 'live-scene',
|
|
name: 'Live Scene',
|
|
projectId: null,
|
|
thumbnailUrl: null,
|
|
version: 1,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
ownerId: null,
|
|
sizeBytes: 0,
|
|
nodeCount: Object.keys(bridge.getNodes()).length,
|
|
}
|
|
const savedGraphs: SceneGraph[] = []
|
|
const eventKinds: string[] = []
|
|
const store: SceneStore = {
|
|
backend: 'sqlite',
|
|
async save(opts) {
|
|
expect(opts.id).toBe(savedMeta.id)
|
|
expect(opts.expectedVersion).toBe(1)
|
|
savedGraphs.push(opts.graph)
|
|
return {
|
|
...savedMeta,
|
|
version: 2,
|
|
updatedAt: new Date().toISOString(),
|
|
sizeBytes: JSON.stringify(opts.graph).length,
|
|
nodeCount: Object.keys(opts.graph.nodes).length,
|
|
}
|
|
},
|
|
async load() {
|
|
return null
|
|
},
|
|
async list() {
|
|
return []
|
|
},
|
|
async delete() {
|
|
return false
|
|
},
|
|
async rename() {
|
|
return savedMeta
|
|
},
|
|
async appendSceneEvent(opts) {
|
|
eventKinds.push(opts.kind)
|
|
return {
|
|
eventId: 1,
|
|
sceneId: opts.sceneId,
|
|
version: opts.version,
|
|
kind: opts.kind,
|
|
createdAt: new Date().toISOString(),
|
|
graph: opts.graph,
|
|
}
|
|
},
|
|
}
|
|
const liveServer = new McpServer({ name: 'test-live', version: '0.0.0' })
|
|
const liveClient = new Client({ name: 'test-live-client', version: '0.0.0' })
|
|
const operations = createSceneOperations({ bridge, store })
|
|
operations.setActiveScene(savedMeta)
|
|
registerCreateWall(liveServer, operations)
|
|
const [srvT, cliT] = InMemoryTransport.createLinkedPair()
|
|
await Promise.all([liveServer.connect(srvT), liveClient.connect(cliT)])
|
|
|
|
const level = Object.values(bridge.getNodes()).find((n) => n.type === 'level')!
|
|
const result = await liveClient.callTool({
|
|
name: 'create_wall',
|
|
arguments: {
|
|
levelId: level.id,
|
|
start: [0, 1],
|
|
end: [4, 1],
|
|
},
|
|
})
|
|
|
|
expect(result.isError).toBeFalsy()
|
|
const parsed = JSON.parse((result.content as Array<{ type: string; text: string }>)[0]!.text)
|
|
expect(savedGraphs).toHaveLength(1)
|
|
expect(savedGraphs[0]!.nodes[parsed.wallId]).toBeDefined()
|
|
expect(eventKinds).toEqual(['create_wall'])
|
|
expect(bridge.getActiveScene()?.version).toBe(2)
|
|
})
|
|
|
|
test('rejects unknown level id', async () => {
|
|
const result = await client.callTool({
|
|
name: 'create_wall',
|
|
arguments: {
|
|
levelId: 'level_nope',
|
|
start: [0, 0],
|
|
end: [1, 0],
|
|
},
|
|
})
|
|
expect(result.isError).toBe(true)
|
|
})
|
|
|
|
test('rejects invalid start tuple', async () => {
|
|
const level = Object.values(bridge.getNodes()).find((n) => n.type === 'level')!
|
|
const result = await client.callTool({
|
|
name: 'create_wall',
|
|
arguments: {
|
|
levelId: level.id,
|
|
start: [0],
|
|
end: [1, 0],
|
|
},
|
|
})
|
|
expect(result.isError).toBe(true)
|
|
})
|
|
})
|