@@ -46,7 +46,114 @@ async function failInitialEvidence(context: Awaited<ReturnType<typeof setup>>) {
4646 return claimed . id ;
4747}
4848
49+ async function childEvidence (
50+ context : Awaited < ReturnType < typeof setup > > ,
51+ complete = true ,
52+ ) {
53+ let child = await context . storage . channels . create ( {
54+ id : crypto . randomUUID ( ) ,
55+ repositoryId : REPOSITORY_ID ,
56+ repositoryOwner : "octo-org" ,
57+ repositoryName : "score" ,
58+ parentChannelId : context . channelId ,
59+ title : "Published research child" ,
60+ createdBy : context . userId ,
61+ now : context . advance ( ) ,
62+ } ) ;
63+ let legacy = await context . storage . research . start ( {
64+ id : crypto . randomUUID ( ) ,
65+ channelId : child . id ,
66+ title : "Legacy child research" ,
67+ question : "Create a grandchild" ,
68+ origin : "inline" ,
69+ createdBy : context . userId ,
70+ turnId : crypto . randomUUID ( ) ,
71+ messageId : crypto . randomUUID ( ) ,
72+ requestId : requestId ( 1 ) ,
73+ idempotencyKey : "legacy-child-request" ,
74+ fingerprint : "legacy-child-request" ,
75+ now : context . advance ( ) ,
76+ lease : context . lease ,
77+ } ) ;
78+ let evidence = await context . jobs . enqueueUser ( {
79+ channelId : child . id ,
80+ type : "research-evidence" ,
81+ targetKey : `workspace:${ legacy . workspace . id } :turn:${ legacy . turn . id } :evidence` ,
82+ idempotencyKey : "legacy-child-evidence" ,
83+ input : {
84+ workspaceId : legacy . workspace . id ,
85+ turnId : legacy . turn . id ,
86+ query : legacy . turn . question ,
87+ } ,
88+ } ) ;
89+ await context . storage . research . linkJob ( {
90+ channelId : child . id ,
91+ workspaceId : legacy . workspace . id ,
92+ turnId : legacy . turn . id ,
93+ role : "evidence" ,
94+ jobId : evidence . job . id ,
95+ now : context . advance ( ) ,
96+ lease : context . lease ,
97+ } ) ;
98+ if ( ! complete ) return { child, legacy, evidence : evidence . job } ;
99+ let [ claimed ] = await context . storage . jobs . claim ( {
100+ channelId : child . id ,
101+ claimOwner : "completed-child-worker" ,
102+ count : 1 ,
103+ ttlMs : 30_000 ,
104+ now : context . advance ( ) ,
105+ lease : context . lease ,
106+ } ) ;
107+ if ( ! claimed ) throw new Error ( "child evidence job was not claimable" ) ;
108+ let completed = await context . jobs . settle ( {
109+ channelId : child . id ,
110+ jobId : claimed . id ,
111+ claimOwner : "completed-child-worker" ,
112+ claimGeneration : claimed . claimGeneration ,
113+ artifact : evidenceArtifact ( claimed ) ,
114+ } ) ;
115+ return { child, legacy, evidence : completed . job } ;
116+ }
117+
49118describe ( "research workspace service" , ( ) => {
119+ it ( "observes completed legacy child evidence without reconciling new answer work" , async ( ) => {
120+ let context = await setup ( ) ;
121+ let { child, legacy } = await childEvidence ( context ) ;
122+
123+ expect ( await context . service . reconcile ( child . id , legacy . workspace . id ) ) . toBe ( true ) ;
124+ expect ( await context . service . get ( child . id , legacy . workspace . id ) )
125+ . toMatchObject ( { workspace : { id : legacy . workspace . id } } ) ;
126+ expect ( await context . storage . research . get ( child . id , legacy . workspace . id ) )
127+ . toMatchObject ( { turns : [ { evidenceJobId : expect . any ( String ) , answerJobId : undefined } ] } ) ;
128+ expect ( ( await context . jobs . list ( child . id , 100 ) ) ! . jobs ) . toHaveLength ( 1 ) ;
129+ } ) ;
130+
131+ it ( "does not advance completed legacy child evidence from job changes" , async ( ) => {
132+ let context = await setup ( ) ;
133+ let { child, legacy, evidence } = await childEvidence ( context ) ;
134+
135+ await context . service . jobChanged ( evidence ) ;
136+ expect ( await context . storage . research . get ( child . id , legacy . workspace . id ) )
137+ . toMatchObject ( { turns : [ { evidenceJobId : expect . any ( String ) , answerJobId : undefined } ] } ) ;
138+ expect ( ( await context . jobs . list ( child . id , 100 ) ) ! . jobs ) . toHaveLength ( 1 ) ;
139+ } ) ;
140+
141+ it ( "keeps active legacy child research cancellable and readable" , async ( ) => {
142+ let context = await setup ( ) ;
143+ let { child, legacy, evidence } = await childEvidence ( context , false ) ;
144+
145+ expect ( await context . service . get ( child . id , legacy . workspace . id ) )
146+ . toMatchObject ( { workspace : { id : legacy . workspace . id } } ) ;
147+ expect (
148+ await context . service . cancelRequest ( {
149+ channelId : child . id ,
150+ workspaceId : legacy . workspace . id ,
151+ } ) ,
152+ ) . toMatchObject ( { id : legacy . workspace . id , stage : "cancelled" } ) ;
153+ expect ( await context . jobs . get ( child . id , evidence . id ) )
154+ . toMatchObject ( { job : { state : "cancelled" } } ) ;
155+ } ) ;
156+
50157 it ( "starts one inline request and schedules its initial turn immediately" , async ( ) => {
51158 let context = await setup ( ) ;
52159 let brief = " Compare API v2 with v3.\nExplain migration risks. " ;
0 commit comments