Files
Agent-System/web/src/lib/controlledDocHydrate.test.ts
T
weichenandCursor f693e1c474 fix(web): stop hydrating local legacy mock documents
Login-seeded doc-001 is not server-backed; align shouldLoad with
revisionToolbarMountMode so content and revision-state are never requested.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-14 02:56:35 +00:00

370 lines
12 KiB
TypeScript

import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import {
canonicalDocIdForTask,
controlledHydrateFingerprint,
hydrateControlledDocumentContent,
resolveCanonicalDocId,
shouldLoadControlledContent,
} from './controlledDocHydrate';
import {
revisionToolbarMountMode,
shouldMountDocumentRevisionToolbar,
} from '../features/documentRevision/revisionToolbarMount';
import { resolvePatchEtag } from './revisionEtag';
import { createEditorHydrationGate } from './editorHydrationGate';
import type { DeliverableItem } from '../types/workbench';
const base: DeliverableItem = {
docId: 'doc_proj56_intake',
projectId: 56,
taskKey: 'intake',
title: '用户初始需求收集表',
stage: 'V0.5',
category: 'workflow',
creatorId: 1,
creatorName: '张工',
isFixedWorkflow: true,
isTemplateOnly: false,
lifecycleState: 'drafting',
isClaimed: true,
author: '张工',
reviewer: '李主管',
approver: '王总监',
updatedAt: '2026-09-11',
governanceMode: 'controlled',
};
describe('controlledDocHydrate', () => {
it('shouldLoadControlledContent when non-template workflow doc', () => {
assert.equal(shouldLoadControlledContent(base), true);
assert.equal(shouldLoadControlledContent({ ...base, isTemplateOnly: true }), false);
});
it('shouldLoadControlledContent rejects local legacy/mock ids like doc-001', () => {
const local = { ...base, docId: 'doc-001', projectId: 1, taskKey: 'feasibility_reg' };
assert.equal(shouldLoadControlledContent(local), false);
assert.equal(shouldLoadControlledContent({ ...base, docId: 'doc-002' }), false);
assert.equal(shouldLoadControlledContent({ ...base, docId: 'doc-shared-1' }), false);
assert.equal(shouldLoadControlledContent({ ...base, docId: 'doc-private-01' }), false);
assert.equal(shouldLoadControlledContent({ ...base, docId: 'doc-cusa-feasibility-reg' }), false);
});
it('shouldLoadControlledContent keeps canonical, freeform-off, and template preview rules', () => {
assert.equal(shouldLoadControlledContent(base), true);
assert.equal(
shouldLoadControlledContent({
...base,
docId: 'doc-p143-intake',
isTemplateOnly: true,
taskKey: 'intake',
}),
false,
);
assert.equal(
shouldLoadControlledContent({
...base,
docId: 'doc-free-1789263145197-2',
governanceMode: 'freeform',
taskKey: 'freeform_doc',
isFixedWorkflow: false,
}),
false,
);
assert.equal(
shouldLoadControlledContent({
...base,
docId: 'doc-upload-1789263145197-0',
sourceType: 'uploaded_file',
}),
false,
);
});
it('hydrateControlledDocumentContent never requests content/revision for doc-001', async () => {
let contentCalls = 0;
let revisionCalls = 0;
const ast = await hydrateControlledDocumentContent(
'doc-001',
{ ...base, docId: 'doc-001' },
{
fetchContent: async () => {
contentCalls += 1;
throw new Error('must not fetch content for local mock');
},
fetchRevisionState: async () => {
revisionCalls += 1;
throw new Error('must not fetch revision-state for local mock');
},
},
);
assert.equal(ast, null);
assert.equal(contentCalls, 0);
assert.equal(revisionCalls, 0);
});
it('hydrateControlledDocumentContent never requests for template preview or registered freeform', async () => {
const forbidden = {
fetchContent: async () => {
throw new Error('must not fetch content');
},
fetchRevisionState: async () => {
throw new Error('must not fetch revision-state');
},
};
const template = await hydrateControlledDocumentContent(
'doc-p143-intake',
{ ...base, docId: 'doc-p143-intake', isTemplateOnly: true, taskKey: 'intake' },
forbidden,
);
const freeform = await hydrateControlledDocumentContent(
'doc-free-1789263145197-2',
{
...base,
docId: 'doc-free-1789263145197-2',
governanceMode: 'freeform',
taskKey: 'freeform_doc',
isFixedWorkflow: false,
},
forbidden,
);
assert.equal(template, null);
assert.equal(freeform, null);
});
it('shouldLoadControlledContent stays inside toolbar server-backed eligibility', () => {
const samples: DeliverableItem[] = [
base,
{ ...base, docId: 'doc-001', projectId: 1, taskKey: 'feasibility_reg' },
{ ...base, docId: 'doc-p143-intake', isTemplateOnly: true, taskKey: 'intake' },
{
...base,
docId: 'doc-free-1789263145197-2',
governanceMode: 'freeform',
taskKey: 'freeform_doc',
isFixedWorkflow: false,
},
{ ...base, docId: 'doc-upload-1789263145197-0', sourceType: 'uploaded_file' },
];
for (const doc of samples) {
const load = shouldLoadControlledContent(doc);
const mount = shouldMountDocumentRevisionToolbar(doc);
if (load) {
assert.equal(mount, true, `${doc.docId} loaded without toolbar eligibility`);
assert.equal(revisionToolbarMountMode(doc), 'toolbar');
}
if (doc.docId === 'doc-001') {
assert.equal(load, false);
assert.equal(mount, false);
assert.equal(revisionToolbarMountMode(doc), 'local_hint');
}
}
});
it('resolveCanonicalDocId maps doc-p56-intake to doc_proj56_intake', () => {
assert.equal(resolveCanonicalDocId('doc-p56-intake', [base]), 'doc_proj56_intake');
});
it('canonicalDocIdForTask maps doc-p56-research to doc_proj56_research', () => {
assert.equal(
canonicalDocIdForTask(56, 'research', undefined, 'doc-p56-research'),
'doc_proj56_research',
);
});
it('attaches hydrate ETag as PATCH baseline and ignores a later GET', async () => {
const ast = await hydrateControlledDocumentContent('doc_proj56_intake', base, {
fetchContent: async () => ({
doc_id: 'doc_proj56_intake',
version: 1,
content_sha256: 'abc',
markdown: '# 标题\n\n正文',
is_template_only: false,
}),
fetchRevisionState: async () => ({ etag: '2:7' }),
});
assert.ok(ast);
assert.equal(ast?.revisionEtag, '2:7');
const latestGet = { etag: '2:8' };
const patchEtag = resolvePatchEtag({
revisionEtag: ast?.revisionEtag,
governanceMode: 'controlled',
});
assert.equal(patchEtag, '2:7');
assert.notEqual(patchEtag, latestGet.etag);
});
it('rejects hydrate when revision-state network or 5xx fails', async () => {
await assert.rejects(
() =>
hydrateControlledDocumentContent('doc_proj56_intake', base, {
fetchContent: async () => ({
doc_id: 'doc_proj56_intake',
version: 1,
content_sha256: 'abc',
markdown: '# 标题\n\n正文',
is_template_only: false,
}),
fetchRevisionState: async () => {
const err = new Error('revision-state unavailable') as Error & { status?: number };
err.status = 502;
throw err;
},
}),
(err: Error & { status?: number }) => err.status === 502,
);
});
it('aligns markdownToBlockAST random ids onto revision-state head_block_map', async () => {
const markdown = '# 标题\n\n正文';
const head_block_map = [
{ id: 'srv_heading_intake', type: 'heading', start_offset: 0, end_offset: 4, order: 0 },
{ id: 'srv_para_intake', type: 'paragraph', start_offset: 5, end_offset: 7, order: 1 },
];
const hydrate = () =>
hydrateControlledDocumentContent('doc_proj56_intake', base, {
fetchContent: async () => ({
doc_id: 'doc_proj56_intake',
version: 1,
content_sha256: 'abc',
markdown,
is_template_only: false,
}),
fetchRevisionState: async () => ({ etag: '1:0', head_block_map }),
});
const first = await hydrate();
const second = await hydrate();
assert.ok(first && second);
const firstIds = first.rootBlock.children?.map((c) => c.id);
const secondIds = second.rootBlock.children?.map((c) => c.id);
assert.deepEqual(firstIds, ['srv_heading_intake', 'srv_para_intake']);
assert.deepEqual(secondIds, firstIds);
});
it('leaves Math.random ids when head_block_map is omitted (why gate + fail-closed remain)', async () => {
const markdown = '# 标题\n\n正文';
const hydrate = () =>
hydrateControlledDocumentContent('doc_proj56_intake', base, {
fetchContent: async () => ({
doc_id: 'doc_proj56_intake',
version: 1,
content_sha256: 'abc',
markdown,
is_template_only: false,
}),
fetchRevisionState: async () => ({ etag: '1:0' }),
});
const first = await hydrate();
const second = await hydrate();
assert.ok(first && second);
const firstIds = first.rootBlock.children?.map((c) => c.id) ?? [];
const secondIds = second.rootBlock.children?.map((c) => c.id) ?? [];
assert.equal(firstIds.length, 2);
assert.equal(secondIds.length, 2);
assert.notDeepEqual(firstIds, secondIds);
});
it('rejects controlled hydrate on NOT_FOUND or NEEDS_CONTENT', async () => {
const missingLedger = async () => {
const err = new Error('needs content') as Error & { code?: string };
err.code = 'DOCUMENT_REVISION_NEEDS_CONTENT';
throw err;
};
await assert.rejects(
() =>
hydrateControlledDocumentContent('doc_proj56_intake', base, {
fetchContent: async () => ({
doc_id: 'doc_proj56_intake',
version: 1,
content_sha256: 'abc',
markdown: '# 标题\n\n正文',
is_template_only: false,
}),
fetchRevisionState: missingLedger,
}),
(err: Error & { code?: string }) => err.code === 'DOCUMENT_REVISION_NEEDS_CONTENT',
);
});
it('fingerprint stays stable when only object identity changes', () => {
assert.equal(controlledHydrateFingerprint(base), controlledHydrateFingerprint({ ...base }));
assert.notEqual(
controlledHydrateFingerprint(base),
controlledHydrateFingerprint({ ...base, updatedAt: '2026-09-14' }),
);
});
it('dedupes in-flight hydrate for the same doc fingerprint', async () => {
let calls = 0;
let release!: () => void;
const hold = new Promise<void>((resolve) => {
release = resolve;
});
const fetchContent = async () => {
calls += 1;
await hold;
return {
doc_id: 'doc_proj56_intake',
version: 1,
content_sha256: 'abc',
markdown: '# 标题\n\n正文',
is_template_only: false,
};
};
const fetchRevisionState = async () => ({ etag: '1:0' });
const a = hydrateControlledDocumentContent('doc_proj56_intake', base, {
fetchContent,
fetchRevisionState,
});
const b = hydrateControlledDocumentContent('doc_proj56_intake', { ...base }, {
fetchContent,
fetchRevisionState,
});
release();
await Promise.all([a, b]);
assert.equal(calls, 1);
});
it('aligned late hydrate still cannot overwrite a PATCH-success baseline', async () => {
const markdown = '# 标题\n\n正文';
const head_block_map = [
{ id: 'srv_heading_intake', type: 'heading', start_offset: 0, end_offset: 4, order: 0 },
{ id: 'srv_para_intake', type: 'paragraph', start_offset: 5, end_offset: 7, order: 1 },
];
let release!: (value: 'go') => void;
const hold = new Promise<'go'>((resolve) => {
release = resolve;
});
const gate = createEditorHydrationGate();
gate.setActiveDocId('doc_proj56_intake');
const token = gate.beginHydration('doc_proj56_intake');
assert.ok(token);
const pending = hydrateControlledDocumentContent('doc_proj56_intake', base, {
fetchContent: async () => {
await hold;
return {
doc_id: 'doc_proj56_intake',
version: 1,
content_sha256: 'abc',
markdown,
is_template_only: false,
};
},
fetchRevisionState: async () => ({ etag: '1:0', head_block_map }),
});
gate.setEditorOpen(true);
gate.markSaved();
gate.setEditorOpen(false);
release('go');
const ast = await pending;
assert.ok(ast);
assert.deepEqual(ast.rootBlock.children?.map((c) => c.id), [
'srv_heading_intake',
'srv_para_intake',
]);
assert.deepEqual(gate.decide(token), { apply: false, reason: 'superseded_by_save' });
});
});