Files
editor/packages/mcp/test-reports/villa-azul/v6-page.ts
T
Adrian PerezandClaude Opus 4.7 f230d9a401 test(mcp): Villa Azul + 10-agent deep verification
Builds a larger, richer house than Casa del Sol via MCP save_scene
(no injection hack), then dispatches 10 parallel verifiers across
schema, geometry, dimensions, openings, HTTP, page render,
parentage, round-trip, spatial, visual.

Villa Azul — 56 nodes, validate_scene=true, 44KB on disk:
- 15x10m building envelope (vs Casa del Sol's 12x8)
- 9 interior zones (master bed/bath, bed 2/3, shared bath,
  living/dining, kitchen, entry hall, corridor)
- 10 doors + 12 windows (all cut successfully)
- 4 exterior zones (pool 8x4 + basin slab at -2m, outdoor kitchen,
  driveway, back patio)
- 5 rail-style fences (vs Casa del Sol's privacy) with 2m entrance gap

Verification: 108 checks, 104 PASS, 4 findings:
- V1 schema: 56/56
- V2 geometry: 7/7 (perimeter closes, interior T-junctions, no
  zone overlaps, fence gap verified)
- V3 dimensions: 13/13 zone areas exact (1 spec mismatch on site
  polygon default, not a build bug)
- V4 openings: 22/22 dimensional fit, surfaced a tool gap in
  cut_opening (no adjacency check) + my build packed too tightly
- V5 HTTP: 10/10 (GET/PUT/PATCH/DELETE/HEAD, If-Match conflicts)
- V6 page: 14/14 (/scene/:id 81KB, /scenes 20KB, 404 fallback)
- V7 parentage: surfaced CROSS_CUTTING §2 site->building->level
  parentId=null (pre-existing in core's loadScene)
- V8 round-trip: 10/10 byte-equal, duplicate_level -> 110 nodes
- V9 spatial: 12/12 (find_nodes, measure, constraints resource)
- V10 visual: HTML fallback (Chrome extension disconnected during
  run); API layer intact

Follow-up tracked: `cut_opening` should check opening-adjacency on
the same wall (minimum gap) to catch tight packing during patch
construction. Currently returns success and relies on the UI to
visualise the overlap.

Live at http://localhost:3002/scene/a6e7919eacbe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 20:39:08 +02:00

228 lines
7.2 KiB
TypeScript

/**
* Villa Azul — Phase 9 Verifier V6: Next.js page render checks.
* Usage:
* bun run packages/mcp/test-reports/villa-azul/v6-page.ts
* Assumes editor is running at http://localhost:3002.
*/
const BASE = 'http://localhost:3002'
const SCENE_ID = 'a6e7919eacbe'
const REPORT_PATH = 'packages/mcp/test-reports/villa-azul/v6-page.md'
type FetchResult = {
url: string
status: number
bytes: number
elapsedMs: number
text: string
}
async function fetchPage(path: string): Promise<FetchResult> {
const url = `${BASE}${path}`
const start = performance.now()
const res = await fetch(url)
const text = await res.text()
const elapsedMs = performance.now() - start
return {
url,
status: res.status,
bytes: new TextEncoder().encode(text).length,
elapsedMs,
text,
}
}
function contains(text: string, needle: string): boolean {
return text.includes(needle)
}
function countMatches(text: string, re: RegExp): number {
return (text.match(re) ?? []).length
}
type Check = { label: string; pass: boolean; detail: string }
const checks: Check[] = []
const ERROR_STRINGS = ['Application error', 'Hydration', 'Failed to']
// === 1. /scene/a6e7919eacbe ===
console.log(`---- V6 page verifier ----`)
const scene = await fetchPage(`/scene/${SCENE_ID}`)
console.log(
`01 /scene/${SCENE_ID} status=${scene.status} bytes=${scene.bytes} time=${scene.elapsedMs.toFixed(1)}ms`,
)
checks.push({
label: 'scene: 200 status',
pass: scene.status === 200,
detail: `got ${scene.status}`,
})
checks.push({
label: 'scene: HTML >= 10 KB',
pass: scene.bytes >= 10_000,
detail: `${scene.bytes} bytes`,
})
checks.push({
label: "scene: contains 'SceneLoader'",
pass: contains(scene.text, 'SceneLoader'),
detail: '',
})
checks.push({
label: `scene: contains sceneId '${SCENE_ID}'`,
pass: contains(scene.text, SCENE_ID),
detail: '',
})
checks.push({
label: "scene: contains 'Villa Azul'",
pass: contains(scene.text, 'Villa Azul'),
detail: '',
})
const hasEditorChunk =
contains(scene.text, '@pascal-app/editor') ||
contains(scene.text, '/packages/editor/') ||
contains(scene.text, 'packages_editor')
const hasViewerChunk =
contains(scene.text, '@pascal-app/viewer') ||
contains(scene.text, '/packages/viewer/') ||
contains(scene.text, 'packages_viewer')
checks.push({
label: 'scene: references editor or viewer chunks',
pass: hasEditorChunk || hasViewerChunk,
detail: `editor=${hasEditorChunk} viewer=${hasViewerChunk}`,
})
const errorsFound = ERROR_STRINGS.filter((s) => contains(scene.text, s))
checks.push({
label: 'scene: no obvious error strings',
pass: errorsFound.length === 0,
detail: errorsFound.length ? `found ${errorsFound.join(', ')}` : 'clean',
})
// === 2. /scene/nope ===
const nope = await fetchPage(`/scene/nope`)
console.log(
`02 /scene/nope status=${nope.status} bytes=${nope.bytes} time=${nope.elapsedMs.toFixed(1)}ms`,
)
const isNotFoundResponse = nope.status === 404 || contains(nope.text, 'Scene not found')
checks.push({
label: 'nope: 404 or Scene-not-found page',
pass: isNotFoundResponse,
detail: `status=${nope.status} hasFallback=${contains(nope.text, 'Scene not found')}`,
})
// If status 200, it should be a fallback page that has no SceneLoader initialized
// If status 404, content should also NOT contain SceneLoader
const nopeHasSceneLoader = contains(nope.text, 'SceneLoader')
checks.push({
label: 'nope: does NOT initialize SceneLoader',
pass: !nopeHasSceneLoader,
detail: nopeHasSceneLoader ? 'found SceneLoader' : 'not found',
})
checks.push({
label: 'nope: does NOT contain Villa Azul',
pass: !contains(nope.text, 'Villa Azul'),
detail: '',
})
// === 3. /scenes ===
const scenes = await fetchPage(`/scenes`)
console.log(
`03 /scenes status=${scenes.status} bytes=${scenes.bytes} time=${scenes.elapsedMs.toFixed(1)}ms`,
)
checks.push({
label: 'scenes: 200 status',
pass: scenes.status === 200,
detail: `got ${scenes.status}`,
})
checks.push({
label: `scenes: contains link /scene/${SCENE_ID}`,
pass: contains(scenes.text, `/scene/${SCENE_ID}`),
detail: '',
})
checks.push({
label: "scenes: contains 'Villa Azul'",
pass: contains(scenes.text, 'Villa Azul'),
detail: '',
})
// === 4. /scenes link count ===
const linkRe = /<a\s[^>]*href="\/scene\/[^"]+"/g
const linkCount = countMatches(scenes.text, linkRe)
console.log(`04 /scene/... links on /scenes = ${linkCount}`)
checks.push({
label: 'scenes: >=1 <a href="/scene/..."> link',
pass: linkCount >= 1,
detail: `count=${linkCount}`,
})
// === Report ===
const pass = checks.filter((c) => c.pass).length
const fail = checks.filter((c) => !c.pass).length
console.log(`\n=== V6 SUMMARY ===`)
console.log(`pass=${pass} fail=${fail}`)
for (const c of checks) {
console.log(` ${c.pass ? 'OK' : 'FAIL'} ${c.label} ${c.detail}`)
}
// === Build markdown report ===
const lines: string[] = []
lines.push('# V6 — Next.js Page Render Verification')
lines.push('')
lines.push(`- Scene ID: \`${SCENE_ID}\``)
lines.push(`- Base URL: \`${BASE}\``)
lines.push(`- Generated: ${new Date().toISOString()}`)
lines.push(`- Overall: ${fail === 0 ? 'PASS' : 'FAIL'} (${pass}/${checks.length})`)
lines.push('')
lines.push('## Request summary')
lines.push('')
lines.push('| Path | Status | Bytes | Time (ms) |')
lines.push('|---|---|---|---|')
for (const r of [scene, nope, scenes]) {
const path = r.url.replace(BASE, '')
lines.push(`| \`${path}\` | ${r.status} | ${r.bytes} | ${r.elapsedMs.toFixed(1)} |`)
}
lines.push('')
lines.push('## Checks')
lines.push('')
lines.push('| Result | Check | Detail |')
lines.push('|---|---|---|')
for (const c of checks) {
lines.push(`| ${c.pass ? 'PASS' : 'FAIL'} | ${c.label} | ${c.detail} |`)
}
lines.push('')
lines.push('## Strings-found snapshot')
lines.push('')
lines.push(`### \`/scene/${SCENE_ID}\``)
lines.push('')
lines.push(`- SceneLoader present: ${contains(scene.text, 'SceneLoader')}`)
lines.push(`- sceneId '${SCENE_ID}' present: ${contains(scene.text, SCENE_ID)}`)
lines.push(`- 'Villa Azul' present: ${contains(scene.text, 'Villa Azul')}`)
lines.push(`- editor chunk reference: ${hasEditorChunk}`)
lines.push(`- viewer chunk reference: ${hasViewerChunk}`)
lines.push(`- error strings found: ${errorsFound.length ? errorsFound.join(', ') : 'none'}`)
lines.push('')
lines.push('### `/scene/nope`')
lines.push('')
lines.push(`- status: ${nope.status}`)
lines.push(`- 'Scene not found' fallback: ${contains(nope.text, 'Scene not found')}`)
lines.push(`- SceneLoader NOT present: ${!nopeHasSceneLoader}`)
lines.push(`- 'Villa Azul' NOT present: ${!contains(nope.text, 'Villa Azul')}`)
lines.push('')
lines.push('### `/scenes`')
lines.push('')
lines.push(`- '/scene/${SCENE_ID}' link present: ${contains(scenes.text, `/scene/${SCENE_ID}`)}`)
lines.push(`- 'Villa Azul' present: ${contains(scenes.text, 'Villa Azul')}`)
lines.push(`- <a href="/scene/..."> link count: ${linkCount}`)
lines.push('')
lines.push('## Response times')
lines.push('')
lines.push(`- /scene/${SCENE_ID}: ${scene.elapsedMs.toFixed(1)} ms`)
lines.push(`- /scene/nope: ${nope.elapsedMs.toFixed(1)} ms`)
lines.push(`- /scenes: ${scenes.elapsedMs.toFixed(1)} ms`)
lines.push('')
await Bun.write(REPORT_PATH, lines.join('\n'))
console.log(`\nwrote ${REPORT_PATH}`)
if (fail > 0) process.exit(1)