From f8377fb8efc4f0d489ba960c5cbdc21521070430 Mon Sep 17 00:00:00 2001 From: Anil Date: Mon, 15 Jun 2026 14:29:16 +0000 Subject: [PATCH] feat(scraper): add --auto-save to self-healing approve/heal resume Thread the new optional auto_save parameter into the resume_automation_job call. When a heal is approved (message:true) and --auto-save is set, the body becomes {message:true, auto_save:true} so the approved template is saved automatically once the job completes. auto_save is omitted on reject (the API ignores it there), keeping the request minimal. - resume_and_poll: build body with auto_save only on approve - approve + heal subcommands: new --auto-save flag - types: autoSave on Scraper_approve_opts / Scraper_heal_opts - tests: assert body carries auto_save only when approving + flag set Ref: docs resume-self-healing-job (auto_save) --- src/__tests__/commands/scraper.test.ts | 51 ++++++++++++++++++++++++++ src/commands/scraper.ts | 15 +++++++- src/types/scraper.ts | 2 + 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/src/__tests__/commands/scraper.test.ts b/src/__tests__/commands/scraper.test.ts index 6ec9167..92c1409 100644 --- a/src/__tests__/commands/scraper.test.ts +++ b/src/__tests__/commands/scraper.test.ts @@ -1871,6 +1871,43 @@ describe('commands/scraper', ()=>{ {timing: undefined}, 600); expect(mocks.post.mock.calls[0][2]).toEqual({message: false}); }); + + it('adds auto_save:true to the body when approving with ' + +'autoSave set', async()=>{ + mocks.post.mockResolvedValueOnce({ok: true}); + mocks.poll_until.mockResolvedValue({ + result: {status: 'done', completed_steps: []}, + attempts: 1, + }); + await resume_and_poll('api_key', 'c_abc', true, + {timing: undefined, autoSave: true}, 600); + expect(mocks.post.mock.calls[0][2]).toEqual({ + message: true, auto_save: true, + }); + }); + + it('omits auto_save when approving without autoSave', async()=>{ + mocks.post.mockResolvedValueOnce({ok: true}); + mocks.poll_until.mockResolvedValue({ + result: {status: 'done', completed_steps: []}, + attempts: 1, + }); + await resume_and_poll('api_key', 'c_abc', true, + {timing: undefined}, 600); + expect(mocks.post.mock.calls[0][2]).toEqual({message: true}); + }); + + it('never sends auto_save on reject, even if autoSave set', + async()=>{ + mocks.post.mockResolvedValueOnce({ok: true}); + mocks.poll_until.mockResolvedValue({ + result: {status: 'done', completed_steps: []}, + attempts: 1, + }); + await resume_and_poll('api_key', 'c_abc', false, + {timing: undefined, autoSave: true}, 600); + expect(mocks.post.mock.calls[0][2]).toEqual({message: false}); + }); }); describe('handle_approve_scraper', ()=>{ @@ -2142,5 +2179,19 @@ describe('commands/scraper', ()=>{ expect(heal.options.map(o=>o.long)) .toContain('--auto-approve'); }); + + it('approve exposes --auto-save', ()=>{ + const approve = scraper_command.commands + .find(c=>c.name()=='approve')!; + expect(approve.options.map(o=>o.long)) + .toContain('--auto-save'); + }); + + it('heal exposes --auto-save', ()=>{ + const heal = scraper_command.commands + .find(c=>c.name()=='heal')!; + expect(heal.options.map(o=>o.long)) + .toContain('--auto-save'); + }); }); }); diff --git a/src/commands/scraper.ts b/src/commands/scraper.ts index e3a7a6c..d96bbd9 100644 --- a/src/commands/scraper.ts +++ b/src/commands/scraper.ts @@ -501,13 +501,18 @@ const resume_and_poll = async( api_key: string, collector_id: string, approve: boolean, - opts: {timing?: boolean}, + opts: {timing?: boolean; autoSave?: boolean}, timeout: number ): Promise>=>{ + // auto_save only takes effect on approval (message:true); the API + // ignores it on reject, so we omit it there to keep the body minimal. + const resume_body = approve && opts.autoSave + ? {message: approve, auto_save: true} + : {message: approve}; await post( api_key, `/dca/collectors/${collector_id}/${RESUME_JOB_PATH}`, - {message: approve}, + resume_body, {timing: opts.timing, hints: SCRAPER_BODY_HINTS} ); return poll_until({ @@ -1380,6 +1385,9 @@ const heal_subcommand = new Command('heal') .option('--auto-approve', 'When the heal hits the approval gate, approve it automatically ' +'and poll through to done (default: stop and let you review).') + .option('--auto-save', + 'With --auto-approve, also save the healed template automatically ' + +'once the job completes (sent as auto_save to the resume call).') .option('--timeout ', 'Polling timeout in seconds (default: 600)') .option('--max-retries ', @@ -1422,6 +1430,9 @@ const approve_subcommand = new Command('approve') 'Collector ID of the scraper whose heal is awaiting approval') .option('--reject', 'Reject the proposed fix instead of approving it.') + .option('--auto-save', + 'Save the approved template automatically once the job completes ' + +'successfully (sent as auto_save to the resume call).') .option('--url ', 'Verify target woven into the next-step hint on success.') .option('--timeout ', diff --git a/src/types/scraper.ts b/src/types/scraper.ts index 6dfc588..b836778 100644 --- a/src/types/scraper.ts +++ b/src/types/scraper.ts @@ -130,6 +130,7 @@ type Scraper_heal_opts = { maxRetries?: string; retry?: boolean; autoApprove?: boolean; + autoSave?: boolean; }; type Scraper_approve_opts = { @@ -142,6 +143,7 @@ type Scraper_approve_opts = { timing?: boolean; apiKey?: string; legacyOutput?: boolean; + autoSave?: boolean; }; export type {