From 62795a97b92dd97d70b5bf82cf3b18126bd6130d Mon Sep 17 00:00:00 2001 From: que01 Date: Fri, 28 Aug 2026 09:37:27 +0800 Subject: [PATCH] fix: install the recompile hook on UI import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `/api/import` set `session.area` but never `session.context.compileFresh`, which is only wired when the server starts with an area on the command line. Every session imported through the browser therefore answered "请先导入 OSM 文件" to `/api/compile`, even though the import had just succeeded. This predates direct editing — it broke "保存并重新生成" for UI-imported workspaces — but it surfaced now, because saving a direct edit recompiles and so reported a failed regeneration after a save that had in fact succeeded. Covered by a new HTTP-level test: import the fixture, then recompile twice. The bug lived in the wiring between two handlers rather than in either one, so nothing below the HTTP boundary could catch it. Verified by reverting the fix and watching the test fail. Co-Authored-By: Claude Opus 5 (1M context) --- package.json | 2 +- test/workbench-import-compile.js | 94 ++++++++++++++++++++++++++++++++ workbench/server.js | 10 ++++ 3 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 test/workbench-import-compile.js diff --git a/package.json b/package.json index 3cf4e6c..378022e 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "road-compiler": "bin/road-compiler.js" }, "scripts": { - "test": "node test/index.js && node test/native-road-edits.js && node test/road-revisions.js && node test/direct-edit-solver.js && node test/workbench-edit-api.js && node test/fixtures.js", + "test": "node test/index.js && node test/native-road-edits.js && node test/road-revisions.js && node test/direct-edit-solver.js && node test/workbench-edit-api.js && node test/workbench-import-compile.js && node test/fixtures.js", "test:fixtures:update-baseline": "node test/update-fixture-baselines.js", "road:workbench": "node bin/road-workbench.js", "road:export": "node bin/road-compiler.js", diff --git a/test/workbench-import-compile.js b/test/workbench-import-compile.js new file mode 100644 index 0000000..fc0754d --- /dev/null +++ b/test/workbench-import-compile.js @@ -0,0 +1,94 @@ +'use strict'; + +// Importing through the UI and then recompiling — the path a browser actually +// takes. It is exercised over HTTP because the bug it guards against lived in the +// wiring between the two handlers, not in either one: `/api/import` set +// `session.area` but never installed `session.context.compileFresh`, which is only +// wired when the server is started with an area on the command line. So every +// UI-imported session answered "请先导入 OSM 文件" to `/api/compile` — breaking +// "保存并重新生成", and later making a successful direct-edit save report a failed +// regeneration. + +const assert = require('assert/strict'); +const fs = require('fs'); +const http = require('http'); +const os = require('os'); +const path = require('path'); +const { startWorkbench } = require('../workbench/server'); + +const PORT = 8899; +const dataRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'workbench-import-compile-')); +const osm = fs.readFileSync(path.join(__dirname, 'fixtures', 'fengshu-er-road.osm')); + +function request(options, body) { + return new Promise((resolve, reject) => { + const call = http.request({ host: '127.0.0.1', port: PORT, ...options }, (response) => { + const chunks = []; + response.on('data', (chunk) => chunks.push(chunk)); + response.on('end', () => + resolve({ status: response.statusCode, body: JSON.parse(Buffer.concat(chunks).toString('utf8')) }), + ); + }); + call.on('error', reject); + if (body) call.write(body); + call.end(); + }); +} + +function multipart(field, filename, contents) { + const boundary = '----roadcompilertest'; + return { + boundary, + body: Buffer.concat([ + Buffer.from( + `--${boundary}\r\nContent-Disposition: form-data; name="${field}"; filename="${filename}"\r\n` + + 'Content-Type: application/xml\r\n\r\n', + ), + contents, + Buffer.from(`\r\n--${boundary}--\r\n`), + ]), + }; +} + +const server = startWorkbench({ port: PORT, dataRoot, repoRoot: process.cwd() }); + +async function run() { + const upload = multipart('file', 'fengshu-er-road.osm', osm); + const imported = await request( + { + method: 'POST', + path: '/api/import', + headers: { + 'Content-Type': `multipart/form-data; boundary=${upload.boundary}`, + 'Content-Length': upload.body.length, + }, + }, + upload.body, + ); + assert.equal(imported.status, 200, `import failed: ${JSON.stringify(imported.body).slice(0, 200)}`); + assert.ok(imported.body.areaId, 'import must report the area it created'); + + // The regression: this used to answer 400 "请先导入 OSM 文件" even though the + // import above had just succeeded. + const compiled = await request({ method: 'POST', path: '/api/compile' }); + assert.equal(compiled.status, 200, `compile after import failed: ${JSON.stringify(compiled.body).slice(0, 200)}`); + assert.ok(compiled.body.compiled, 'compile must return the recompiled state'); + assert.ok(compiled.body.compiled.model.roads.length > 0, 'the recompile must still produce roads'); + + // And it must stay repeatable, since saving a direct edit now recompiles. + const again = await request({ method: 'POST', path: '/api/compile' }); + assert.equal(again.status, 200, 'a second recompile must also succeed'); +} + +run() + .then(() => { + console.log('workbench import/compile tests passed'); + }) + .catch((error) => { + console.error(error); + process.exitCode = 1; + }) + .finally(() => { + server.close(); + fs.rmSync(dataRoot, { recursive: true, force: true }); + }); diff --git a/workbench/server.js b/workbench/server.js index 5a24367..843eddb 100644 --- a/workbench/server.js +++ b/workbench/server.js @@ -513,6 +513,16 @@ function readUpload(request, session) { try { const compiled = compileWorkbenchInput(input); session.area = compiled.area; + // Install the recompile hook too. Without it `/api/compile` answers "请先导入 + // OSM 文件" for the whole life of a UI-imported session, because compileFresh + // is only wired when the server is started with an area on the command line. + // That broke "保存并重新生成" and, once direct edits recompiled on save, made a + // successful save report a failed regeneration. + session.context.compileFresh = () => { + const recompiled = compileWorkbenchInput(input); + session.area = recompiled.area; + return recompiled; + }; return compiled; } catch (error) { fs.rmSync(root, { recursive: true, force: true });