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 });