@@ -1270,4 +1270,175 @@ suite('Protocol WebSocket E2E', function () {
12701270
12711271 await client . waitForNotification ( n => isActionNotification ( n , 'session/turnComplete' ) ) ;
12721272 } ) ;
1273+
1274+ // ---- Truncation tests ---------------------------------------------------
1275+
1276+ test ( 'truncate session removes turns after specified turn' , async function ( ) {
1277+ this . timeout ( 15_000 ) ;
1278+
1279+ const sessionUri = await createAndSubscribeSession ( client , 'test-truncate' ) ;
1280+
1281+ // Create two turns
1282+ dispatchTurnStarted ( client , sessionUri , 'turn-t1' , 'hello' , 1 ) ;
1283+ await client . waitForNotification ( n => isActionNotification ( n , 'session/turnComplete' ) && ( getActionEnvelope ( n ) . action as { turnId : string } ) . turnId === 'turn-t1' ) ;
1284+
1285+ client . clearReceived ( ) ;
1286+ dispatchTurnStarted ( client , sessionUri , 'turn-t2' , 'hello' , 2 ) ;
1287+ await client . waitForNotification ( n => isActionNotification ( n , 'session/turnComplete' ) && ( getActionEnvelope ( n ) . action as { turnId : string } ) . turnId === 'turn-t2' ) ;
1288+
1289+ // Verify 2 turns exist
1290+ let snapshot = await client . call < ISubscribeResult > ( 'subscribe' , { resource : sessionUri } ) ;
1291+ let state = snapshot . snapshot . state as ISessionState ;
1292+ assert . strictEqual ( state . turns . length , 2 ) ;
1293+
1294+ client . clearReceived ( ) ;
1295+
1296+ // Truncate: keep only turn-t1
1297+ client . notify ( 'dispatchAction' , {
1298+ clientSeq : 3 ,
1299+ action : { type : 'session/truncated' , session : sessionUri , turnId : 'turn-t1' } ,
1300+ } ) ;
1301+
1302+ await client . waitForNotification ( n => isActionNotification ( n , 'session/truncated' ) ) ;
1303+
1304+ snapshot = await client . call < ISubscribeResult > ( 'subscribe' , { resource : sessionUri } ) ;
1305+ state = snapshot . snapshot . state as ISessionState ;
1306+ assert . strictEqual ( state . turns . length , 1 ) ;
1307+ assert . strictEqual ( state . turns [ 0 ] . id , 'turn-t1' ) ;
1308+ } ) ;
1309+
1310+ test ( 'truncate all turns clears session history' , async function ( ) {
1311+ this . timeout ( 15_000 ) ;
1312+
1313+ const sessionUri = await createAndSubscribeSession ( client , 'test-truncate-all' ) ;
1314+
1315+ dispatchTurnStarted ( client , sessionUri , 'turn-ta1' , 'hello' , 1 ) ;
1316+ await client . waitForNotification ( n => isActionNotification ( n , 'session/turnComplete' ) ) ;
1317+
1318+ client . clearReceived ( ) ;
1319+
1320+ // Truncate all (no turnId)
1321+ client . notify ( 'dispatchAction' , {
1322+ clientSeq : 2 ,
1323+ action : { type : 'session/truncated' , session : sessionUri } ,
1324+ } ) ;
1325+
1326+ await client . waitForNotification ( n => isActionNotification ( n , 'session/truncated' ) ) ;
1327+
1328+ const snapshot = await client . call < ISubscribeResult > ( 'subscribe' , { resource : sessionUri } ) ;
1329+ const state = snapshot . snapshot . state as ISessionState ;
1330+ assert . strictEqual ( state . turns . length , 0 ) ;
1331+ } ) ;
1332+
1333+ test ( 'new turn after truncation works correctly' , async function ( ) {
1334+ this . timeout ( 15_000 ) ;
1335+
1336+ const sessionUri = await createAndSubscribeSession ( client , 'test-truncate-resume' ) ;
1337+
1338+ dispatchTurnStarted ( client , sessionUri , 'turn-tr1' , 'hello' , 1 ) ;
1339+ await client . waitForNotification ( n => isActionNotification ( n , 'session/turnComplete' ) && ( getActionEnvelope ( n ) . action as { turnId : string } ) . turnId === 'turn-tr1' ) ;
1340+
1341+ client . clearReceived ( ) ;
1342+ dispatchTurnStarted ( client , sessionUri , 'turn-tr2' , 'hello' , 2 ) ;
1343+ await client . waitForNotification ( n => isActionNotification ( n , 'session/turnComplete' ) && ( getActionEnvelope ( n ) . action as { turnId : string } ) . turnId === 'turn-tr2' ) ;
1344+
1345+ client . clearReceived ( ) ;
1346+
1347+ // Truncate to turn-tr1
1348+ client . notify ( 'dispatchAction' , {
1349+ clientSeq : 3 ,
1350+ action : { type : 'session/truncated' , session : sessionUri , turnId : 'turn-tr1' } ,
1351+ } ) ;
1352+
1353+ await client . waitForNotification ( n => isActionNotification ( n , 'session/truncated' ) ) ;
1354+
1355+ // Send a new turn after truncation
1356+ dispatchTurnStarted ( client , sessionUri , 'turn-tr3' , 'hello' , 4 ) ;
1357+ await client . waitForNotification ( n => isActionNotification ( n , 'session/turnComplete' ) ) ;
1358+
1359+ const snapshot = await client . call < ISubscribeResult > ( 'subscribe' , { resource : sessionUri } ) ;
1360+ const state = snapshot . snapshot . state as ISessionState ;
1361+ assert . strictEqual ( state . turns . length , 2 ) ;
1362+ assert . strictEqual ( state . turns [ 0 ] . id , 'turn-tr1' ) ;
1363+ assert . strictEqual ( state . turns [ 1 ] . id , 'turn-tr3' ) ;
1364+ } ) ;
1365+
1366+ // ---- Fork tests ---------------------------------------------------------
1367+
1368+ test ( 'fork creates a new session with source history' , async function ( ) {
1369+ this . timeout ( 15_000 ) ;
1370+
1371+ const sessionUri = await createAndSubscribeSession ( client , 'test-fork' ) ;
1372+
1373+ // Create two turns
1374+ dispatchTurnStarted ( client , sessionUri , 'turn-f1' , 'hello' , 1 ) ;
1375+ await client . waitForNotification ( n => isActionNotification ( n , 'session/turnComplete' ) && ( getActionEnvelope ( n ) . action as { turnId : string } ) . turnId === 'turn-f1' ) ;
1376+
1377+ client . clearReceived ( ) ;
1378+ dispatchTurnStarted ( client , sessionUri , 'turn-f2' , 'hello' , 2 ) ;
1379+ await client . waitForNotification ( n => isActionNotification ( n , 'session/turnComplete' ) && ( getActionEnvelope ( n ) . action as { turnId : string } ) . turnId === 'turn-f2' ) ;
1380+
1381+ client . clearReceived ( ) ;
1382+
1383+ // Fork at turn-f1 (keep turns up to and including turn-f1)
1384+ const forkedSessionUri = nextSessionUri ( ) ;
1385+ await client . call ( 'createSession' , {
1386+ session : forkedSessionUri ,
1387+ provider : 'mock' ,
1388+ fork : { session : sessionUri , turnId : 'turn-f1' } ,
1389+ } ) ;
1390+
1391+ const addedNotif = await client . waitForNotification ( n =>
1392+ n . method === 'notification' && ( n . params as INotificationBroadcastParams ) . notification . type === 'notify/sessionAdded'
1393+ ) ;
1394+ const addedSession = ( addedNotif . params as INotificationBroadcastParams ) . notification as ISessionAddedNotification ;
1395+
1396+ // Subscribe — forked session should have 1 turn (from the protocol state
1397+ // populated during createSession with fork params).
1398+ const snapshot = await client . call < ISubscribeResult > ( 'subscribe' , { resource : addedSession . summary . resource } ) ;
1399+ const state = snapshot . snapshot . state as ISessionState ;
1400+ assert . strictEqual ( state . lifecycle , 'ready' ) ;
1401+ assert . strictEqual ( state . turns . length , 1 , 'forked session should have 1 turn' ) ;
1402+
1403+ // Source session should be unaffected
1404+ const sourceSnapshot = await client . call < ISubscribeResult > ( 'subscribe' , { resource : sessionUri } ) ;
1405+ const sourceState = sourceSnapshot . snapshot . state as ISessionState ;
1406+ assert . strictEqual ( sourceState . turns . length , 2 ) ;
1407+ } ) ;
1408+
1409+ test ( 'fork with invalid turn ID returns error' , async function ( ) {
1410+ this . timeout ( 10_000 ) ;
1411+
1412+ const sessionUri = await createAndSubscribeSession ( client , 'test-fork-invalid' ) ;
1413+
1414+ let gotError = false ;
1415+ try {
1416+ await client . call ( 'createSession' , {
1417+ session : nextSessionUri ( ) ,
1418+ provider : 'mock' ,
1419+ fork : { session : sessionUri , turnId : 'nonexistent-turn' } ,
1420+ } ) ;
1421+ } catch {
1422+ gotError = true ;
1423+ }
1424+ assert . ok ( gotError , 'should get error for invalid fork turn ID' ) ;
1425+ } ) ;
1426+
1427+ test ( 'fork with invalid source session returns error' , async function ( ) {
1428+ this . timeout ( 10_000 ) ;
1429+
1430+ await client . call ( 'initialize' , { protocolVersion : PROTOCOL_VERSION , clientId : 'test-fork-no-source' } ) ;
1431+
1432+ let gotError = false ;
1433+ try {
1434+ await client . call ( 'createSession' , {
1435+ session : nextSessionUri ( ) ,
1436+ provider : 'mock' ,
1437+ fork : { session : 'mock://nonexistent-session' , turnId : 'turn-1' } ,
1438+ } ) ;
1439+ } catch {
1440+ gotError = true ;
1441+ }
1442+ assert . ok ( gotError , 'should get error for invalid fork source session' ) ;
1443+ } ) ;
12731444} ) ;
0 commit comments