Skip to content

Commit d8e0252

Browse files
committed
test(document-api): prove metadata roundtrip story (SD-3104)
1 parent 39fbea0 commit d8e0252

2 files changed

Lines changed: 119 additions & 12 deletions

File tree

tests/doc-api-stories/tests/harness.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,11 @@ export function corpusDoc(relativePath: string): string {
6868
}
6969

7070
export function unwrap<T>(payload: any): T {
71-
return payload?.result ?? payload?.undefined ?? payload;
71+
if (payload && typeof payload === 'object') {
72+
if ('result' in payload) return payload.result;
73+
if ('undefined' in payload) return payload.undefined;
74+
}
75+
return payload;
7276
}
7377

7478
export interface StoryContext {

tests/doc-api-stories/tests/metadata/all-commands.ts

Lines changed: 114 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
*/
1313
import { describe, expect, it } from 'vitest';
1414
import { writeFile } from 'node:fs/promises';
15-
import { corpusDoc, unwrap, useStoryHarness } from '../harness';
15+
import path from 'node:path';
16+
import { unwrap, useStoryHarness } from '../harness';
1617

1718
const ALL_METADATA_COMMAND_IDS = [
1819
'metadata.attach',
@@ -42,7 +43,7 @@ type Scenario = {
4243
run: (sessionId: string, fixture: Fixture | null) => Promise<any>;
4344
};
4445

45-
const BASE_DOC = corpusDoc('basic/longer-header.docx');
46+
const BASE_DOC = path.resolve(import.meta.dirname, '../../../../shared/common/data/blank.docx');
4647
const NAMESPACE = 'urn:superdoc:metadata-story:1';
4748

4849
describe('document-api story: all metadata commands', () => {
@@ -70,6 +71,10 @@ describe('document-api story: all metadata commands', () => {
7071
return `${slug(operationId)}.docx`;
7172
}
7273

74+
function roundtripDocNameFor(operationId: MetadataCommandId): string {
75+
return `${slug(operationId)}-roundtrip.docx`;
76+
}
77+
7378
function readOutputNameFor(operationId: MetadataCommandId): string {
7479
return `${slug(operationId)}-read-output.json`;
7580
}
@@ -96,8 +101,10 @@ describe('document-api story: all metadata commands', () => {
96101
await callDocOperation('save', { sessionId, out: outPath(sourceDocNameFor(operationId)), force: true });
97102
}
98103

99-
async function saveResult(sessionId: string, operationId: MetadataCommandId): Promise<void> {
100-
await callDocOperation('save', { sessionId, out: outPath(resultDocNameFor(operationId)), force: true });
104+
async function saveResult(sessionId: string, operationId: MetadataCommandId): Promise<string> {
105+
const docPath = outPath(resultDocNameFor(operationId));
106+
await callDocOperation('save', { sessionId, out: docPath, force: true });
107+
return docPath;
101108
}
102109

103110
function assertMutationSuccess(operationId: string, result: any): void {
@@ -111,6 +118,86 @@ describe('document-api story: all metadata commands', () => {
111118
return fixture;
112119
}
113120

121+
function itemIds(result: any): string[] {
122+
return (result?.items ?? [])
123+
.map((item: any) => item?.id ?? item?.domain?.id)
124+
.filter((id: unknown) => typeof id === 'string');
125+
}
126+
127+
function expectedIdFor(operationId: MetadataCommandId, fixture: Fixture | null, result: any): string | null {
128+
if (operationId === 'metadata.attach' && typeof result?.id === 'string') return result.id;
129+
if (typeof fixture?.id === 'string') return fixture.id;
130+
return null;
131+
}
132+
133+
async function assertRoundtripState(
134+
operationId: MetadataCommandId,
135+
resultDocPath: string,
136+
fixture: Fixture | null,
137+
result: any,
138+
): Promise<void> {
139+
const reopenSessionId = makeSessionId(`${slug(operationId)}-reopen`);
140+
const expectedId = expectedIdFor(operationId, fixture, result);
141+
try {
142+
await callDocOperation('open', { sessionId: reopenSessionId, doc: resultDocPath });
143+
144+
if (operationId === 'metadata.remove') {
145+
if (!expectedId) throw new Error('metadata.remove roundtrip requires an id fixture.');
146+
const reopenedGet = await callDocOperation<any>('metadata.get', { sessionId: reopenSessionId, id: expectedId });
147+
expect(reopenedGet).toBeNull();
148+
const reopenedResolve = await callDocOperation<any>('metadata.resolve', {
149+
sessionId: reopenSessionId,
150+
id: expectedId,
151+
});
152+
expect(reopenedResolve).toBeNull();
153+
const reopenedList = await callDocOperation<any>('metadata.list', {
154+
sessionId: reopenSessionId,
155+
namespace: NAMESPACE,
156+
});
157+
expect(itemIds(reopenedList)).not.toContain(expectedId);
158+
await callDocOperation('save', {
159+
sessionId: reopenSessionId,
160+
out: outPath(roundtripDocNameFor(operationId)),
161+
force: true,
162+
});
163+
return;
164+
}
165+
166+
if (!expectedId) throw new Error(`${operationId} roundtrip requires an anchored metadata id.`);
167+
const reopenedInfo = await callDocOperation<any>('metadata.get', { sessionId: reopenSessionId, id: expectedId });
168+
expect(reopenedInfo?.id).toBe(expectedId);
169+
expect(reopenedInfo?.namespace).toBe(NAMESPACE);
170+
171+
const reopenedResolve = await callDocOperation<any>('metadata.resolve', {
172+
sessionId: reopenSessionId,
173+
id: expectedId,
174+
});
175+
expect(reopenedResolve?.id).toBe(expectedId);
176+
expect(reopenedResolve?.target?.kind).toBe('selection');
177+
178+
const reopenedList = await callDocOperation<any>('metadata.list', {
179+
sessionId: reopenSessionId,
180+
namespace: NAMESPACE,
181+
});
182+
expect(itemIds(reopenedList)).toContain(expectedId);
183+
184+
const reopenedWithin = await callDocOperation<any>('metadata.list', {
185+
sessionId: reopenSessionId,
186+
namespace: NAMESPACE,
187+
within: reopenedResolve.target,
188+
});
189+
expect(itemIds(reopenedWithin)).toContain(expectedId);
190+
191+
await callDocOperation('save', {
192+
sessionId: reopenSessionId,
193+
out: outPath(roundtripDocNameFor(operationId)),
194+
force: true,
195+
});
196+
} finally {
197+
await callDocOperation('close', { sessionId: reopenSessionId, discard: true }).catch(() => {});
198+
}
199+
}
200+
114201
async function seedTextTarget(sessionId: string, text: string): Promise<TextTarget> {
115202
const insertResult = await callDocOperation<any>('insert', { sessionId, value: text });
116203
const blockId = insertResult?.target?.blockId;
@@ -158,10 +245,15 @@ describe('document-api story: all metadata commands', () => {
158245
payload: { kind: 'citation', source: 'Story v1' },
159246
});
160247

161-
// Survives a list round-trip
248+
const withinResult = await callDocOperation<any>('metadata.list', {
249+
sessionId,
250+
namespace: NAMESPACE,
251+
within: f.target,
252+
});
253+
expect(itemIds(withinResult)).toContain(id);
254+
162255
const listResult = await callDocOperation<any>('metadata.list', { sessionId, namespace: NAMESPACE });
163-
const ids = (listResult?.items ?? []).map((item: any) => item?.id ?? item?.domain?.id);
164-
expect(ids).toContain(id);
256+
expect(itemIds(listResult)).toContain(id);
165257

166258
return attachResult;
167259
},
@@ -173,11 +265,22 @@ describe('document-api story: all metadata commands', () => {
173265
await attachOne(sessionId, id, { kind: 'citation', source: 'List scenario' });
174266
return { id };
175267
},
176-
run: async (sessionId) => {
268+
run: async (sessionId, fixture) => {
177269
const result = await callDocOperation<any>('metadata.list', { sessionId, namespace: NAMESPACE });
178270
expect(typeof result?.total).toBe('number');
179271
expect(result.total).toBeGreaterThanOrEqual(1);
180272
expect(Array.isArray(result?.items)).toBe(true);
273+
274+
const f = requireFixture('metadata.list', fixture);
275+
if (!f.id) throw new Error('metadata.list requires an id fixture.');
276+
const resolved = await callDocOperation<any>('metadata.resolve', { sessionId, id: f.id });
277+
expect(resolved?.target?.kind).toBe('selection');
278+
const narrowed = await callDocOperation<any>('metadata.list', {
279+
sessionId,
280+
namespace: NAMESPACE,
281+
within: resolved.target,
282+
});
283+
expect(itemIds(narrowed)).toContain(f.id);
181284
return result;
182285
},
183286
},
@@ -260,8 +363,7 @@ describe('document-api story: all metadata commands', () => {
260363
expect(afterResolve).toBeNull();
261364

262365
const listAfter = await callDocOperation<any>('metadata.list', { sessionId, namespace: NAMESPACE });
263-
const ids = (listAfter?.items ?? []).map((item: any) => item?.id ?? item?.domain?.id);
264-
expect(ids).not.toContain(f.id);
366+
expect(itemIds(listAfter)).not.toContain(f.id);
265367

266368
return removeResult;
267369
},
@@ -292,7 +394,8 @@ describe('document-api story: all metadata commands', () => {
292394
assertMutationSuccess(scenario.operationId, result);
293395
}
294396

295-
await saveResult(sessionId, scenario.operationId);
397+
const resultDocPath = await saveResult(sessionId, scenario.operationId);
398+
await assertRoundtripState(scenario.operationId, resultDocPath, fixture, result);
296399
} finally {
297400
await callDocOperation('close', { sessionId, discard: true }).catch(() => {});
298401
}

0 commit comments

Comments
 (0)