diff --git a/test/workbench-edit-api.js b/test/workbench-edit-api.js index ee01d9b..573610b 100644 --- a/test/workbench-edit-api.js +++ b/test/workbench-edit-api.js @@ -7,6 +7,7 @@ const path = require('path'); const { compileInput } = require('../src/compile/compiler'); const { ensureRevisionStore } = require('../src/compile/road-revisions'); const { editPreview, editState, rebaseEdits, saveEdits } = require('../workbench/server'); +const { snapshot: outputSnapshot } = require('./fixture-baseline'); const workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'workbench-edit-api-')); const input = { @@ -119,5 +120,31 @@ assert.deepEqual( assert.equal(reloaded.constraintStates[0].status, 'exact'); assert.equal(reloaded.constraintStates[0].applied, true); +// A saved document has to reach the geometry, not just the file. `compileInput()` +// is the only caller that passes `editsFile`, and the workbench serves the outputs +// it wrote — so a stage that ignored the document would make a save look successful +// while a refresh still showed the pre-edit map. The existing identity test cannot +// catch that: an ignored document and an empty one produce the same output. +const noEdits = { + ...input, + outDir: path.join(workspace, 'outputs', 'no-edits'), + stagingDir: path.join(workspace, 'outputs', '_no-edits'), + areaConfigSnapshotFile: initialized.paths.activeAreaConfig, +}; +const withEdits = { + ...input, + outDir: path.join(workspace, 'outputs', 'with-edits'), + stagingDir: path.join(workspace, 'outputs', '_with-edits'), + areaConfigSnapshotFile: initialized.paths.activeAreaConfig, + editsFile: initialized.paths.activeEdits, +}; +compileInput(noEdits); +compileInput(withEdits); +assert.notDeepEqual( + outputSnapshot(withEdits).files, + outputSnapshot(noEdits).files, + 'a saved constraint must change the compiled output, or saving cannot survive a refresh', +); + fs.rmSync(workspace, { recursive: true, force: true }); console.log('workbench edit API tests passed'); diff --git a/workbench/client/src/App.tsx b/workbench/client/src/App.tsx index 48e1a0c..64159d3 100644 --- a/workbench/client/src/App.tsx +++ b/workbench/client/src/App.tsx @@ -277,15 +277,16 @@ function App() { const saveDirectEdits = async () => { if (!session) return; const fragment = session.fragment(); + let version: number; try { const result = await api.saveEdits({ expectedDocumentVersion: session.documentVersion, constraints: fragment.constraints, operations: fragment.operations, }); - session.markSaved(result.documentVersion); + version = result.documentVersion; + session.markSaved(version); touch(); - setStatus(`直接编辑已保存,文档版本 ${result.documentVersion}`); } catch (error) { if (!isVersionConflict(error)) { setStatus(`保存失败:${(error as Error).message}`); @@ -298,6 +299,22 @@ function App() { if (Number.isInteger(current)) session.setDocumentVersion(current as number); touch(); setStatus(`保存冲突:文档已被改到版本 ${current ?? '未知'},请刷新后重做本次编辑。`); + return; + } + // The document is written, but `/api/state` serves the outputs of the last + // compile — without recompiling, a refresh shows the pre-edit geometry and a + // successful save is indistinguishable from a failed one. Reported separately + // because at this point the save has already succeeded. + try { + setStatus('直接编辑已保存,正在重新生成几何...'); + const next = await api.compile(); + setState(next); + setSelected((current) => next.compiled.model.roads.find((road) => road.id === current?.id) || null); + // The baseline now carries the edit, so the preview overlay would draw it twice. + setPreview(null); + setStatus(`直接编辑已保存并重新生成,文档版本 ${version}`); + } catch (error) { + setStatus(`直接编辑已保存(版本 ${version}),但重新生成失败:${(error as Error).message}`); } }; const stage = (change: Override) =>