Drop test-only ETag helpers. App now calls saveControlledBlockEdit, which PATCHes once on the bound editor docId or fail-closes. Resume hydrate after a blocked first load, and refuse silent doc switches. Co-authored-by: Cursor <cursoragent@cursor.com>
94 lines
3.2 KiB
TypeScript
94 lines
3.2 KiB
TypeScript
import { describe, it } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
DOC_SWITCH_WHILE_EDITING_MESSAGE,
|
|
EDITOR_DOC_MISMATCH_MESSAGE,
|
|
HEAD_CONFLICT_SAVE_MESSAGE,
|
|
STALE_BLOCK_SAVE_MESSAGE,
|
|
decideActiveDocSwitch,
|
|
decideEditorDocBinding,
|
|
interpretBlockSaveError,
|
|
resolveSaveTargetBlock,
|
|
} from './blockSavePolicy';
|
|
|
|
describe('resolveSaveTargetBlock', () => {
|
|
it('keeps the editor and does not fake success when the block id is gone', () => {
|
|
const decision = resolveSaveTargetBlock(
|
|
[{ id: 'blk_rebuilt', content: '同一段正文' }],
|
|
{ id: 'blk_from_modal', content: '同一段正文' },
|
|
);
|
|
assert.equal(decision.ok, false);
|
|
if (decision.ok) return;
|
|
assert.equal(decision.keepEditor, true);
|
|
assert.equal(decision.reason, 'block_not_found');
|
|
assert.equal(decision.message, STALE_BLOCK_SAVE_MESSAGE);
|
|
});
|
|
|
|
it('never retargets another block that happens to share the same content', () => {
|
|
const children = [
|
|
{ id: 'blk_other', content: '条款甲' },
|
|
{ id: 'blk_current', content: '条款甲' },
|
|
];
|
|
const stale = resolveSaveTargetBlock(children, { id: 'blk_missing', content: '条款甲' });
|
|
assert.equal(stale.ok, false);
|
|
const hit = resolveSaveTargetBlock(children, { id: 'blk_current', content: '条款甲(已改)' });
|
|
assert.deepEqual(hit, { ok: true, index: 1 });
|
|
});
|
|
});
|
|
|
|
describe('editor session document binding', () => {
|
|
it('fail-closes when activeDocId drifts away from the opened editor doc', () => {
|
|
const decision = decideEditorDocBinding({
|
|
editorDocId: 'doc_proj6140_intake',
|
|
activeDocId: 'doc_proj6140_research',
|
|
});
|
|
assert.equal(decision.ok, false);
|
|
if (decision.ok) return;
|
|
assert.equal(decision.keepEditor, true);
|
|
assert.equal(decision.reason, 'doc_mismatch');
|
|
assert.equal(decision.message, EDITOR_DOC_MISMATCH_MESSAGE);
|
|
});
|
|
|
|
it('accepts a save only when the editor is still bound to the active doc', () => {
|
|
assert.deepEqual(
|
|
decideEditorDocBinding({
|
|
editorDocId: 'doc_proj6140_intake',
|
|
activeDocId: 'doc_proj6140_intake',
|
|
}),
|
|
{ ok: true },
|
|
);
|
|
});
|
|
|
|
it('blocks switching documents while the editor is open', () => {
|
|
const blocked = decideActiveDocSwitch({
|
|
editorDocId: 'doc_a',
|
|
nextDocId: 'doc_b',
|
|
});
|
|
assert.equal(blocked.allow, false);
|
|
if (blocked.allow) return;
|
|
assert.equal(blocked.keepEditor, true);
|
|
assert.equal(blocked.message, DOC_SWITCH_WHILE_EDITING_MESSAGE);
|
|
assert.deepEqual(decideActiveDocSwitch({ editorDocId: 'doc_a', nextDocId: 'doc_a' }), {
|
|
allow: true,
|
|
});
|
|
assert.deepEqual(decideActiveDocSwitch({ editorDocId: null, nextDocId: 'doc_b' }), {
|
|
allow: true,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('interpretBlockSaveError', () => {
|
|
it('409 keeps the draft in the editor', () => {
|
|
const ui = interpretBlockSaveError({ code: 'DOCUMENT_HEAD_CONFLICT' });
|
|
assert.equal(ui.keepEditor, true);
|
|
assert.equal(ui.conflict, HEAD_CONFLICT_SAVE_MESSAGE);
|
|
});
|
|
|
|
it('other PATCH failures also keep the editor', () => {
|
|
const ui = interpretBlockSaveError({ message: 'network down' });
|
|
assert.equal(ui.keepEditor, true);
|
|
assert.match(ui.conflict, /network down/);
|
|
assert.match(ui.conflict, /仍保留/);
|
|
});
|
|
});
|