@@ -4,7 +4,14 @@ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
44import { existsSync , mkdirSync , readFileSync , rmSync , writeFileSync } from "node:fs" ;
55import { homedir } from "node:os" ;
66import { join } from "node:path" ;
7- import { filterHistory , getHistoryPath , getSpawnDir , loadHistory , saveSpawnRecord } from "../history.js" ;
7+ import {
8+ filterHistory ,
9+ getHistoryPath ,
10+ getSpawnDir ,
11+ HISTORY_SCHEMA_VERSION ,
12+ loadHistory ,
13+ saveSpawnRecord ,
14+ } from "../history.js" ;
815
916describe ( "history" , ( ) => {
1017 let testDir : string ;
@@ -185,6 +192,55 @@ describe("history", () => {
185192 writeFileSync ( join ( testDir , "history.json" ) , "" ) ;
186193 expect ( loadHistory ( ) ) . toEqual ( [ ] ) ;
187194 } ) ;
195+
196+ it ( "loads v1 format: { version: 1, records: [...] }" , ( ) => {
197+ const records : SpawnRecord [ ] = [
198+ {
199+ agent : "claude" ,
200+ cloud : "sprite" ,
201+ timestamp : "2026-01-01T00:00:00.000Z" ,
202+ } ,
203+ ] ;
204+ writeFileSync (
205+ join ( testDir , "history.json" ) ,
206+ JSON . stringify ( {
207+ version : 1 ,
208+ records,
209+ } ) ,
210+ ) ;
211+ expect ( loadHistory ( ) ) . toEqual ( records ) ;
212+ } ) ;
213+
214+ it ( "returns empty array for v1 format with unknown version" , ( ) => {
215+ const records : SpawnRecord [ ] = [
216+ {
217+ agent : "claude" ,
218+ cloud : "sprite" ,
219+ timestamp : "2026-01-01T00:00:00.000Z" ,
220+ } ,
221+ ] ;
222+ writeFileSync (
223+ join ( testDir , "history.json" ) ,
224+ JSON . stringify ( {
225+ version : 99 ,
226+ records,
227+ } ) ,
228+ ) ;
229+ // Unknown version is not a recognized format; treated as invalid non-array
230+ expect ( loadHistory ( ) ) . toEqual ( [ ] ) ;
231+ } ) ;
232+
233+ it ( "loads v0 format: bare array (backward compatibility)" , ( ) => {
234+ const records : SpawnRecord [ ] = [
235+ {
236+ agent : "claude" ,
237+ cloud : "sprite" ,
238+ timestamp : "2026-01-01T00:00:00.000Z" ,
239+ } ,
240+ ] ;
241+ writeFileSync ( join ( testDir , "history.json" ) , JSON . stringify ( records ) ) ;
242+ expect ( loadHistory ( ) ) . toEqual ( records ) ;
243+ } ) ;
188244 } ) ;
189245
190246 // ── saveSpawnRecord ─────────────────────────────────────────────────────
@@ -202,8 +258,9 @@ describe("history", () => {
202258
203259 expect ( existsSync ( join ( nestedDir , "history.json" ) ) ) . toBe ( true ) ;
204260 const data = JSON . parse ( readFileSync ( join ( nestedDir , "history.json" ) , "utf-8" ) ) ;
205- expect ( data ) . toHaveLength ( 1 ) ;
206- expect ( data [ 0 ] . agent ) . toBe ( "claude" ) ;
261+ expect ( data . version ) . toBe ( HISTORY_SCHEMA_VERSION ) ;
262+ expect ( data . records ) . toHaveLength ( 1 ) ;
263+ expect ( data . records [ 0 ] . agent ) . toBe ( "claude" ) ;
207264
208265 // Clean up
209266 rmSync ( join ( homedir ( ) , ".spawn-test" ) , {
@@ -229,9 +286,10 @@ describe("history", () => {
229286 } ) ;
230287
231288 const data = JSON . parse ( readFileSync ( join ( testDir , "history.json" ) , "utf-8" ) ) ;
232- expect ( data ) . toHaveLength ( 2 ) ;
233- expect ( data [ 0 ] . agent ) . toBe ( "claude" ) ;
234- expect ( data [ 1 ] . agent ) . toBe ( "codex" ) ;
289+ expect ( data . version ) . toBe ( HISTORY_SCHEMA_VERSION ) ;
290+ expect ( data . records ) . toHaveLength ( 2 ) ;
291+ expect ( data . records [ 0 ] . agent ) . toBe ( "claude" ) ;
292+ expect ( data . records [ 1 ] . agent ) . toBe ( "codex" ) ;
235293 } ) ;
236294
237295 it ( "saves record with prompt field" , ( ) => {
@@ -243,7 +301,7 @@ describe("history", () => {
243301 } ) ;
244302
245303 const data = JSON . parse ( readFileSync ( join ( testDir , "history.json" ) , "utf-8" ) ) ;
246- expect ( data [ 0 ] . prompt ) . toBe ( "Fix all linter errors" ) ;
304+ expect ( data . records [ 0 ] . prompt ) . toBe ( "Fix all linter errors" ) ;
247305 } ) ;
248306
249307 it ( "saves record without prompt field" , ( ) => {
@@ -254,7 +312,7 @@ describe("history", () => {
254312 } ) ;
255313
256314 const data = JSON . parse ( readFileSync ( join ( testDir , "history.json" ) , "utf-8" ) ) ;
257- expect ( data [ 0 ] . prompt ) . toBeUndefined ( ) ;
315+ expect ( data . records [ 0 ] . prompt ) . toBeUndefined ( ) ;
258316 } ) ;
259317
260318 it ( "writes pretty-printed JSON with trailing newline" , ( ) => {
@@ -281,9 +339,47 @@ describe("history", () => {
281339 }
282340
283341 const data = JSON . parse ( readFileSync ( join ( testDir , "history.json" ) , "utf-8" ) ) ;
284- expect ( data ) . toHaveLength ( 5 ) ;
285- expect ( data [ 0 ] . agent ) . toBe ( "agent-0" ) ;
286- expect ( data [ 4 ] . agent ) . toBe ( "agent-4" ) ;
342+ expect ( data . version ) . toBe ( HISTORY_SCHEMA_VERSION ) ;
343+ expect ( data . records ) . toHaveLength ( 5 ) ;
344+ expect ( data . records [ 0 ] . agent ) . toBe ( "agent-0" ) ;
345+ expect ( data . records [ 4 ] . agent ) . toBe ( "agent-4" ) ;
346+ } ) ;
347+
348+ it ( "writes v1 format with version and records fields" , ( ) => {
349+ saveSpawnRecord ( {
350+ agent : "claude" ,
351+ cloud : "sprite" ,
352+ timestamp : "2026-01-01T00:00:00.000Z" ,
353+ } ) ;
354+
355+ const data = JSON . parse ( readFileSync ( join ( testDir , "history.json" ) , "utf-8" ) ) ;
356+ expect ( data . version ) . toBe ( HISTORY_SCHEMA_VERSION ) ;
357+ expect ( Array . isArray ( data . records ) ) . toBe ( true ) ;
358+ } ) ;
359+
360+ it ( "migrates v0 bare array to v1 format on next save" , ( ) => {
361+ const existing : SpawnRecord [ ] = [
362+ {
363+ agent : "claude" ,
364+ cloud : "sprite" ,
365+ timestamp : "2026-01-01T00:00:00.000Z" ,
366+ } ,
367+ ] ;
368+ // Write v0 bare array
369+ writeFileSync ( join ( testDir , "history.json" ) , JSON . stringify ( existing ) ) ;
370+
371+ // Trigger a write via saveSpawnRecord
372+ saveSpawnRecord ( {
373+ agent : "codex" ,
374+ cloud : "hetzner" ,
375+ timestamp : "2026-01-02T00:00:00.000Z" ,
376+ } ) ;
377+
378+ const data = JSON . parse ( readFileSync ( join ( testDir , "history.json" ) , "utf-8" ) ) ;
379+ expect ( data . version ) . toBe ( HISTORY_SCHEMA_VERSION ) ;
380+ expect ( data . records ) . toHaveLength ( 2 ) ;
381+ expect ( data . records [ 0 ] . agent ) . toBe ( "claude" ) ;
382+ expect ( data . records [ 1 ] . agent ) . toBe ( "codex" ) ;
287383 } ) ;
288384
289385 it ( "recovers from corrupted existing history file" , ( ) => {
@@ -297,8 +393,9 @@ describe("history", () => {
297393
298394 // loadHistory returns [] for corrupted files, so saveSpawnRecord starts fresh
299395 const data = JSON . parse ( readFileSync ( join ( testDir , "history.json" ) , "utf-8" ) ) ;
300- expect ( data ) . toHaveLength ( 1 ) ;
301- expect ( data [ 0 ] . agent ) . toBe ( "claude" ) ;
396+ expect ( data . version ) . toBe ( HISTORY_SCHEMA_VERSION ) ;
397+ expect ( data . records ) . toHaveLength ( 1 ) ;
398+ expect ( data . records [ 0 ] . agent ) . toBe ( "claude" ) ;
302399 } ) ;
303400 } ) ;
304401
0 commit comments