import sqlite3 from types import SimpleNamespace from click.testing import CliRunner from ocado_grocy.enrichment_retry import RetryQueue from ocado_grocy.history import private_json, main from ocado_grocy import postprocess from ocado_grocy.order_state import mark_orders def test_legacy_report_recovery_is_scoped_and_does_not_resurrect_old_errors(tmp_path): output = tmp_path/'enrichment'/'old' private_json(output/'summary.json',{'order_ids':['123'],'errors':['Open Food Facts: failed']}) private_json(output/'openfoodfacts.json',{'products':[ {'product_id':1,'status':'error','error':'ConnectionError'}, {'product_id':2,'status':'unchanged'}, {'product_id':99,'status':'error'}]}) queue = RetryQueue(tmp_path/'state.sqlite3','server') queue.migrate_reports(tmp_path,{1,2}) assert queue.pending('openfoodfacts') == {1} queue.results('openfoodfacts',[{'product_id':1,'status':'unchanged'}]) queue.migrate_reports(tmp_path,{1,2}) assert not queue.pending('openfoodfacts') queue.close() def test_queue_separates_stages_and_servers_and_keeps_only_failures(tmp_path): queue = RetryQueue(tmp_path/'state.sqlite3','server') queue.start('images',[{'id':1},{'id':2}]) queue.start('openfoodfacts',[{'id':1}]) queue.results('images',[{'product_id':1,'status':'updated'},{'product_id':2,'status':'error'}]) assert queue.pending('images') == {2} assert queue.pending('openfoodfacts') == {1} other = RetryQueue(tmp_path/'state.sqlite3','other') assert not other.pending('images') other.close() queue.close() def test_retry_targets_only_failed_live_imports_and_clears_success(tmp_path, monkeypatch): state = tmp_path/'imports.sqlite3' with sqlite3.connect(state) as db: db.execute('CREATE TABLE imports(server,product_id,status)') db.executemany('INSERT INTO imports VALUES (?,?,?)',[('server',1,'done'),('server',2,'done'),('server',3,'done')]) queue = RetryQueue(state,'server') queue.start('openfoodfacts',[{'id':1},{'id':3}]) queue.close() class Client: url = 'server' def get(self,path): assert path == '/objects/products' return [{'id':1},{'id':2}] # 3 was deleted monkeypatch.setattr(postprocess,'Grocy',lambda *a:Client()) calls = [] def run(client, products, cache, contact): calls.append(products) return {'counts':{'unchanged':1},'products':[{'product_id':1,'status':'unchanged'}]} monkeypatch.setattr(postprocess,'run_openfoodfacts',run) result = postprocess.retry_enrichment({'openfoodfacts':{'contact':'test@example.com'}},tmp_path/'config.toml',tmp_path) assert calls == [[{'id':1}]] and not result['errors'] queue = RetryQueue(state,'server') assert not queue.pending('openfoodfacts') queue.close() def test_main_retries_even_with_no_new_orders(tmp_path, monkeypatch): from ocado_grocy import history monkeypatch.setattr(history,'cached_orders',lambda *a:([],[])) calls = [] monkeypatch.setattr(postprocess,'retry_enrichment',lambda *a,**kw:calls.append(True) or {'errors':[]}) result = CliRunner().invoke(main,['--cached','--config',str(tmp_path/'config.toml'),'--archive',str(tmp_path)]) assert result.exit_code == 0 and calls == [True] assert 'No new delivered orders' in result.output calls.clear() result = CliRunner().invoke(main,['--cached','--dry-run','--config',str(tmp_path/'config.toml'),'--archive',str(tmp_path)]) assert result.exit_code == 0 and not calls def test_retry_failure_keeps_error_exit_even_without_new_orders(tmp_path, monkeypatch): from ocado_grocy import history monkeypatch.setattr(history,'cached_orders',lambda *a:([],[])) monkeypatch.setattr(postprocess,'retry_enrichment',lambda *a,**kw:{'errors':['ConnectionError']}) result = CliRunner().invoke(main,['--cached','--config',str(tmp_path/'config.toml'),'--archive',str(tmp_path)]) assert result.exit_code == 1 assert 'Enrichment retries still need attention' in result.output