12345module sync67import os8import src.events9import src.history10import src.remote11import src.snapshot12import src.storage13141516fn machine(name string) !string {17 dir := os.join_path(os.temp_dir(), 'koh3_sync_${os.getpid()}_${name}', 'proj')18 os.rmdir_all(os.dir(dir)) or {}19 os.mkdir_all(dir, os.MkdirParams{})!20 return dir21}2223fn scratch(name string) !string {24 dir := os.join_path(os.temp_dir(), 'koh3_sync_${os.getpid()}_${name}')25 os.rmdir_all(dir) or {}26 os.mkdir_all(dir, os.MkdirParams{})!27 return dir28}2930fn open_repo(dir string) !storage.Store {31 mut store := storage.create(dir)!32 os.write_file(os.join_path(dir, '.kohignore'), '*.log\n')!33 return store34}3536fn put(root string, rel string, text string) ! {37 full := os.join_path(root, rel)38 os.mkdir_all(os.dir(full), os.MkdirParams{})!39 os.write_file(full, text)!40}4142fn commit(store &storage.Store, message string) !history.Save {43 return history.commit_worktree(store, store.root, snapshot.load(store.root), history.SaveInput{44 message: message45 })!46}4748fn offer_now(store &storage.Store, mut backend remote.Backend) !OfferReport {49 return offer(store, mut backend)!50}5152fn steal_now(store &storage.Store, mut backend remote.Backend) !StealReport {53 return steal(store, mut backend, StealOptions{54 theirs: true55 })!56}5758fn test_first_offer_publishes_the_whole_line() {59 base := scratch('first')!60 defer {61 os.rmdir_all(base) or {}62 }63 mut a := open_repo(machine('first_a')!)!64 defer {65 a.close()66 }67 put(a.root, 'a.txt', 'one\n')!68 commit(a, 'first')!69 put(a.root, 'a.txt', 'two\n')!70 commit(a, 'second')!71 mut backend := remote.new_folder(os.join_path(base, 'remote'))!7273 report := offer_now(a, mut backend)!74 assert report.repo == 'proj'75 assert report.saves == 276 assert report.objects >= 277 assert backend.get(head_key('proj'))!.found78 assert backend.list('proj/saves')!.len == 27980 81 again := offer_now(a, mut backend)!82 assert again.saves == 083}8485fn test_the_repo_name_and_offered_head_are_remembered() {86 base := scratch('remember')!87 defer {88 os.rmdir_all(base) or {}89 }90 mut a := open_repo(machine('remember_a')!)!91 defer {92 a.close()93 }94 put(a.root, 'a.txt', 'one\n')!95 commit(a, 'first')!96 mut backend := remote.new_folder(os.join_path(base, 'remote'))!97 offer_now(a, mut backend)!98 assert a.state('remote_repo') or { '' } == 'proj'99 assert a.state('remote_head') or { '' } == history.head(a)!.id100101 102 103 put(a.root, 'a.txt', 'two\n')!104 commit(a, 'second')!105 offer_now(a, mut backend)!106 assert a.state('remote_head') or { '' } == history.head(a)!.id107}108109fn test_steal_fast_forwards_a_machine_that_has_nothing_of_its_own() {110 base := scratch('forward')!111 defer {112 os.rmdir_all(base) or {}113 }114 mut backend := remote.new_folder(os.join_path(base, 'remote'))!115 mut a := open_repo(machine('fwd_a')!)!116 defer {117 a.close()118 }119 put(a.root, 'a.txt', 'from A\n')!120 commit(a, 'A writes')!121 offer_now(a, mut backend)!122123 124 mut b := open_repo(machine('fwd_b')!)!125 defer {126 b.close()127 }128 fetched := fetch_repo(mut backend, 'proj')!129 b.begin()!130 write_fetched(b, fetched)!131 history.set_head(b, fetched.head)!132 b.commit()!133 snapshot.restore_files(b, b.root, snapshot.load(b.root), history.files_of(b, fetched.head)!, [])!134 assert os.read_file(os.join_path(b.root, 'a.txt'))! == 'from A\n'135 assert history.head(b)!.id == fetched.head136}137138139140fn test_steal_while_only_ahead_leaves_the_line_alone() {141 base := scratch('ahead')!142 defer {143 os.rmdir_all(base) or {}144 }145 mut backend := remote.new_folder(os.join_path(base, 'remote'))!146 mut a := open_repo(machine('ahead_a')!)!147 defer {148 a.close()149 }150 put(a.root, 'a.txt', 'one\n')!151 commit(a, 'offered')!152 offer_now(a, mut backend)!153 put(a.root, 'a.txt', 'two\n')!154 ahead := commit(a, 'not offered yet')!155 report := steal_now(a, mut backend)!156 assert report.replayed == 0157 assert history.head(a)!.id == ahead.id158 assert history.all_saves(a)!.len == 2159 offer_now(a, mut backend)!160 assert backend.get(head_key('proj'))!.body.bytestr() == ahead.id161}162163164165fn test_a_clone_imports_parents_before_children() {166 base := scratch('clone_order')!167 defer {168 os.rmdir_all(base) or {}169 }170 mut backend := remote.new_folder(os.join_path(base, 'remote'))!171 mut a := open_repo(machine('order_a')!)!172 defer {173 a.close()174 }175 put(a.root, 'a.txt', 'one\n')!176 parent := commit(a, 'parent')!177 child := history.commit_files(a, '', history.files_of(a, parent.id)!, history.SaveInput{178 message: 'replayed, older than its parent'179 created_at: parent.created_at - 3600180 })!181 offer_now(a, mut backend)!182 mut b := open_repo(machine('order_b')!)!183 defer {184 b.close()185 }186 join(b, mut backend)!187 assert history.head(b)!.id == child.id188}189190191fn test_steal_refuses_an_unsaved_executable_bit_change() {192 base := scratch('chmod')!193 defer {194 os.rmdir_all(base) or {}195 }196 mut backend := remote.new_folder(os.join_path(base, 'remote'))!197 mut a := open_repo(machine('chmod_a')!)!198 defer {199 a.close()200 }201 put(a.root, 'run.sh', 'echo hi\n')!202 commit(a, 'script')!203 offer_now(a, mut backend)!204 os.chmod(os.join_path(a.root, 'run.sh'), 0o755)!205 if _ := steal_now(a, mut backend) {206 assert false, 'a chmod-only change should make the tree dirty'207 } else {208 assert err.msg() == dirty_tree209 }210}211212fn test_divergence_without_clashes_keeps_both_machines_work() {213 base := scratch('diverge')!214 defer {215 os.rmdir_all(base) or {}216 }217 mut backend := remote.new_folder(os.join_path(base, 'remote'))!218 mut a := open_repo(machine('div_a')!)!219 defer {220 a.close()221 }222 mut b := open_repo(machine('div_b')!)!223 defer {224 b.close()225 }226 put(a.root, 'shared.txt', 'base\n')!227 commit(a, 'base')!228 offer_now(a, mut backend)!229230 231 join(b, mut backend)!232 put(a.root, 'fromA.txt', 'A\n')!233 commit(a, 'A adds a file')!234 put(b.root, 'fromB.txt', 'B\n')!235 commit(b, 'B adds a file')!236 offer_now(a, mut backend)!237238 239 if _ := offer_now(b, mut backend) {240 assert false, 'offering on top of unknown remote saves should be refused'241 } else {242 assert err.msg().contains('run koh steal')243 }244245 report := steal_now(b, mut backend)!246 assert !report.forward247 assert report.replayed == 1248 assert report.clashes.len == 0249 250 assert os.read_file(os.join_path(b.root, 'fromA.txt'))! == 'A\n'251 assert os.read_file(os.join_path(b.root, 'fromB.txt'))! == 'B\n'252 assert os.read_file(os.join_path(b.root, 'shared.txt'))! == 'base\n'253 254 mut saw_replay := false255 for save in history.all_saves(b)! {256 if save.replayed_from != '' {257 saw_replay = true258 }259 }260 assert saw_replay261 262 offer_now(b, mut backend)!263 line_head := backend.get(head_key('proj'))!.body.bytestr().trim_space()264 assert line_head == history.head(b)!.id265}266267fn test_clashing_files_are_chosen_one_at_a_time() {268 base := scratch('clash')!269 defer {270 os.rmdir_all(base) or {}271 }272 mut backend := remote.new_folder(os.join_path(base, 'remote'))!273 mut a := open_repo(machine('clash_a')!)!274 defer {275 a.close()276 }277 mut b := open_repo(machine('clash_b')!)!278 defer {279 b.close()280 }281 put(a.root, 'same.txt', 'base\n')!282 put(a.root, 'other.txt', 'quiet\n')!283 commit(a, 'base')!284 offer_now(a, mut backend)!285 join(b, mut backend)!286287 288 put(a.root, 'same.txt', 'A version\n')!289 commit(a, 'A edits same')!290 offer_now(a, mut backend)!291 put(b.root, 'same.txt', 'B version\n')!292 commit(b, 'B edits same')!293294 295 if _ := steal(b, mut backend, StealOptions{}) {296 assert false, 'an undecided clash must be refused'297 } else {298 assert err.msg().contains('--mine') || err.msg().contains('--theirs')299 }300 assert find_clashes(history.files_of(b, history.head(b)!.id)!, history.files_of(b,301 history.head(b)!.id)!, history.files_of(b, history.head(b)!.id)!).len == 0302303 304 report := steal_now(b, mut backend)!305 assert report.clashes == ['same.txt']306 assert report.kept_mine == 0307 assert os.read_file(os.join_path(b.root, 'same.txt'))! == 'A version\n'308 assert os.read_file(os.join_path(b.root, 'other.txt'))! == 'quiet\n'309}310311fn test_choosing_mine_keeps_the_local_version() {312 base := scratch('mine')!313 defer {314 os.rmdir_all(base) or {}315 }316 mut backend := remote.new_folder(os.join_path(base, 'remote'))!317 mut a := open_repo(machine('mine_a')!)!318 defer {319 a.close()320 }321 mut b := open_repo(machine('mine_b')!)!322 defer {323 b.close()324 }325 put(a.root, 'same.txt', 'base\n')!326 commit(a, 'base')!327 offer_now(a, mut backend)!328 join(b, mut backend)!329 put(a.root, 'same.txt', 'A version\n')!330 commit(a, 'A edits')!331 offer_now(a, mut backend)!332 put(b.root, 'same.txt', 'B version\n')!333 commit(b, 'B edits')!334335 report := steal(b, mut backend, StealOptions{336 mine: true337 })!338 assert report.kept_mine == 1339 assert os.read_file(os.join_path(b.root, 'same.txt'))! == 'B version\n'340 341 assert history.head(b)!.message.contains('combine: kept mine for 1 file')342 343 assert history.head(b)!.restores == ''344 assert history.all_saves(b)!.len >= 4345}346347fn test_flags_travel_with_the_saves() {348 base := scratch('flags')!349 defer {350 os.rmdir_all(base) or {}351 }352 mut backend := remote.new_folder(os.join_path(base, 'remote'))!353 mut a := open_repo(machine('flag_a')!)!354 defer {355 a.close()356 }357 mut b := open_repo(machine('flag_b')!)!358 defer {359 b.close()360 }361 put(a.root, 'a.txt', 'one\n')!362 save := commit(a, 'suspect save')!363 events.flag(a, save.id, 'look at this')!364 offer_now(a, mut backend)!365366 367 fetched := fetch_back_to_known(b, mut backend, 'proj')!368 assert fetched.events_out.len == 1369 assert fetched.events_out[0].kind == events.kind_flag370 b.begin()!371 write_fetched(b, fetched)!372 history.set_head(b, fetched.head)!373 b.commit()!374 state := events.flag_state(b, save.id)!375 assert state.flagged376 assert state.reason == 'look at this'377}378379fn test_a_second_offer_racing_head_is_refused_not_forced() {380 base := scratch('race')!381 defer {382 os.rmdir_all(base) or {}383 }384 mut backend := remote.new_folder(os.join_path(base, 'remote'))!385 mut a := open_repo(machine('race_a')!)!386 defer {387 a.close()388 }389 put(a.root, 'a.txt', 'one\n')!390 commit(a, 'base')!391 offer_now(a, mut backend)!392393 394 assert backend.put_if_match(head_key('proj'), 'a-save-we-do-not-have'.bytes(),395 backend.get(head_key('proj'))!.tag)!396397 put(a.root, 'a.txt', 'two\n')!398 commit(a, 'second')!399 if _ := offer_now(a, mut backend) {400 assert false, 'offering while the remote head moved must be refused'401 } else {402 assert err.msg().contains('run koh steal')403 }404}405406407fn join(store &storage.Store, mut backend remote.Backend) ! {408 fetched := fetch_repo(mut backend, 'proj')!409 store.begin()!410 write_fetched(store, fetched)!411 history.set_head(store, fetched.head)!412 store.commit()!413 snapshot.restore_files(store, store.root, snapshot.load(store.root), history.files_of(store,414 fetched.head)!, [])!415 store.set_state('remote_repo', 'proj')!416}417418struct FaultyBackend {419mut:420 inner remote.Backend421 writes int422 reads int423 fail_write int 424 fail_read int425 426 fail_delete bool427}428429fn (mut f FaultyBackend) get(key string) !remote.Object {430 f.reads++431 if f.fail_read > 0 && f.reads >= f.fail_read {432 return error('injected read failure')433 }434 return f.inner.get(key)!435}436437fn (mut f FaultyBackend) list(prefix string) ![]string {438 return f.inner.list(prefix)!439}440441fn (mut f FaultyBackend) put_if_absent(key string, body []u8) !bool {442 f.writes++443 if f.fail_write > 0 && f.writes >= f.fail_write {444 return error('injected write failure')445 }446 return f.inner.put_if_absent(key, body)!447}448449fn (mut f FaultyBackend) put_if_match(key string, body []u8, tag string) !bool {450 f.writes++451 if f.fail_write > 0 && f.writes >= f.fail_write {452 return error('injected write failure')453 }454 return f.inner.put_if_match(key, body, tag)!455}456457fn (mut f FaultyBackend) delete(key string) ! {458 if f.fail_delete {459 return error('injected delete failure')460 }461 f.inner.delete(key)!462}463464fn test_a_failed_offer_is_safe_to_re_run() {465 base := scratch('fault_offer')!466 defer {467 os.rmdir_all(base) or {}468 }469 remote_dir := os.join_path(base, 'remote')470 mut a := open_repo(machine('fault_a')!)!471 defer {472 a.close()473 }474 put(a.root, 'a.txt', 'one\n')!475 commit(a, 'first')!476 put(a.root, 'a.txt', 'two\n')!477 commit(a, 'second')!478479 480 mut flaky := FaultyBackend{481 inner: remote.new_folder(remote_dir)!482 fail_write: 2483 }484 if _ := offer(a, mut flaky) {485 assert false, 'the injected failure should have surfaced'486 }487488 489 490 mut healthy := remote.new_folder(remote_dir)!491 report := offer(a, mut healthy)!492 assert report.saves == 2493 assert healthy.get(head_key('proj'))!.body.bytestr().trim_space() == history.head(a)!.id494 assert healthy.list('proj/saves')!.len == 2495}496497fn test_a_failed_steal_leaves_nothing_behind() {498 base := scratch('fault_steal')!499 defer {500 os.rmdir_all(base) or {}501 }502 remote_dir := os.join_path(base, 'remote')503 mut a := open_repo(machine('fault_s_a')!)!504 defer {505 a.close()506 }507 mut b := open_repo(machine('fault_s_b')!)!508 defer {509 b.close()510 }511 put(a.root, 'a.txt', 'from A\n')!512 commit(a, 'A writes')!513 mut healthy := remote.new_folder(remote_dir)!514 offer(a, mut healthy)!515516 517 mut flaky := FaultyBackend{518 inner: remote.new_folder(remote_dir)!519 fail_read: 2520 }521 if _ := fetch_back_to_known(b, mut flaky, 'proj') {522 assert false, 'the injected read failure should have surfaced'523 }524 525 assert history.head(b) == none526 assert b.object_count()! == 0527528 529 fetched := fetch_back_to_known(b, mut healthy, 'proj')!530 b.begin()!531 write_fetched(b, fetched)!532 history.set_head(b, fetched.head)!533 b.commit()!534 snapshot.restore_files(b, b.root, snapshot.load(b.root), history.files_of(b, fetched.head)!, [])!535 assert os.read_file(os.join_path(b.root, 'a.txt'))! == 'from A\n'536}537538fn test_an_interrupted_strip_is_idempotent() {539 540 541 542 base := scratch('fault_strip')!543 defer {544 os.rmdir_all(base) or {}545 }546 mut a := open_repo(machine('fault_strip_a')!)!547 defer {548 a.close()549 }550 put(a.root, 'secret.txt', 'shh\n')!551 put(a.root, 'keep.txt', 'fine\n')!552 save := commit(a, 'add both')!553554 for _ in 0 .. 2 {555 strip_local(a, 'secret.txt', '')!556 }557 files := history.files_of(a, save.id)!558 559 560 assert files.map(it.path) == ['.kohignore', 'keep.txt']561 assert events.strip_events(a)!.len == 1562}563564fn test_a_strip_reaches_the_remote_and_other_machines() {565 base := scratch('strip_sync')!566 defer {567 os.rmdir_all(base) or {}568 }569 mut a := open_repo(machine('strip_a')!)!570 defer {571 a.close()572 }573 mut b := open_repo(machine('strip_b')!)!574 defer {575 b.close()576 }577 put(a.root, 'secrets.env', 'api_key=1\n')!578 put(a.root, 'keep.txt', 'fine\n')!579 save := commit(a, 'add both')!580 mut backend := remote.new_folder(os.join_path(base, 'remote'))!581 offer_now(a, mut backend)!582 join(b, mut backend)!583 assert b.object_count()! > 0584585 586 587 mut secret_hash := ''588 for file in history.files_of(a, save.id)! {589 if file.path == 'secrets.env' {590 secret_hash = file.hash591 }592 }593 assert secret_hash != ''594 strip_local(a, 'secrets.env', '')!595 report := push_strip(a, mut backend, 'secrets.env')!596 assert report.event_sent597 assert report.records == 1598599 600 601 record := decode_save(backend.get(save_key('proj', save.id))!.body)!602 for file in record.files {603 assert file.path != 'secrets.env'604 }605 606 607 assert !backend.get(object_key('proj', secret_hash))!.found608 assert backend.list('proj/objects')!.len == 2609610 611 b.begin()!612 fetched := fetch_back_to_known(b, mut backend, 'proj')!613 write_fetched(b, fetched)!614 b.commit()!615 mut paths := []string{}616 for file in history.files_of(b, save.id)! {617 paths << file.path618 }619 assert 'secrets.env' !in paths620 assert 'keep.txt' in paths621622 623 624 625 assert events.strip_events(b)!.map(it.id) == events.strip_events(a)!.map(it.id)626 put(b.root, 'secrets.env', 'rotated\n')!627 readded := commit(b, 'add the rotated key back')!628 offer_now(b, mut backend)!629 assert backend.list('proj/events')!.len == 1630 b.begin()!631 write_fetched(b, fetch_back_to_known(b, mut backend, 'proj')!)!632 b.commit()!633 assert 'secrets.env' in history.files_of(b, readded.id)!.map(it.path)634}635636637638639fn test_an_interrupted_remote_strip_finishes_when_re_run() {640 base := scratch('strip_resume')!641 defer {642 os.rmdir_all(base) or {}643 }644 remote_dir := os.join_path(base, 'remote')645 mut backend := remote.new_folder(remote_dir)!646 mut a := open_repo(machine('resume_a')!)!647 defer {648 a.close()649 }650 mut b := open_repo(machine('resume_b')!)!651 defer {652 b.close()653 }654 put(a.root, 'secrets.env', 'api_key=1\n')!655 put(a.root, 'shared.env', 'same bytes on B\n')!656 save := commit(a, 'add secrets')!657 offer_now(a, mut backend)!658 join(b, mut backend)!659 put(b.root, 'copy.txt', 'same bytes on B\n')!660 b_save := commit(b, 'B keeps a copy')!661 offer_now(b, mut backend)!662 shared_hash := storage.hash_hex('same bytes on B\n'.bytes())663 secret_hash := storage.hash_hex('api_key=1\n'.bytes())664665 strip_local(a, 'secrets.env', '')!666 667 668 mut flaky := FaultyBackend{669 inner: remote.new_folder(remote_dir)!670 fail_write: 3671 }672 if _ := push_strip(a, mut flaky, 'secrets.env') {673 assert false, 'the injected failure should have surfaced'674 }675 assert strip_pending(a, 'secrets.env')676 assert strip_local(a, 'secrets.env', '')!.files_removed == 0677 report := push_strip(a, mut backend, 'secrets.env')!678 assert report.records == 1679 assert !strip_pending(a, 'secrets.env')680 assert !backend.get(object_key('proj', secret_hash))!.found681 record := decode_save(backend.get(save_key('proj', save.id))!.body)!682 assert 'secrets.env' !in record.files.map(it.path)683 b_record := decode_save(backend.get(save_key('proj', b_save.id))!.body)!684 assert 'secrets.env' !in b_record.files.map(it.path)685686 687 strip_local(a, 'shared.env', '')!688 push_strip(a, mut backend, 'shared.env')!689 assert backend.get(object_key('proj', shared_hash))!.found690}691692693694fn test_offer_waits_for_an_unapplied_strip() {695 base := scratch('strip_first')!696 defer {697 os.rmdir_all(base) or {}698 }699 mut backend := remote.new_folder(os.join_path(base, 'remote'))!700 mut a := open_repo(machine('strip_first_a')!)!701 defer {702 a.close()703 }704 mut b := open_repo(machine('strip_first_b')!)!705 defer {706 b.close()707 }708 put(a.root, 'secrets.env', 'api_key=1\n')!709 commit(a, 'add secrets')!710 offer_now(a, mut backend)!711 join(b, mut backend)!712 strip_local(a, 'secrets.env', '')!713 push_strip(a, mut backend, 'secrets.env')!714 put(b.root, 'more.txt', 'B keeps working\n')!715 later := commit(b, 'B saves with the secret still in its tree')!716 if _ := offer_now(b, mut backend) {717 assert false, 'B offered records that still list the stripped path'718 }719 steal_now(b, mut backend)!720 offer_now(b, mut backend)!721 record := decode_save(backend.get(save_key('proj', later.id))!.body)!722 assert 'secrets.env' !in record.files.map(it.path)723}724725726727fn test_a_second_strip_keeps_the_first_ones_unfinished_objects() {728 base := scratch('strip_twice')!729 defer {730 os.rmdir_all(base) or {}731 }732 remote_dir := os.join_path(base, 'remote')733 mut backend := remote.new_folder(remote_dir)!734 mut a := open_repo(machine('strip_twice_a')!)!735 defer {736 a.close()737 }738 put(a.root, 'secrets.env', 'first key\n')!739 commit(a, 'first key')!740 offer_now(a, mut backend)!741 strip_local(a, 'secrets.env', '')!742 mut flaky := FaultyBackend{743 inner: remote.new_folder(remote_dir)!744 fail_delete: true745 }746 if _ := push_strip(a, mut flaky, 'secrets.env') {747 assert false, 'the injected delete failure should have surfaced'748 }749 put(a.root, 'secrets.env', 'second key\n')!750 commit(a, 'second key')!751 offer_now(a, mut backend)!752 strip_local(a, 'secrets.env', '')!753 push_strip(a, mut backend, 'secrets.env')!754 for key in ['first key\n', 'second key\n'] {755 assert !backend.get(object_key('proj', storage.hash_hex(key.bytes())))!.found756 }757}758759760fn test_unfinished_strips_of_one_path_in_different_saves_finish_together() {761 base := scratch('strip_scopes')!762 defer {763 os.rmdir_all(base) or {}764 }765 remote_dir := os.join_path(base, 'remote')766 mut backend := remote.new_folder(remote_dir)!767 mut a := open_repo(machine('strip_scopes_a')!)!768 defer {769 a.close()770 }771 put(a.root, 'secrets.env', 'first key\n')!772 first := commit(a, 'first key')!773 put(a.root, 'secrets.env', 'second key\n')!774 second := commit(a, 'second key')!775 offer_now(a, mut backend)!776 strip_local(a, 'secrets.env', first.id)!777 778 mut flaky := FaultyBackend{779 inner: remote.new_folder(remote_dir)!780 fail_write: 2781 }782 if _ := push_strip(a, mut flaky, 'secrets.env') {783 assert false, 'the injected failure should have surfaced'784 }785 strip_local(a, 'secrets.env', second.id)!786 push_strip(a, mut backend, 'secrets.env')!787 assert !strip_pending(a, 'secrets.env')788 for save in [first, second] {789 record := decode_save(backend.get(save_key('proj', save.id))!.body)!790 assert 'secrets.env' !in record.files.map(it.path)791 }792 for key in ['first key\n', 'second key\n'] {793 assert !backend.get(object_key('proj', storage.hash_hex(key.bytes())))!.found794 }795}796797798799fn test_a_second_strip_of_the_same_path_reaches_other_machines() {800 base := scratch('strip_again')!801 defer {802 os.rmdir_all(base) or {}803 }804 mut backend := remote.new_folder(os.join_path(base, 'remote'))!805 mut a := open_repo(machine('strip_again_a')!)!806 defer {807 a.close()808 }809 mut b := open_repo(machine('strip_again_b')!)!810 defer {811 b.close()812 }813 put(a.root, 'secrets.env', 'first key\n')!814 commit(a, 'first key')!815 offer_now(a, mut backend)!816 join(b, mut backend)!817 strip_local(a, 'secrets.env', '')!818 push_strip(a, mut backend, 'secrets.env')!819 steal_now(b, mut backend)!820 821 os.rm(os.join_path(b.root, 'secrets.env'))!822 put(a.root, 'secrets.env', 'second key\n')!823 second := commit(a, 'second key')!824 offer_now(a, mut backend)!825 steal_now(b, mut backend)!826 assert 'secrets.env' in history.files_of(b, second.id)!.map(it.path)827 strip_local(a, 'secrets.env', '')!828 push_strip(a, mut backend, 'secrets.env')!829 steal_now(b, mut backend)!830 assert 'secrets.env' !in history.files_of(b, second.id)!.map(it.path)831}