Saving wrote the v2 document but nothing else, and `/api/state` serves the outputs of the last compile — so a refresh showed the pre-edit geometry and a successful save was indistinguishable from a failed one. Reported as "saved, refreshed, edit gone"; the document on disk was in fact correct, resolving as `exact` with the constraint reaching the profile. Save now recompiles and clears the preview overlay, since the baseline carries the edit afterwards and would otherwise draw it twice. The recompile is reported separately from the save: if it fails the message says the edit was saved and names the compile error, rather than claiming the save failed. The existing identity test could not have caught this. An ignored `editsFile` and an empty document produce the same output, so it proved nothing about a non-empty one. The API test now compiles with and without a saved constraint and asserts the outputs differ. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
151 lines
6.2 KiB
JavaScript
151 lines
6.2 KiB
JavaScript
'use strict';
|
|
|
|
const assert = require('assert/strict');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
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 = {
|
|
areaId: 'edit-api-test',
|
|
osmFile: path.join(workspace, 'source.osm'),
|
|
outDir: path.join(workspace, 'outputs', 'native-road'),
|
|
stagingDir: path.join(workspace, 'outputs', '_pipeline'),
|
|
overridesFile: path.join(workspace, 'native-road-overrides.json'),
|
|
trafficSignalsFile: path.join(workspace, 'native-traffic-signals.json'),
|
|
options: { edgeLines: false, junctionTemplates: { enabled: false, references: [] } },
|
|
};
|
|
fs.copyFileSync(path.join(__dirname, 'fixtures', 'fengshu-er-road.osm'), input.osmFile);
|
|
fs.writeFileSync(input.overridesFile, `${JSON.stringify({ schema: 'native-road-overrides/v1', overrides: [] })}\n`);
|
|
fs.writeFileSync(
|
|
input.trafficSignalsFile,
|
|
`${JSON.stringify({ schema: 'native-traffic-signals/v1', provenance: 'empty', assemblies: { type: 'FeatureCollection', features: [] } })}\n`,
|
|
);
|
|
const initialized = ensureRevisionStore(workspace, input.options);
|
|
const area = compileInput({
|
|
...input,
|
|
areaConfigSnapshotFile: initialized.paths.activeAreaConfig,
|
|
editsFile: initialized.paths.activeEdits,
|
|
}).area;
|
|
|
|
const state = editState(area);
|
|
assert.equal(state.active, true);
|
|
assert.equal(state.documentVersion, 0);
|
|
assert.equal(state.handles.schema, 'road-edit-handles/v1');
|
|
assert.equal(state.activeRevisionId, 'rev-0001');
|
|
|
|
const trackedFiles = [
|
|
initialized.paths.activeEdits,
|
|
initialized.paths.activeState,
|
|
initialized.paths.activeAreaConfig,
|
|
].map((file) => ({
|
|
file,
|
|
bytes: fs.readFileSync(file),
|
|
mtimeMs: fs.statSync(file).mtimeMs,
|
|
}));
|
|
const preview = editPreview(area, { previewSeq: 17, document: state.document });
|
|
assert.equal(preview.ok, true);
|
|
assert.equal(preview.previewSeq, 17);
|
|
assert.equal(preview.degraded, false);
|
|
assert.equal(preview.handles.schema, 'road-edit-handles/v1');
|
|
assert.ok(preview.layers.roadSurface);
|
|
for (const tracked of trackedFiles) {
|
|
assert.deepEqual(fs.readFileSync(tracked.file), tracked.bytes, `${tracked.file} bytes unchanged by preview`);
|
|
assert.equal(fs.statSync(tracked.file).mtimeMs, tracked.mtimeMs, `${tracked.file} mtime unchanged by preview`);
|
|
}
|
|
|
|
const saved = saveEdits(area, { expectedDocumentVersion: 0, document: state.document });
|
|
assert.equal(saved.documentVersion, 1);
|
|
const beforeConflict = fs.readFileSync(initialized.paths.activeEdits);
|
|
assert.throws(
|
|
() => saveEdits(area, { expectedDocumentVersion: 0, document: state.document }),
|
|
(error) => error.statusCode === 409 && error.current.documentVersion === 1,
|
|
);
|
|
assert.deepEqual(fs.readFileSync(initialized.paths.activeEdits), beforeConflict, '409 does not write the document');
|
|
|
|
const checkpoint = require('../src/compile/road-revisions').createCheckpoint(workspace, 'Preview checkpoint');
|
|
assert.equal(checkpoint.manifest.id, 'rev-0002');
|
|
const rebased = rebaseEdits(area, checkpoint.manifest.id);
|
|
assert.equal(rebased.counts.exact, 0);
|
|
assert.deepEqual(rebased.constraints, []);
|
|
|
|
// The map saves `constraints` + `operations`, not a whole document. That merge path
|
|
// is what the client actually exercises, and leaving the operations out is exactly
|
|
// how the first wiring produced a 400.
|
|
const edgeHandle = state.handles.handles.find((handle) => handle.kind === 'road-edge-offset');
|
|
assert.ok(edgeHandle, 'the fixture must publish a road edge handle');
|
|
const clientConstraint = {
|
|
id: `constraint:${edgeHandle.handleId}`,
|
|
kind: 'road-edge-offset',
|
|
anchor: edgeHandle.anchor,
|
|
anchorSnapshot: {
|
|
coordinate: edgeHandle.position,
|
|
tangentAzimuth: 0,
|
|
roadLengthMeters: 120,
|
|
osmNodeIds: ['1'],
|
|
},
|
|
value: { offsetMeters: 0.5, transition: 'smoothstep' },
|
|
enabled: true,
|
|
status: 'exact',
|
|
provenance: { operationId: 'operation:client:1', createdAt: '2026-08-27T00:00:00.000Z' },
|
|
};
|
|
const clientOperation = {
|
|
id: clientConstraint.provenance.operationId,
|
|
createdAt: clientConstraint.provenance.createdAt,
|
|
constraintIds: [clientConstraint.id],
|
|
};
|
|
assert.throws(
|
|
() => saveEdits(area, { expectedDocumentVersion: 1, constraints: [clientConstraint], operations: [] }),
|
|
/provenance\.operationId/,
|
|
'a constraint saved without its operation must be rejected, not written',
|
|
);
|
|
const clientSaved = saveEdits(area, {
|
|
expectedDocumentVersion: 1,
|
|
constraints: [clientConstraint],
|
|
operations: [clientOperation],
|
|
});
|
|
assert.equal(clientSaved.documentVersion, 2);
|
|
|
|
// A save that does not survive a reload as `exact` was cosmetic.
|
|
const reloaded = editState(area);
|
|
assert.equal(reloaded.documentVersion, 2);
|
|
assert.deepEqual(
|
|
reloaded.document.constraints.map((item) => item.id),
|
|
[clientConstraint.id],
|
|
);
|
|
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');
|