Background GET rebuilt markdown AST with Math.random ids and dropped head_block_map, so save closed the modal without PATCH. Align hydrate to the ledger map, gate in-flight responses, and fail-closed on stale ids. Co-authored-by: Cursor <cursoragent@cursor.com>
121 lines
3.7 KiB
TypeScript
121 lines
3.7 KiB
TypeScript
import { describe, it } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
createEditorHydrationGate,
|
|
decideHydrationApply,
|
|
} from './editorHydrationGate';
|
|
|
|
describe('decideHydrationApply', () => {
|
|
const token = { gen: 2, saveEpoch: 0, docId: 'doc_a' };
|
|
|
|
it('applies only when gen, doc, save epoch match and editor is closed', () => {
|
|
assert.deepEqual(
|
|
decideHydrationApply({
|
|
token,
|
|
currentGen: 2,
|
|
currentSaveEpoch: 0,
|
|
activeDocId: 'doc_a',
|
|
editorOpen: false,
|
|
}),
|
|
{ apply: true, reason: 'ok' },
|
|
);
|
|
});
|
|
|
|
it('rejects a response for another document', () => {
|
|
assert.deepEqual(
|
|
decideHydrationApply({
|
|
token,
|
|
currentGen: 2,
|
|
currentSaveEpoch: 0,
|
|
activeDocId: 'doc_b',
|
|
editorOpen: false,
|
|
}),
|
|
{ apply: false, reason: 'doc_mismatch' },
|
|
);
|
|
});
|
|
|
|
it('rejects a superseded generation', () => {
|
|
assert.deepEqual(
|
|
decideHydrationApply({
|
|
token,
|
|
currentGen: 3,
|
|
currentSaveEpoch: 0,
|
|
activeDocId: 'doc_a',
|
|
editorOpen: false,
|
|
}),
|
|
{ apply: false, reason: 'stale_generation' },
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('createEditorHydrationGate', () => {
|
|
it('does not start a new hydrate while the editor is open (UI-06 fill-window GET)', () => {
|
|
const gate = createEditorHydrationGate();
|
|
gate.setActiveDocId('doc_proj6140_intake');
|
|
const before = gate.beginHydration('doc_proj6140_intake');
|
|
assert.ok(before);
|
|
gate.setEditorOpen(true);
|
|
assert.equal(gate.beginHydration('doc_proj6140_intake'), null);
|
|
assert.equal(gate.decide(before).reason, 'editor_open');
|
|
assert.equal(gate.decide(before).apply, false);
|
|
});
|
|
|
|
it('deferred hydrate that resolves after the editor opens does not replace the session', async () => {
|
|
const gate = createEditorHydrationGate();
|
|
gate.setActiveDocId('doc_proj6140_intake');
|
|
const token = gate.beginHydration('doc_proj6140_intake');
|
|
assert.ok(token);
|
|
|
|
let resolveHydrate!: (ast: { blockId: string }) => void;
|
|
const pending = new Promise<{ blockId: string }>((resolve) => {
|
|
resolveHydrate = resolve;
|
|
});
|
|
|
|
gate.setEditorOpen(true);
|
|
resolveHydrate({ blockId: 'blk_rebuilt_during_fill' });
|
|
const rebuilt = await pending;
|
|
const decision = gate.decide(token);
|
|
|
|
assert.equal(rebuilt.blockId, 'blk_rebuilt_during_fill');
|
|
assert.equal(decision.apply, false);
|
|
assert.equal(decision.reason, 'editor_open');
|
|
});
|
|
|
|
it('covers hydrate resolving between fill and save', async () => {
|
|
const gate = createEditorHydrationGate();
|
|
gate.setActiveDocId('doc_proj6140_intake');
|
|
const token = gate.beginHydration('doc_proj6140_intake');
|
|
assert.ok(token);
|
|
|
|
gate.setEditorOpen(true);
|
|
|
|
let resolveDuringFill!: (value: 'hydrated') => void;
|
|
const hydrateDuringFill = new Promise<'hydrated'>((resolve) => {
|
|
resolveDuringFill = resolve;
|
|
});
|
|
|
|
resolveDuringFill('hydrated');
|
|
await hydrateDuringFill;
|
|
assert.equal(gate.decide(token).reason, 'editor_open');
|
|
|
|
gate.markSaved();
|
|
gate.setEditorOpen(false);
|
|
assert.deepEqual(gate.decide(token), { apply: false, reason: 'superseded_by_save' });
|
|
});
|
|
|
|
it('late hydrate after a successful PATCH does not roll back the new baseline', () => {
|
|
const gate = createEditorHydrationGate();
|
|
gate.setActiveDocId('doc_a');
|
|
const stale = gate.beginHydration('doc_a');
|
|
assert.ok(stale);
|
|
gate.setEditorOpen(true);
|
|
gate.markSaved();
|
|
gate.setEditorOpen(false);
|
|
assert.deepEqual(gate.decide(stale), { apply: false, reason: 'superseded_by_save' });
|
|
|
|
const fresh = gate.beginHydration('doc_a');
|
|
assert.ok(fresh);
|
|
assert.deepEqual(gate.decide(fresh), { apply: true, reason: 'ok' });
|
|
});
|
|
});
|