@1agh/maude 0.26.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -317,15 +317,30 @@ export function createCanvasSyncAgent(opts: CanvasSyncAgentOptions): CanvasSyncA
|
|
|
317
317
|
lastMeta = docMeta;
|
|
318
318
|
lastCss = docCss;
|
|
319
319
|
if (localHtml !== docHtml) {
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
|
|
324
|
-
|
|
320
|
+
// DATA-LOSS GUARD: an EMPTY hub doc at cold-start means the hub holds no
|
|
321
|
+
// body for this slug yet (fresh / never-seeded hub) — NOT an authoritative
|
|
322
|
+
// "this canvas is blank". Overwriting a non-empty local body with it would
|
|
323
|
+
// silently destroy the canvas (observed in the wild: a fresh hub emptied
|
|
324
|
+
// every local .tsx on first connect). The comments/annotations/meta/css
|
|
325
|
+
// branches below already skip empty-doc writes, and the shared-doc
|
|
326
|
+
// projection path documents the same "never clobber non-empty local with
|
|
327
|
+
// an empty doc value" rule — the HTML body was the one branch missing it.
|
|
328
|
+
// Resolution: seed the doc FROM local instead, so the body survives AND
|
|
329
|
+
// the hub gets our content (local→doc, exactly like a first-sync seed).
|
|
330
|
+
if (docHtml === '' && localHtml !== null && localHtml.trim() !== '') {
|
|
331
|
+
applyHtmlToDoc(doc, localHtml, origin);
|
|
332
|
+
lastHtml = localHtml;
|
|
333
|
+
} else {
|
|
334
|
+
// Local had divergent, non-empty content that hub-wins is discarding —
|
|
335
|
+
// notify so the user knows their local edits were overwritten (Task 8).
|
|
336
|
+
// An absent/empty local file is a clean first-sync, not a conflict.
|
|
337
|
+
if (localHtml !== null && localHtml.trim() !== '') {
|
|
338
|
+
opts.onConflict?.({ slug, kind: 'cold-start-hub-wins' });
|
|
339
|
+
}
|
|
340
|
+
const hash = hashBytes(docHtml);
|
|
341
|
+
echoGuard.record(paths.html, hash);
|
|
342
|
+
writer(paths.html, docHtml);
|
|
325
343
|
}
|
|
326
|
-
const hash = hashBytes(docHtml);
|
|
327
|
-
echoGuard.record(paths.html, hash);
|
|
328
|
-
writer(paths.html, docHtml);
|
|
329
344
|
}
|
|
330
345
|
if (docCommentsStr !== '' && localComments !== docCommentsStr) {
|
|
331
346
|
const hash = hashBytes(docCommentsStr);
|
|
@@ -202,6 +202,34 @@ describe('CanvasSyncAgent — cold start reconciliation', () => {
|
|
|
202
202
|
expect(htmlFromDoc(docB)).toBe('<button>hub-v2</button>');
|
|
203
203
|
});
|
|
204
204
|
|
|
205
|
+
test('empty hub doc does NOT clobber a non-empty local body — seeds local up instead (data-loss guard)', async () => {
|
|
206
|
+
// Local holds the real canvas body; the hub is fresh — no state for this
|
|
207
|
+
// slug yet (docB starts empty → docHtml === ''). The old behaviour wrote
|
|
208
|
+
// the empty doc over disk and emptied the file; the guard must keep local.
|
|
209
|
+
writeFileSync(paths().html, '<button>real-canvas</button>');
|
|
210
|
+
|
|
211
|
+
let conflicts = 0;
|
|
212
|
+
agent = createCanvasSyncAgent({
|
|
213
|
+
slug: 'screen',
|
|
214
|
+
doc: docB,
|
|
215
|
+
paths: paths(),
|
|
216
|
+
echoGuard: createEchoGuard(),
|
|
217
|
+
flushMs: 0,
|
|
218
|
+
onConflict: () => {
|
|
219
|
+
conflicts++;
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
agent.start();
|
|
223
|
+
await agent.reconcile();
|
|
224
|
+
|
|
225
|
+
// The local body MUST survive — an empty hub must never empty a real canvas.
|
|
226
|
+
expect(readFileSync(paths().html, 'utf8')).toBe('<button>real-canvas</button>');
|
|
227
|
+
// …and it is seeded UP to the hub instead of being discarded (local→doc).
|
|
228
|
+
expect(htmlFromDoc(docA)).toBe('<button>real-canvas</button>');
|
|
229
|
+
// An empty-hub seed is not a "hub overwrote your local" conflict.
|
|
230
|
+
expect(conflicts).toBe(0);
|
|
231
|
+
});
|
|
232
|
+
|
|
205
233
|
test('identical disk + doc states: no disk write', async () => {
|
|
206
234
|
writeFileSync(paths().html, '<button>same</button>');
|
|
207
235
|
applyHtmlToDoc(docA, '<button>same</button>');
|