@@ -999,6 +999,68 @@ test("createSession initializes file-history repo and session branch", async (t)
999999 ) ;
10001000} ) ;
10011001
1002+ test ( "createSession initializes an empty file-history manifest without scanning existing files" , async ( t ) => {
1003+ if ( ! hasGit ( ) ) {
1004+ t . skip ( "git is not available" ) ;
1005+ return ;
1006+ }
1007+
1008+ const workspace = createTempDir ( "deepcode-file-history-empty-init-workspace-" ) ;
1009+ const home = createTempDir ( "deepcode-file-history-empty-init-home-" ) ;
1010+ setHomeDir ( home ) ;
1011+ fs . writeFileSync ( path . join ( workspace , "unrelated.txt" ) , "keep me\n" , "utf8" ) ;
1012+ fs . mkdirSync ( path . join ( workspace , "nested" ) ) ;
1013+ fs . writeFileSync ( path . join ( workspace , "nested" , "another.txt" ) , "also keep me\n" , "utf8" ) ;
1014+
1015+ const manager = createSessionManager ( workspace , "machine-id-file-history-empty-init" ) ;
1016+ ( manager as any ) . activateSession = async ( ) => { } ;
1017+
1018+ const sessionId = await manager . createSession ( { text : "first prompt" } ) ;
1019+ const userMessage = manager . listSessionMessages ( sessionId ) . find ( ( message ) => message . role === "user" ) ;
1020+ assert . ok ( userMessage ?. checkpointHash ) ;
1021+
1022+ const manifest = readFileHistoryManifest ( home , workspace , userMessage . checkpointHash ) ;
1023+ assert . deepEqual ( manifest . files , { } ) ;
1024+ } ) ;
1025+
1026+ test ( "replySession snapshots manual edits to tracked files before appending the user prompt" , async ( t ) => {
1027+ if ( ! hasGit ( ) ) {
1028+ t . skip ( "git is not available" ) ;
1029+ return ;
1030+ }
1031+
1032+ const workspace = createTempDir ( "deepcode-prompt-checkpoint-manual-edit-workspace-" ) ;
1033+ const home = createTempDir ( "deepcode-prompt-checkpoint-manual-edit-home-" ) ;
1034+ setHomeDir ( home ) ;
1035+
1036+ const filePath = path . join ( workspace , "hello_world.py" ) ;
1037+ const manager = createSessionManager ( workspace , "machine-id-prompt-checkpoint-manual-edit" ) ;
1038+ ( manager as any ) . activateSession = async ( ) => { } ;
1039+
1040+ const sessionId = await manager . createSession ( { text : "create hello world" } ) ;
1041+ const gitDir = getFileHistoryGitDir ( home , workspace ) ;
1042+ const fileHistory = new GitFileHistory ( workspace , gitDir ) ;
1043+
1044+ fs . writeFileSync ( filePath , 'print("Hello, World!")\n' , "utf8" ) ;
1045+ assert . ok ( fileHistory . recordCheckpoint ( sessionId , [ filePath ] , "created hello world" ) ) ;
1046+
1047+ const manualEdit = 'if name == main:\n print("Hello, World!")\n' ;
1048+ fs . writeFileSync ( filePath , manualEdit , "utf8" ) ;
1049+ await manager . replySession ( sessionId , { text : "I manually edited @hello_world.py, note it" } ) ;
1050+ const manualEditUserMessage = manager
1051+ . listSessionMessages ( sessionId )
1052+ . filter ( ( message ) => message . role === "user" )
1053+ . at ( - 1 ) ;
1054+ assert . ok ( manualEditUserMessage ?. checkpointHash ) ;
1055+
1056+ fs . writeFileSync ( filePath , 'if __name__ == "__main__":\n print("Hello, World!")\n' , "utf8" ) ;
1057+ assert . ok ( fileHistory . recordCheckpoint ( sessionId , [ filePath ] , "fixed hello world" ) ) ;
1058+
1059+ manager . restoreSessionCode ( sessionId , manualEditUserMessage . id ) ;
1060+
1061+ assert . equal ( fs . readFileSync ( filePath , "utf8" ) , manualEdit ) ;
1062+ } ) ;
1063+
10021064test ( "Write tool advances file-history while preserving the user prompt checkpoint" , async ( t ) => {
10031065 if ( ! hasGit ( ) ) {
10041066 t . skip ( "git is not available" ) ;
@@ -1191,6 +1253,8 @@ test("restoreSessionCode restores project files from the recorded Git checkpoint
11911253 const manager = createSessionManager ( workspace , "machine-id-undo-code" ) ;
11921254 const sessionId = "session-code-restore" ;
11931255 const checkpointHash = createFileHistoryCommit ( home , workspace , sessionId , { "tracked.txt" : "before\n" } ) ;
1256+ const fileHistory = new GitFileHistory ( workspace , getFileHistoryGitDir ( home , workspace ) ) ;
1257+ assert . ok ( fileHistory . recordCheckpoint ( sessionId , [ path . join ( workspace , "new.txt" ) ] , "pre-create new.txt" ) ) ;
11941258 createFileHistoryCommit ( home , workspace , sessionId , { "tracked.txt" : "after\n" , "new.txt" : "remove me\n" } ) ;
11951259 fs . writeFileSync ( path . join ( workspace , "tracked.txt" ) , "after\n" , "utf8" ) ;
11961260 fs . writeFileSync ( path . join ( workspace , "new.txt" ) , "remove me\n" , "utf8" ) ;
@@ -1206,6 +1270,91 @@ test("restoreSessionCode restores project files from the recorded Git checkpoint
12061270 assert . equal ( fs . existsSync ( path . join ( workspace , "new.txt" ) ) , false ) ;
12071271} ) ;
12081272
1273+ test ( "restoreSessionCode preserves files that predate their first tracked mutation" , async ( t ) => {
1274+ if ( ! hasGit ( ) ) {
1275+ t . skip ( "git is not available" ) ;
1276+ return ;
1277+ }
1278+
1279+ const workspace = createTempDir ( "deepcode-undo-preexisting-files-workspace-" ) ;
1280+ const home = createTempDir ( "deepcode-undo-preexisting-files-home-" ) ;
1281+ setHomeDir ( home ) ;
1282+
1283+ const readmePath = path . join ( workspace , "README.md" ) ;
1284+ const readmeEnPath = path . join ( workspace , "README-en.md" ) ;
1285+ const readmeZhPath = path . join ( workspace , "README-zh_CN.md" ) ;
1286+ fs . writeFileSync ( readmePath , "这是一个hello world演示项目\n" , "utf8" ) ;
1287+ fs . writeFileSync ( readmeEnPath , "This is a hello world demo project.\n" , "utf8" ) ;
1288+ fs . writeFileSync ( readmeZhPath , "" , "utf8" ) ;
1289+
1290+ const manager = createSessionManager ( workspace , "machine-id-undo-preexisting-files" ) ;
1291+ const sessionId = "session-undo-preexisting-files" ;
1292+ const gitDir = getFileHistoryGitDir ( home , workspace ) ;
1293+ const fileHistory = new GitFileHistory ( workspace , gitDir ) ;
1294+ fileHistory . ensureSession ( sessionId ) ;
1295+
1296+ const targetCheckpoint = fileHistory . recordCheckpoint (
1297+ sessionId ,
1298+ [ readmePath , readmeEnPath ] ,
1299+ "checkpoint before syncing all readmes"
1300+ ) ;
1301+ assert . ok ( targetCheckpoint ) ;
1302+
1303+ assert . ok ( fileHistory . recordCheckpoint ( sessionId , [ readmeZhPath ] , "pre-sync zh readme" ) ) ;
1304+ fs . writeFileSync ( readmePath , "Synced readme\n" , "utf8" ) ;
1305+ fs . writeFileSync ( readmeEnPath , "Synced readme\n" , "utf8" ) ;
1306+ fs . writeFileSync ( readmeZhPath , "Synced readme\n" , "utf8" ) ;
1307+ assert . ok ( fileHistory . recordCheckpoint ( sessionId , [ readmePath , readmeEnPath , readmeZhPath ] , "synced readmes" ) ) ;
1308+
1309+ ( manager as any ) . appendSessionMessage ( sessionId , {
1310+ ...buildTestMessage ( "user-with-readme-checkpoint" , sessionId , "user" , "sync README*.md" ) ,
1311+ checkpointHash : targetCheckpoint ,
1312+ } ) ;
1313+
1314+ manager . restoreSessionCode ( sessionId , "user-with-readme-checkpoint" ) ;
1315+
1316+ assert . equal ( fs . readFileSync ( readmePath , "utf8" ) , "这是一个hello world演示项目\n" ) ;
1317+ assert . equal ( fs . readFileSync ( readmeEnPath , "utf8" ) , "This is a hello world demo project.\n" ) ;
1318+ assert . equal ( fs . readFileSync ( readmeZhPath , "utf8" ) , "" ) ;
1319+ } ) ;
1320+
1321+ test ( "restoreSessionCode restores deleted tracked files and leaves unrelated files alone" , async ( t ) => {
1322+ if ( ! hasGit ( ) ) {
1323+ t . skip ( "git is not available" ) ;
1324+ return ;
1325+ }
1326+
1327+ const workspace = createTempDir ( "deepcode-undo-deleted-files-workspace-" ) ;
1328+ const home = createTempDir ( "deepcode-undo-deleted-files-home-" ) ;
1329+ setHomeDir ( home ) ;
1330+
1331+ const trackedPath = path . join ( workspace , "tracked.txt" ) ;
1332+ const unrelatedPath = path . join ( workspace , "unrelated.txt" ) ;
1333+ fs . writeFileSync ( trackedPath , "before delete\n" , "utf8" ) ;
1334+ fs . writeFileSync ( unrelatedPath , "do not touch\n" , "utf8" ) ;
1335+
1336+ const manager = createSessionManager ( workspace , "machine-id-undo-deleted-files" ) ;
1337+ const sessionId = "session-undo-deleted-files" ;
1338+ const gitDir = getFileHistoryGitDir ( home , workspace ) ;
1339+ const fileHistory = new GitFileHistory ( workspace , gitDir ) ;
1340+ fileHistory . ensureSession ( sessionId ) ;
1341+ const targetCheckpoint = fileHistory . recordCheckpoint ( sessionId , [ trackedPath ] , "before delete" ) ;
1342+ assert . ok ( targetCheckpoint ) ;
1343+
1344+ fs . unlinkSync ( trackedPath ) ;
1345+ assert . ok ( fileHistory . recordCheckpoint ( sessionId , [ trackedPath ] , "after delete" ) ) ;
1346+
1347+ ( manager as any ) . appendSessionMessage ( sessionId , {
1348+ ...buildTestMessage ( "user-before-delete" , sessionId , "user" , "restore deleted file" ) ,
1349+ checkpointHash : targetCheckpoint ,
1350+ } ) ;
1351+
1352+ manager . restoreSessionCode ( sessionId , "user-before-delete" ) ;
1353+
1354+ assert . equal ( fs . readFileSync ( trackedPath , "utf8" ) , "before delete\n" ) ;
1355+ assert . equal ( fs . readFileSync ( unrelatedPath , "utf8" ) , "do not touch\n" ) ;
1356+ } ) ;
1357+
12091358test ( "replySession /continue runs trailing pending tool calls before requesting another response" , async ( ) => {
12101359 const workspace = createTempDir ( "deepcode-continue-tool-workspace-" ) ;
12111360 const home = createTempDir ( "deepcode-continue-tool-home-" ) ;
@@ -2617,6 +2766,18 @@ function createFileHistoryCommit(
26172766 return commitHash ;
26182767}
26192768
2769+ function getFileHistoryGitDir ( home : string , workspace : string ) : string {
2770+ const projectCode = workspace . replace ( / [ \\ / ] / g, "-" ) . replace ( / : / g, "" ) ;
2771+ return path . join ( home , ".deepcode" , "projects" , projectCode , "file-history" , ".git" ) ;
2772+ }
2773+
2774+ function readFileHistoryManifest ( home : string , workspace : string , checkpointHash : string ) : any {
2775+ const gitDir = getFileHistoryGitDir ( home , workspace ) ;
2776+ return JSON . parse (
2777+ runFileHistoryGit ( gitDir , workspace , [ "cat-file" , "blob" , `${ checkpointHash } :.deepcode-file-history.json` ] )
2778+ ) ;
2779+ }
2780+
26202781function runFileHistoryGit (
26212782 gitDir : string ,
26222783 workspace : string ,
0 commit comments