@@ -14,6 +14,7 @@ import { brokerEndpointFor, brokerIdentityNameForWireOptions, ensureZCodeBroker,
1414import { atomicWriteJson , withFileLock } from '../scripts/lib/fs.mjs' ;
1515import { PluginError } from '../scripts/lib/errors.mjs' ;
1616import { resolveWorkspaceStorage } from '../scripts/lib/workspace.mjs' ;
17+ import { validCreateSnapshot , validSetupAuthProbeSnapshot , validSnapshot } from '../scripts/lib/zcode-schema.mjs' ;
1718
1819const fixture = fileURLToPath ( new URL ( './fixtures/fake-zcode-cli.mjs' , import . meta. url ) ) ;
1920const brokerStartupFault = fileURLToPath ( new URL ( './fixtures/broker-startup-fault.cjs' , import . meta. url ) ) ;
@@ -236,6 +237,86 @@ test('typed operations use real 0.16.1 method and parameter shapes', async () =>
236237 } ) ;
237238} ) ;
238239
240+ test ( 'ordinary session/create accepts the bounded 0.16.1 initial empty-session snapshot' , async ( ) => {
241+ await withClient ( async ( client ) => {
242+ const created = await client . createSession ( { workspace : '/repo' } ) ;
243+ assert . equal ( created . session . sessionId , 'session-1' ) ;
244+ assert . equal ( created . projection . sessionId , 'unknown' ) ;
245+ assert . deepEqual ( created . messages , [ ] ) ;
246+ } , { FAKE_ZCODE_EMPTY_SESSION : '1' } ) ;
247+ } ) ;
248+
249+ test ( 'ordinary session/create rejects conflicting or non-empty unknown-projection snapshots' , async ( t ) => {
250+ for ( const variant of [ 'conflict' , 'non-idle' , 'event-seq' , 'messages' , 'target' ] ) await t . test ( variant , ( ) => withClient ( async ( client ) => {
251+ await assert . rejects ( client . createSession ( { workspace : '/repo' } ) , { code : 'ZCODE_OUTPUT_INVALID' } ) ;
252+ } , { FAKE_ZCODE_EMPTY_SESSION : '1' , FAKE_ZCODE_EMPTY_SESSION_VARIANT : variant } ) ) ;
253+ } ) ;
254+
255+ test ( 'ordinary empty session/create retains explicit session ID binding' , async ( ) => {
256+ await withClient ( async ( client ) => {
257+ await assert . rejects ( client . createSession ( { workspace : '/repo' , sessionId : 'requested-session' } ) , { code : 'ZCODE_OUTPUT_INVALID' } ) ;
258+ } , { FAKE_ZCODE_EMPTY_SESSION : '1' , FAKE_ZCODE_SESSION_ID : 'different-session' } ) ;
259+ } ) ;
260+
261+ test ( 'the unknown-projection exception remains confined to session/create' , async ( t ) => {
262+ for ( const method of [ 'session/read' , 'session/resume' , 'session/setModel' , 'session/setThoughtLevel' ] ) await t . test ( method , ( ) => withClient ( async ( client ) => {
263+ const sessionId = ( await client . createSession ( { workspace : '/repo' } ) ) . session . sessionId ;
264+ const operation = method === 'session/read' ? ( ) => client . readSession ( sessionId )
265+ : method === 'session/resume' ? ( ) => client . resumeSession ( sessionId )
266+ : method === 'session/setModel' ? ( ) => client . setModel ( sessionId , { providerId : 'fake2' , modelId : 'other' } )
267+ : ( ) => client . setThoughtLevel ( sessionId , 'high' ) ;
268+ await assert . rejects ( operation ( ) , { code : 'ZCODE_OUTPUT_INVALID' } ) ;
269+ } , { FAKE_ZCODE_EMPTY_SESSION : '1' } ) ) ;
270+ } ) ;
271+
272+ test ( 'the empty-create validator rejects every remaining non-empty or conflicting relation' , async ( ) => {
273+ await withClient ( async ( client ) => {
274+ const empty = await client . createSession ( { workspace : '/repo' } ) ;
275+ const sessionId = empty . session . sessionId ; const workspace = resolve ( '/repo' ) ;
276+ assert . equal ( validSnapshot ( empty , sessionId , workspace ) , false , 'fixture must enter the empty-create branch' ) ;
277+ const target = { sessionId, targetId : 'target-1' , objective : 'not empty' , summaryTitle : null , status : 'active' , tokenBudget : null , tokensUsed : 0 , timeUsedSeconds : 0 , createdAt : 1 , updatedAt : 1 } ;
278+ const permission = { requestId : 'request-1' , toolCallId : 'tool-1' , toolName : 'write' , reason : 'not empty' , riskLevel : 'low' , options : [ { optionId : 'allow' , kind : 'allow' , name : 'Allow' , response : { decision : 'allow' } } ] , requestedAt : 1 } ;
279+ const verification = { passed : false , reason : 'not empty' } ;
280+ const cases = [
281+ [ 'non-idle session status' , ( value ) => { value . session . status = 'running' ; } , true ] ,
282+ [ 'non-null session target' , ( value ) => { value . session . target = target ; } , true ] ,
283+ [ 'current projection turn' , ( value ) => { value . projection . currentTurnId = 'turn-1' ; } , true ] ,
284+ [ 'nonzero projection turn count' , ( value ) => { value . projection . turnCount = 1 ; } , true ] ,
285+ [ 'nonzero projection token count' , ( value ) => { value . projection . totalTokenCount = 1 ; } , true ] ,
286+ [ 'nonzero projection context use' , ( value ) => { value . projection . contextUsed = 1 ; } , true ] ,
287+ [ 'pending projection permission' , ( value ) => { value . projection . pendingPermissions = [ permission ] ; } , true ] ,
288+ [ 'active projection tool call' , ( value ) => { value . projection . activeToolCalls = [ { toolCallId : 'tool-1' , toolName : 'write' , status : 'pending' } ] ; } , true ] ,
289+ [ 'background projection job' , ( value ) => { value . projection . backgroundJobs = [ { } ] ; } , true ] ,
290+ [ 'projection error' , ( value ) => { value . projection . lastError = { type : 'runtime' , message : 'not empty' } ; } , true ] ,
291+ [ 'nonzero runtime revision' , ( value ) => { value . runtime . stateRevision = 1 ; } , true ] ,
292+ [ 'active runtime turn ID' , ( value ) => { value . runtime . activeTurnId = 'turn-1' ; } , true ] ,
293+ [ 'active runtime turn kind' , ( value ) => { value . runtime . activeTurnKind = 'regular' ; } , true ] ,
294+ [ 'pending runtime request' , ( value ) => { value . runtime . pendingRequestIds = [ 'request-1' ] ; } , true ] ,
295+ [ 'runtime API retry' , ( value ) => { value . runtime . apiRetry = { kind : 'api_retry' , attempt : 1 , maxRetries : 2 , retryDelayMs : 100 , errorStatus : null , error : 'retrying' } ; } , true ] ,
296+ [ 'runtime context usage' , ( value ) => { value . runtime . contextUsage = { used : 1 , size : 128000 } ; } , true ] ,
297+ [ 'runtime goal verification' , ( value ) => { value . runtime . goalVerifications = [ verification ] ; } , true ] ,
298+ [ 'runtime goal verification timeline' , ( value ) => { value . runtime . goalVerificationTimeline = [ { version : 1 , kind : 'synthetic' , type : 'goal_verification' , display : 'separator' , targetId : 'target-1' , verificationId : 'verification-1' , status : 'started' } ] ; } , true ] ,
299+ [ 'wrong workspace path' , ( value ) => { value . session . workspace . workspacePath = '/wrong-workspace' ; } , false ] ,
300+ [ 'wrong workspace key' , ( value ) => { value . session . workspace . workspaceKey = '/wrong-workspace' ; } , false ] ,
301+ ] ;
302+ for ( const [ name , mutate , strictCompatible ] of cases ) {
303+ const candidate = structuredClone ( empty ) ; mutate ( candidate ) ;
304+ assert . equal ( validSnapshot ( candidate , sessionId , workspace ) , false , `${ name } : strict branch must remain unavailable` ) ;
305+ assert . equal ( validCreateSnapshot ( candidate , sessionId , workspace ) , false , `${ name } : empty-create branch must reject the mutation` ) ;
306+ assert . equal ( validSetupAuthProbeSnapshot ( candidate , sessionId , workspace ) , false , `${ name } : setup probe must reject the mutation` ) ;
307+ if ( strictCompatible ) {
308+ candidate . projection . sessionId = sessionId ;
309+ assert . equal ( validSnapshot ( candidate , sessionId , workspace ) , true , `${ name } : mutation must otherwise retain a valid snapshot envelope` ) ;
310+ }
311+ }
312+ const explicitEmpty = structuredClone ( empty ) ;
313+ explicitEmpty . session . target = null ; explicitEmpty . projection . target = null ; explicitEmpty . runtime . apiRetry = null ;
314+ explicitEmpty . runtime . goalVerifications = [ ] ; explicitEmpty . runtime . goalVerificationTimeline = [ ] ;
315+ assert . equal ( validCreateSnapshot ( explicitEmpty , sessionId , workspace ) , true , 'explicit null and empty activity state must remain fresh' ) ;
316+ assert . equal ( validSetupAuthProbeSnapshot ( explicitEmpty , sessionId , workspace ) , true , 'setup probe must accept explicit null and empty activity state' ) ;
317+ } , { FAKE_ZCODE_EMPTY_SESSION : '1' } ) ;
318+ } ) ;
319+
239320test ( 'session/create answers runtime preference requests with the exact string ID' , async ( ) => {
240321 await withClient ( async ( client , record ) => {
241322 const created = await client . createSession ( { workspace : '/repo' } ) ;
0 commit comments