1212 */
1313import { describe , expect , it } from 'vitest' ;
1414import { 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
1718const 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') ;
4647const NAMESPACE = 'urn:superdoc:metadata-story:1' ;
4748
4849describe ( '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