1module cli23import os4import compress.zlib5import crypto.sha2566import json27import src.exchange8import src.release9import src.remote10import src.events11import src.history12import src.snapshot13import src.storage1415fn test_doctor_checks_cycles_and_missing_heads_without_walking_forever() {16 saves := [history.Save{ id: 'aaaaaaaaaaaa', parent: 'bbbbbbbbbbbb' },17 history.Save{ id: 'bbbbbbbbbbbb', parent: 'aaaaaaaaaaaa' }]18 assert doctor_history(saves, 'cccccccccccc').any(it.contains('head'))19 assert doctor_history(saves, 'aaaaaaaaaaaa').any(it.contains('cycle'))20 assert doctor_history([history.Save{ id: 'aaaaaaaaaaaa', parent: 'cccccccccccc' }],21 'aaaaaaaaaaaa').any(it.contains('missing parent'))22 assert doctor_history([]history.Save{}, '').len == 023 assert doctor_history([history.Save{ id: 'aaaaaaaaaaaa' }], '').any(it.contains('no head'))24}2526fn test_doctor_checks_off_line_paths_and_file_directory_collisions() {27 mut report := DoctorJson{}28 doctor_files('aaaaaaaaaaaa', [snapshot.FileEntry{ path: '../unsafe' },29 snapshot.FileEntry{ path: 'a' }, snapshot.FileEntry{ path: 'a/b' }], []events.Event{},30 mut report)31 assert report.problems.any(it.contains('unsafe path'))32 assert report.problems.any(it.contains('file and a directory'))33}3435fn test_doctor_reports_unsafe_save_paths_with_a_quoted_strip_command() {36 dir := os.join_path(os.temp_dir(), 'koh3_doctor_paths_${os.getpid()}')37 os.rmdir_all(dir) or {}38 os.mkdir_all(dir)!39 defer {40 os.rmdir_all(dir) or {}41 }42 store := storage.create(dir)!43 defer {44 store.close()45 }46 hash := store.put_object('unsafe'.bytes())!47 store.begin()!48 history.import_save(store, history.Save{49 id: 'badbadbadbad'50 }, [snapshot.FileEntry{51 path: "../June's file"52 hash: hash53 }])!54 history.set_head(store, 'badbadbadbad')!55 store.end()!56 mut report := DoctorJson{}57 doctor_files('badbadbadbad', history.files_of(store, 'badbadbadbad')!, []events.Event{}, mut report)58 assert report.problems == [59 "save [badbadbadbad] has unsafe path ../June's file — koh strip '../June'\\''s file'",60 ]61}6263struct DoctorReadOnly {64mut:65 items map[string][]u866}6768fn (mut backend DoctorReadOnly) get(key string) !remote.Object {69 if body := backend.items[key] {70 return remote.Object{ found: true, body: body }71 }72 return remote.Object{}73}7475fn (mut backend DoctorReadOnly) list(prefix string) ![]string {76 return backend.items.keys().filter(it.starts_with('${prefix}/'))77}7879fn (mut backend DoctorReadOnly) put_if_absent(key string, body []u8) !bool {80 panic('Doctor attempted a remote write')81}8283fn (mut backend DoctorReadOnly) put_if_match(key string, body []u8, tag string) !bool {84 panic('Doctor attempted a remote write')85}8687fn (mut backend DoctorReadOnly) delete(key string) ! {88 panic('Doctor attempted a remote delete')89}9091fn test_doctor_remote_audits_unreferenced_objects_and_release_checksums_without_writes() {92 data := 'file'.bytes()93 hash := storage.hash_hex(data)94 mut backend := DoctorReadOnly{95 items: {96 'repo/head': 'aaaaaaaaaaaa'.bytes()97 'repo/saves/aaaaaaaaaaaa': json2.encode(exchange.SaveRecord{98 id: 'aaaaaaaaaaaa'99 files: [exchange.FileRecord{ path: 'file', hash: hash }]100 }).bytes()101 'repo/objects/${hash}': zlib.compress(data)!102 'repo/releases/latest': '1.0.0'.bytes()103 'repo/releases/1.0.0/release.json': json2.encode(release.Record{104 repo: 'repo'105 version: '1.0.0'106 save: 'aaaaaaaaaaaa'107 files: [release.File{108 platform: 'linux-aarch64'109 name: 'app'110 size: data.len111 blake3: hash112 sha256: sha256.sum256(data).hex()113 }]114 }).bytes()115 'repo/releases/1.0.0/linux-aarch64/app': data116 }117 }118 mut report := DoctorJson{ head_id: 'aaaaaaaaaaaa' }119 doctor_remote_content(mut backend, 'repo', mut report)!120 assert report.problems.len == 0121 assert report.remote_saves == 1122 assert report.remote_releases == 1123 backend.items['repo/objects/${storage.hash_hex('unreferenced'.bytes())}'] = 'bad zlib'.bytes()124 backend.items['repo/releases/1.0.0/linux-aarch64/app'] = 'corrupt'.bytes()125 mut damaged := DoctorJson{}126 doctor_remote_content(mut backend, 'repo', mut damaged)!127 assert damaged.problems.any(it.contains('damaged remote object'))128 assert damaged.problems.any(it.contains('checksum verification'))129}130131fn test_doctor_reports_event_errors_strip_residue_and_flags() {132 mut report := DoctorJson{}133 saves := [history.Save{ id: 'aaaaaaaaaaaa' }]134 event_list := [events.Event{ id: 'strip', kind: 'strip', path: 'private' },135 events.Event{ id: 'flag', kind: 'flag', save_id: 'aaaaaaaaaaaa' },136 events.Event{ id: 'bad', kind: 'unknown' }]137 doctor_events(event_list, saves, mut report)138 doctor_files(saves[0].id, [snapshot.FileEntry{ path: 'private/file' }], event_list, mut report)139 assert report.flagged_saves == 1140 assert report.problems.any(it.contains('unknown kind'))141 assert report.problems.any(it.contains('stripped path'))142}143144struct DoctorMemory {145mut:146 items map[string][]u8147}148149fn (mut backend DoctorMemory) get(key string) !remote.Object {150 if body := backend.items[key] {151 return remote.Object{ found: true, body: body }152 }153 return remote.Object{}154}155156fn (mut backend DoctorMemory) list(prefix string) ![]string {157 return backend.items.keys().filter(it.starts_with('${prefix}/'))158}159160fn (mut backend DoctorMemory) put_if_absent(key string, body []u8) !bool {161 if key in backend.items {162 return false163 }164 backend.items[key] = body165 return true166}167168fn (mut backend DoctorMemory) put_if_match(key string, body []u8, tag string) !bool {169 return false170}171172fn (mut backend DoctorMemory) delete(key string) ! {173 backend.items.delete(key)174}175176fn doctor_store(name string) !(storage.Store, string) {177 dir := os.join_path(os.temp_dir(), 'koh3_doctor_${name}_${os.getpid()}')178 os.rmdir_all(dir) or {}179 os.mkdir_all(dir)!180 return storage.create(dir)!, dir181}182183fn test_doctor_fetches_missing_parents_from_the_remote_without_writing_to_it() {184 store, dir := doctor_store('fetch')!185 defer {186 store.close()187 os.rmdir_all(dir) or {}188 }189 data := 'parent'.bytes()190 hash := storage.hash_hex(data)191 mut backend := DoctorReadOnly{192 items: {193 'repo/saves/aaaaaaaaaaaa': json2.encode(exchange.SaveRecord{194 id: 'aaaaaaaaaaaa'195 files: [exchange.FileRecord{ path: 'file', hash: hash }]196 }).bytes()197 'repo/objects/${hash}': zlib.compress(data)!198 }199 }200 store.db.exec('PRAGMA foreign_keys=OFF')!201 store.begin()!202 history.import_save(store, history.Save{ id: 'bbbbbbbbbbbb', parent: 'aaaaaaaaaaaa' },203 []snapshot.FileEntry{})!204 history.set_head(store, 'bbbbbbbbbbbb')!205 store.end()!206 store.db.exec('PRAGMA foreign_keys=ON')!207 missing := missing_saves(history.all_saves(store)!, 'bbbbbbbbbbbb')208 assert missing == ['aaaaaaaaaaaa']209 assert exchange.fetch_missing(store, mut backend, 'repo', missing)! == 1210 assert missing_saves(history.all_saves(store)!, 'bbbbbbbbbbbb').len == 0211 assert store.get_object(hash)! == data212 assert exchange.fetch_missing(store, mut backend, 'repo', ['cccccccccccc']) or {213 assert err.msg().contains('does not have save')214 0215 } == 0216}217218fn test_doctor_reapplies_recorded_strips_and_finds_the_only_tip() {219 store, dir := doctor_store('strip')!220 defer {221 store.close()222 os.rmdir_all(dir) or {}223 }224 secret := store.put_object('secret'.bytes())!225 kept := store.put_object('kept'.bytes())!226 store.begin()!227 history.import_save(store, history.Save{ id: 'aaaaaaaaaaaa' }, [228 snapshot.FileEntry{ path: 'keep', hash: kept },229 snapshot.FileEntry{ path: 'private/key', hash: secret },230 ])!231 history.import_save(store, history.Save{ id: 'bbbbbbbbbbbb', parent: 'aaaaaaaaaaaa' },232 [snapshot.FileEntry{ path: 'keep', hash: kept }])!233 events.strip(store, '', 'private')!234 store.end()!235 assert only_tip(store) or { '' } == 'bbbbbbbbbbbb'236 mut report := DoctorJson{}237 doctor_files('aaaaaaaaaaaa', history.files_of(store, 'aaaaaaaaaaaa')!, events.all(store)!, mut238 report)239 assert report.strip_residue240 assert exchange.reapply_strips(store)! == 1241 assert history.files_of(store, 'aaaaaaaaaaaa')!.map(it.path) == ['keep']242 assert !store.has_object(secret)!243 assert events.strip_events(store)!.len == 1244}245246fn test_doctor_restores_objects_from_working_files_and_resends_them_to_the_remote() {247 store, dir := doctor_store('objects')!248 defer {249 store.close()250 os.rmdir_all(dir) or {}251 }252 data := 'content'.bytes()253 hash := store.put_object(data)!254 store.begin()!255 history.import_save(store, history.Save{ id: 'aaaaaaaaaaaa' }, [256 snapshot.FileEntry{ path: 'app', hash: hash },257 ])!258 history.set_head(store, 'aaaaaaaaaaaa')!259 store.end()!260 store.db.exec("UPDATE objects SET data = x'00'")!261 problems := store.check_objects()!262 assert problems.map(it.hash) == [hash]263 assert local_copy(store, hash) == none264 os.write_file(os.join_path(dir, 'app'), 'content')!265 store.repair_object(hash, local_copy(store, hash) or { panic('no working copy') })!266 assert store.check_objects()!.len == 0267 store.repair_object(hash, 'other'.bytes()) or {268 assert err.msg().contains('does not match')269 }270 mut backend := DoctorMemory{271 items: {272 'repo/objects/${hash}': 'damaged'.bytes()273 }274 }275 assert exchange.resend(store, mut backend, 'repo', [hash], ['aaaaaaaaaaaa'])! == 2276 assert zlib.decompress(backend.items['repo/objects/${hash}'])! == data277 assert exchange.decode_save(backend.items['repo/saves/aaaaaaaaaaaa'])!.files[0].hash == hash278}