1module history23import os4import src.snapshot5import src.storage678fn testsuite_begin() {9 os.setenv('HOME', os.join_path(os.temp_dir(), 'koh3-test-home-${os.getpid()}'), true)10}1112fn temp_repo(name string) !(storage.Store, string) {13 dir := os.join_path(os.temp_dir(), 'koh3_hist_${os.getpid()}_${name}')14 os.rmdir_all(dir) or {}15 os.mkdir_all(dir, os.MkdirParams{})!16 store := storage.create(dir)!17 return store, dir18}1920fn write_file(root string, rel string, text string) ! {21 full := os.join_path(root, rel)22 os.mkdir_all(os.dir(full), os.MkdirParams{})!23 os.write_file(full, text)!24}2526fn test_walk_then_save() {27 mut store, dir := temp_repo('save')!28 defer {29 store.close()30 os.rmdir_all(dir) or {}31 }32 write_file(dir, 'a.txt', 'hello\n')!33 write_file(dir, 'sub/b.txt', 'world\n')!34 entries := snapshot.walk(store, dir, snapshot.Ignore{})!35 assert entries.len == 23637 save := save_worktree(store, dir, snapshot.Ignore{}, SaveInput{38 message: 'first save'39 })!40 assert save.id.len == 1241 assert save.parent == ''42 assert head_id(store) == save.id4344 files := files_of(store, save.id)!45 assert files.len == 246 assert files[0].path == 'a.txt'47 assert files[1].path == 'sub/b.txt'4849 50 if _ := save_worktree(store, dir, snapshot.Ignore{}, SaveInput{ message: 'again' }) {51 assert false, 'a save with no changes should refuse'52 } else {53 assert err.msg() == nothing_changed54 }55 second := save_worktree(store, dir, snapshot.Ignore{}, SaveInput{56 message: 'checked anyway'57 is_checkpoint: true58 })!59 assert second.parent == save.id60 assert second.is_checkpoint61}6263fn test_going_back_is_recorded_not_rewound() {64 mut store, dir := temp_repo('back')!65 defer {66 store.close()67 os.rmdir_all(dir) or {}68 }69 write_file(dir, 'a.txt', 'one\n')!70 first := save_worktree(store, dir, snapshot.Ignore{}, SaveInput{ message: 'one' })!71 write_file(dir, 'a.txt', 'two\n')!72 second := save_worktree(store, dir, snapshot.Ignore{}, SaveInput{ message: 'two' })!7374 75 assert effective(store)!.id == second.id76 first_files := files_of(store, first.id)!77 restore := save_tree(store, '', first_files, SaveInput{78 message: 'undo: ${first.id}'79 restores: first.id80 })!81 assert restore.parent == second.id82 assert restore.restores == first.id83 assert effective(store)!.id == first.id84 85 86 if _ := step_back(store, effective(store)!.id, 1) {87 assert false, 'there is nothing before the first save'88 } else {89 assert err.msg().contains('nothing before')90 }9192 93 assert same_files(store, head_id(store), first_files)!94}9596fn same_files(store &storage.Store, id string, expected []snapshot.FileEntry) !bool {97 actual := files_of(store, id)!98 if actual.len != expected.len {99 return false100 }101 for i in 0 .. actual.len {102 if actual[i].path != expected[i].path || actual[i].hash != expected[i].hash {103 return false104 }105 }106 return true107}108109fn test_resolve_accepts_a_unique_prefix() {110 mut store, dir := temp_repo('resolve')!111 defer {112 store.close()113 os.rmdir_all(dir) or {}114 }115 write_file(dir, 'a.txt', 'one\n')!116 first := save_worktree(store, dir, snapshot.Ignore{}, SaveInput{ message: 'one' })!117 assert resolve(store, first.id[..6])!.id == first.id118 if _ := resolve(store, 'abc') {119 assert false, 'a prefix shorter than six characters should be refused'120 } else {121 assert err.msg().contains('at least 6')122 }123 if _ := resolve(store, 'zzzzzz') {124 assert false, 'an unknown prefix should be refused'125 } else {126 assert err.msg().contains('no save starts with')127 }128}129130131132fn test_checkpoint_and_session_lookups_stay_on_the_line() {133 mut store, dir := temp_repo('on_line')!134 defer {135 store.close()136 os.rmdir_all(dir) or {}137 }138 write_file(dir, 'a.txt', 'one\n')!139 kept := save_worktree(store, dir, snapshot.Ignore{}, SaveInput{140 message: 'on the line'141 session: 'work'142 is_checkpoint: true143 })!144 write_file(dir, 'a.txt', 'two\n')!145 save_worktree(store, dir, snapshot.Ignore{}, SaveInput{146 message: 'replayed away later'147 session: 'work'148 is_checkpoint: true149 })!150 151 set_head(store, kept.id)!152 assert latest_checkpoint(store)?.id == kept.id153 assert session_saves(store, 'work')!.map(it.id) == [kept.id]154}155156157158159160161fn test_a_save_whose_walk_went_stale_reads_the_tree_again() {162 mut store, dir := temp_repo('stale_walk')!163 defer {164 store.close()165 os.rmdir_all(dir) or {}166 }167 write_file(dir, 'a.txt', 'one\n')!168 first := save_worktree(store, dir, snapshot.Ignore{}, SaveInput{169 message: 'first'170 })!171 write_file(dir, 'b.txt', 'mine\n')!172 stale := snapshot.walk(store, dir, snapshot.Ignore{})!173 174 write_file(dir, 'a.txt', 'two\n')!175 other := save_tree(store, dir,176 snapshot.walk(store, dir, snapshot.Ignore{})!.filter(it.path == 'a.txt'), SaveInput{177 message: 'somebody else'178 })!179 store.begin()!180 saved := save_walk(store, dir, SaveInput{181 message: 'mine'182 }, first.id, stale)!183 store.end()!184 assert saved.parent == other.id185 a := files_of(store, saved.id)!.filter(it.path == 'a.txt')186 assert a.len == 1 && a[0].hash == storage.hash_hex('two\n'.bytes())187188 189 stale_again := snapshot.walk(store, dir, snapshot.Ignore{})!190 snapshot.mark_restore(store)!191 moved := save_tree(store, '', files_of(store, first.id)!, SaveInput{192 message: 'undo: ${first.id}'193 restores: first.id194 })!195 store.begin()!196 if _ := save_walk(store, dir, SaveInput{197 message: 'on a half tree'198 }, saved.id, stale_again)199 {200 assert false, 'a save must not land on a restore still writing files'201 } else {202 assert err.msg() == snapshot.restore_unfinished203 }204 store.rollback()205 assert head_id(store) == moved.id206}207208209210fn test_a_save_beaten_to_the_same_tree_names_the_save_that_holds_it() {211 mut store, dir := temp_repo('beaten')!212 defer {213 store.close()214 os.rmdir_all(dir) or {}215 }216 write_file(dir, 'a.txt', 'one\n')!217 first := save_worktree(store, dir, snapshot.Ignore{}, SaveInput{218 message: 'first'219 })!220 write_file(dir, 'b.txt', 'both agents\n')!221 walked := snapshot.walk(store, dir, snapshot.Ignore{})!222 winner := save_worktree(store, dir, snapshot.Ignore{}, SaveInput{223 message: 'agent one'224 })!225 store.begin()!226 if _ := save_walk(store, dir, SaveInput{227 message: 'agent two'228 }, first.id, walked)229 {230 assert false, 'the same tree must not be saved twice'231 } else {232 assert err.msg().contains('[${winner.id}] "agent one"')233 }234 store.rollback()235}236237238239fn test_a_save_after_a_restore_moved_head_waits_for_its_files() {240 mut store, dir := temp_repo('after_restore')!241 defer {242 store.close()243 os.rmdir_all(dir) or {}244 }245 write_file(dir, 'a.txt', 'one\n')!246 first := save_worktree(store, dir, snapshot.Ignore{}, SaveInput{247 message: 'first'248 })!249 write_file(dir, 'a.txt', 'two\n')!250 save_worktree(store, dir, snapshot.Ignore{}, SaveInput{251 message: 'second'252 })!253 snapshot.mark_restore(store)!254 moved := save_tree(store, '', files_of(store, first.id)!, SaveInput{255 message: 'undo: ${first.id}'256 restores: first.id257 })!258 259 if _ := save_worktree(store, dir, snapshot.Ignore{}, SaveInput{260 message: 'on a half tree'261 })262 {263 assert false, 'a save must not land while a restore writes its files'264 } else {265 assert err.msg() == snapshot.restore_unfinished266 }267 assert head_id(store) == moved.id268}269270271272fn test_a_stale_walk_is_read_again_with_the_current_ignore_rules() {273 mut store, dir := temp_repo('stale_rules')!274 defer {275 store.close()276 os.rmdir_all(dir) or {}277 }278 write_file(dir, 'a.txt', 'one\n')!279 first := save_worktree(store, dir, snapshot.Ignore{}, SaveInput{280 message: 'first'281 })!282 write_file(dir, 'secret.env', 'key\n')!283 stale := snapshot.walk(store, dir, snapshot.Ignore{})!284 write_file(dir, '.kohignore', 'secret.env\n')!285 save_tree(store, dir, snapshot.walk(store, dir, snapshot.load(dir))!, SaveInput{286 message: 'ignore the secret'287 })!288 write_file(dir, 'a.txt', 'two\n')!289 store.begin()!290 saved := save_walk(store, dir, SaveInput{291 message: 'mine'292 }, first.id, stale)!293 store.end()!294 assert !files_of(store, saved.id)!.any(it.path == 'secret.env')295}296297298299fn test_all_saves_keeps_same_second_saves_newest_first() {300 mut store, dir := temp_repo('same_second')!301 defer {302 store.close()303 os.rmdir_all(dir) or {}304 }305 mut made := []string{}306 for i in 0 .. 6 {307 write_file(dir, 'a.txt', 'version ${i}\n')!308 made << save_worktree(store, dir, snapshot.Ignore{}, SaveInput{309 message: 'save ${i}'310 created_at: 1790000000311 })!.id312 }313 made.reverse_in_place()314 assert all_saves(store)!.map(it.id) == made315}