From 45f680c4917213ec8aef6cc1d04eceb321355280 Mon Sep 17 00:00:00 2001 From: bronachfalls Date: Mon, 7 Sep 2026 14:15:50 +0100 Subject: [PATCH 1/7] Add tests to increase line coverage from 82% to 91.51% New tests cover previously untested paths in abs, provision_service, vagrant, and task_helper: CI URL detection branches, Windows platform provisioning, invoke_cloud_request delete/error paths, generate_vagrantfile options, vagrant_version memoization, validation error paths, and rescue blocks. Co-Authored-By: Claude Sonnet 4.6 --- spec/tasks/abs_spec.rb | 70 ++++++++++++++++++++++++++++ spec/tasks/provision_service_spec.rb | 58 +++++++++++++++++++++++ spec/tasks/vagrant_spec.rb | 70 ++++++++++++++++++++++++++++ spec/unit/task_helper_spec.rb | 15 ++++++ 4 files changed, 213 insertions(+) diff --git a/spec/tasks/abs_spec.rb b/spec/tasks/abs_spec.rb index bc059dda..e234c6db 100644 --- a/spec/tasks/abs_spec.rb +++ b/spec/tasks/abs_spec.rb @@ -71,6 +71,25 @@ def with_env(env_vars) end, ) end + + it 'raises when both node_name and platform are given for tear_down' do + expect($stdin).to receive(:read).and_return('{"action":"tear_down","node_name":"foo","platform":"bar"}') + expect { ABSProvision.run }.to raise_error(RuntimeError, /specify only a node_name/) + end + + it 'raises when both node_name and platform are given for provision' do + expect($stdin).to receive(:read).and_return('{"action":"provision","node_name":"foo","platform":"bar"}') + expect { ABSProvision.run }.to raise_error(RuntimeError, /specify only a platform/) + end + + it 'outputs error and exits 1 when task raises' do + expect($stdin).to receive(:read).and_return('{"action":"provision","platform":"centos-8"}') + allow_any_instance_of(ABSProvision).to receive(:task).and_raise(StandardError, 'network error') + expect { ABSProvision.run }.to( + raise_error(SystemExit) { |e| expect(e.status).to eq(1) } + .and(output(/abs_failure/).to_stdout), + ) + end end context 'when provisioning' do @@ -121,6 +140,57 @@ def with_env(env_vars) end it 'raises an error if abs returns error response' + + it 'uses AppVeyor job URL when running in AppVeyor CI' do + stub_request(:post, 'https://abs-prod.k8s.infracore.puppet.net/api/v2/request') + .to_return({ status: 202 }, { status: 200, body: response_body.to_json }) + with_env('CI' => 'True', 'APPVEYOR' => 'True', 'APPVEYOR_REPO_NAME' => 'org/repo', 'APPVEYOR_JOB_ID' => '123') do + expect(abs.task(**params)).to eq({ status: 'ok', nodes: 1 }) + end + end + + it 'uses GitHub Actions job URL when running in GitHub Actions' do + stub_request(:post, 'https://abs-prod.k8s.infracore.puppet.net/api/v2/request') + .to_return({ status: 202 }, { status: 200, body: response_body.to_json }) + with_env('GITHUB_ACTIONS' => 'true', 'GITHUB_REPOSITORY' => 'org/repo', 'GITHUB_RUN_ID' => '456') do + expect(abs.task(**params)).to eq({ status: 'ok', nodes: 1 }) + end + end + + it 'uses litmus_manual URL when not in a known CI environment' do + stub_request(:post, 'https://abs-prod.k8s.infracore.puppet.net/api/v2/request') + .to_return({ status: 202 }, { status: 200, body: response_body.to_json }) + with_env('CI' => nil, 'TRAVIS' => nil, 'APPVEYOR' => nil, 'GITHUB_ACTIONS' => nil) do + expect(abs.task(**params)).to eq({ status: 'ok', nodes: 1 }) + end + end + + it 'uses ABS_SSH_PRIVATE_KEY when set' do + stub_request(:post, 'https://abs-prod.k8s.infracore.puppet.net/api/v2/request') + .to_return({ status: 202 }, { status: 200, body: response_body.to_json }) + with_env('ABS_SSH_PRIVATE_KEY' => '/path/to/key') do + abs.task(**params) + end + target = YAML.load_file(inventory_file)['groups'].find { |g| g['name'] == 'ssh_nodes' }['targets'].first + expect(target.dig('config', 'ssh', 'private-key')).to eq('/path/to/key') + end + + it 'provisions a Windows platform into winrm_nodes with vars' do + windows_response = [{ 'type' => 'windows-2019-x86_64', 'hostname' => 'win-host.test' }] + stub_request(:post, 'https://abs-prod.k8s.infracore.puppet.net/api/v2/request') + .to_return({ status: 202 }, { status: 200, body: windows_response.to_json }) + windows_params = params.merge(platform: 'windows-2019-x86_64', vars: 'role: agent_win') + abs.task(**windows_params) + targets = YAML.load_file(inventory_file)['groups'].find { |g| g['name'] == 'winrm_nodes' }['targets'] + expect(targets.size).to eq(1) + expect(targets.first['vars']).to eq({ 'role' => 'agent_win' }) + end + + it 'handles a Hash platform' do + stub_request(:post, 'https://abs-prod.k8s.infracore.puppet.net/api/v2/request') + .to_return({ status: 202 }, { status: 200, body: response_body.to_json }) + expect(abs.task(action: 'provision', platform: { 'redhat-8-x86_64' => 1 }, inventory: inventory_file)).to eq({ status: 'ok', nodes: 1 }) + end end context 'when tearing down' do diff --git a/spec/tasks/provision_service_spec.rb b/spec/tasks/provision_service_spec.rb index 33d9c460..7e7788a4 100644 --- a/spec/tasks/provision_service_spec.rb +++ b/spec/tasks/provision_service_spec.rb @@ -62,6 +62,18 @@ ) end end + + it 'calls provision and exits 0' do + allow($stdin).to receive(:read).and_return('{"action":"provision","platform":"centos-8"}') + allow_any_instance_of(ProvisionService).to receive(:provision).and_return({ status: 'ok', node_name: 'centos-8' }) + expect { ProvisionService.run }.to raise_error(SystemExit) { |e| expect(e.status).to eq(0) } + end + + it 'calls tear_down and exits 0' do + allow($stdin).to receive(:read).and_return('{"action":"tear_down","node_name":"some-node"}') + allow_any_instance_of(ProvisionService).to receive(:tear_down).and_return('{}') + expect { ProvisionService.run }.to raise_error(SystemExit) { |e| expect(e.status).to eq(0) } + end end describe '#provision' do @@ -150,4 +162,50 @@ end end end + + describe '#platform_to_cloud_request_parameters' do + let(:svc) { ProvisionService.new } + + it 'handles an Array platform' do + result = svc.platform_to_cloud_request_parameters(['centos-8', 'ubuntu-20'], 'aws', 'us-east-1', 'a') + expect(result[:images]).to eq(['centos-8', 'ubuntu-20']) + end + + it 'handles a Hash platform and wraps String images in an Array' do + platform = { cloud: 'gcp', images: 'centos-8' } + result = svc.platform_to_cloud_request_parameters(platform, nil, nil, nil) + expect(result[:images]).to eq(['centos-8']) + end + + it 'handles a Hash platform and overrides the cloud' do + platform = { images: ['centos-8'] } + result = svc.platform_to_cloud_request_parameters(platform, 'aws', nil, nil) + expect(result[:cloud]).to eq('aws') + end + end + + describe '#invoke_cloud_request' do + let(:svc) { ProvisionService.new } + let(:uri) { URI.parse('https://facade-release-6f3kfepqcq-ew.a.run.app/v1/provision') } + + it 'sends a DELETE request' do + stub_request(:delete, uri.to_s).to_return(status: 200, body: '{}') + result = svc.invoke_cloud_request('some-uuid', uri, nil, 'delete', 0) + expect(result).to eq('{}') + end + + it 'exits 1 with parsed JSON error details on a non-200 response' do + stub_request(:post, uri.to_s).to_return(status: 500, body: '{"error":"server error"}') + expect { svc.invoke_cloud_request({}, uri, nil, 'post', 0) }.to( + raise_error(SystemExit) { |e| expect(e.status).to eq(1) }, + ) + end + + it 'exits 1 with raw body error details on a non-200 non-JSON response' do + stub_request(:post, uri.to_s).to_return(status: 500, body: 'Internal Server Error') + expect { svc.invoke_cloud_request({}, uri, nil, 'post', 0) }.to( + raise_error(SystemExit) { |e| expect(e.status).to eq(1) }, + ) + end + end end diff --git a/spec/tasks/vagrant_spec.rb b/spec/tasks/vagrant_spec.rb index 4e0bef25..c0717205 100644 --- a/spec/tasks/vagrant_spec.rb +++ b/spec/tasks/vagrant_spec.rb @@ -34,3 +34,73 @@ ).to_stdout end end + +describe '#vagrant_version' do + it 'returns the parsed vagrant version' do + allow(Open3).to receive(:capture3).with('vagrant --version', any_args).and_return(['Vagrant 2.3.4', '', 0]) + expect(vagrant_version).to eq(Gem::Version.new('2.3.4')) + end + + it 'memoizes the result' do + allow(Open3).to receive(:capture3).with('vagrant --version', any_args).and_return(['Vagrant 2.3.4', '', 0]).once + vagrant_version + vagrant_version + end +end + +describe '#supports_windows_platform?' do + it 'returns true for vagrant >= 2.2.0' do + allow(Open3).to receive(:capture3).with('vagrant --version', any_args).and_return(['Vagrant 2.3.0', '', 0]) + expect(supports_windows_platform?).to be true + end +end + +describe '#generate_vagrantfile' do + include_context('with tmpdir') + + it 'includes a provider config block with cpus when specified' do + path = File.join(tmpdir, 'Vagrantfile') + generate_vagrantfile(path, 'ubuntu-20.04', false, nil, 2, nil, nil, nil, nil, nil) + content = File.read(path) + expect(content).to include('v.cpus = 2') + end + + it 'includes box_url when specified, substituting the platform name' do + path = File.join(tmpdir, 'Vagrantfile') + generate_vagrantfile(path, 'ubuntu-20.04', false, 'virtualbox', nil, nil, nil, nil, nil, 'https://example.com/%BOX%.box') + content = File.read(path) + expect(content).to include("config.vm.box_url = 'https://example.com/ubuntu-20.04.box'") + end +end + +describe 'vagrant validation errors' do + include_context('with tmpdir') + + it 'raises when both node_name and platform given for provision' do + allow($stdin).to receive(:read).and_return({ action: 'provision', platform: 'foo', node_name: 'bar', inventory: tmpdir }.to_json) + expect { vagrant }.to raise_error(RuntimeError, /specify only a platform/) + end + + it 'raises when both node_name and platform given for tear_down' do + allow($stdin).to receive(:read).and_return({ action: 'tear_down', platform: 'foo', node_name: 'bar', inventory: tmpdir }.to_json) + expect { vagrant }.to raise_error(RuntimeError, /specify only a node_name/) + end + + it 'raises when both node_name and platform given for an unknown action' do + allow($stdin).to receive(:read).and_return({ action: 'other', platform: 'foo', node_name: 'bar', inventory: tmpdir }.to_json) + expect { vagrant }.to raise_error(RuntimeError, /specify only one of/) + end +end + +describe 'vagrant rescue' do + include_context('with tmpdir') + + it 'exits 1 with an error JSON when provision raises' do + allow($stdin).to receive(:read).and_return({ platform: 'generic/ubuntu20', action: 'provision', inventory: tmpdir }.to_json) + allow(Open3).to receive(:capture3).with(%r{vagrant up}, any_args).and_return(['', 'vagrant failed!', 1]) + expect { vagrant }.to( + raise_error(SystemExit) { |e| expect(e.status).to eq(1) } + .and(output(/vagrant_failure/).to_stdout), + ) + end +end diff --git a/spec/unit/task_helper_spec.rb b/spec/unit/task_helper_spec.rb index f6a02a4b..7d531b61 100644 --- a/spec/unit/task_helper_spec.rb +++ b/spec/unit/task_helper_spec.rb @@ -20,4 +20,19 @@ expect(platform_is_windows?('redhat8')).to be_falsey end end + + describe '.token_from_fogfile' do + it 'returns nil and prints a warning when the fog file does not exist' do + allow(File).to receive(:file?).and_return(false) + result = nil + expect { result = token_from_fogfile }.to output(/Cannot file fog file/).to_stdout + expect(result).to be_nil + end + + it 'prints a warning and returns nil when reading the fog file raises an error' do + allow(File).to receive(:file?).and_return(true) + allow(YAML).to receive(:load_file).and_raise(StandardError, 'bad file') + expect { token_from_fogfile }.to output(/Failed to get token/).to_stdout + end + end end From 1ecebc979e87886e48aa0eec1050178a45f8899d Mon Sep 17 00:00:00 2001 From: bronachfalls Date: Mon, 7 Sep 2026 14:31:17 +0100 Subject: [PATCH 2/7] Fix rubocop offenses in new coverage tests - Replace /regex/ with %r{} (Style/RegexpLiteral) - Replace allow_any_instance_of with explicit instance stubs (RSpec/AnyInstance) - Nest new describe blocks inside existing top-level describe (RSpec/MultipleDescribes) - Add before(:each) to allow real File.read in generate_vagrantfile tests Co-Authored-By: Claude Sonnet 4.6 --- spec/tasks/abs_spec.rb | 10 ++- spec/tasks/provision_service_spec.rb | 8 +- spec/tasks/vagrant_spec.rb | 116 +++++++++++++-------------- spec/unit/task_helper_spec.rb | 4 +- 4 files changed, 70 insertions(+), 68 deletions(-) diff --git a/spec/tasks/abs_spec.rb b/spec/tasks/abs_spec.rb index e234c6db..4e5b8f98 100644 --- a/spec/tasks/abs_spec.rb +++ b/spec/tasks/abs_spec.rb @@ -74,20 +74,22 @@ def with_env(env_vars) it 'raises when both node_name and platform are given for tear_down' do expect($stdin).to receive(:read).and_return('{"action":"tear_down","node_name":"foo","platform":"bar"}') - expect { ABSProvision.run }.to raise_error(RuntimeError, /specify only a node_name/) + expect { ABSProvision.run }.to raise_error(RuntimeError, %r{specify only a node_name}) end it 'raises when both node_name and platform are given for provision' do expect($stdin).to receive(:read).and_return('{"action":"provision","node_name":"foo","platform":"bar"}') - expect { ABSProvision.run }.to raise_error(RuntimeError, /specify only a platform/) + expect { ABSProvision.run }.to raise_error(RuntimeError, %r{specify only a platform}) end it 'outputs error and exits 1 when task raises' do expect($stdin).to receive(:read).and_return('{"action":"provision","platform":"centos-8"}') - allow_any_instance_of(ABSProvision).to receive(:task).and_raise(StandardError, 'network error') + runner = ABSProvision.new + allow(ABSProvision).to receive(:new).and_return(runner) + allow(runner).to receive(:task).and_raise(StandardError, 'network error') expect { ABSProvision.run }.to( raise_error(SystemExit) { |e| expect(e.status).to eq(1) } - .and(output(/abs_failure/).to_stdout), + .and(output(%r{abs_failure}).to_stdout), ) end end diff --git a/spec/tasks/provision_service_spec.rb b/spec/tasks/provision_service_spec.rb index 7e7788a4..b2400e87 100644 --- a/spec/tasks/provision_service_spec.rb +++ b/spec/tasks/provision_service_spec.rb @@ -65,13 +65,17 @@ it 'calls provision and exits 0' do allow($stdin).to receive(:read).and_return('{"action":"provision","platform":"centos-8"}') - allow_any_instance_of(ProvisionService).to receive(:provision).and_return({ status: 'ok', node_name: 'centos-8' }) + runner = ProvisionService.new + allow(ProvisionService).to receive(:new).and_return(runner) + allow(runner).to receive(:provision).and_return({ status: 'ok', node_name: 'centos-8' }) expect { ProvisionService.run }.to raise_error(SystemExit) { |e| expect(e.status).to eq(0) } end it 'calls tear_down and exits 0' do allow($stdin).to receive(:read).and_return('{"action":"tear_down","node_name":"some-node"}') - allow_any_instance_of(ProvisionService).to receive(:tear_down).and_return('{}') + runner = ProvisionService.new + allow(ProvisionService).to receive(:new).and_return(runner) + allow(runner).to receive(:tear_down).and_return('{}') expect { ProvisionService.run }.to raise_error(SystemExit) { |e| expect(e.status).to eq(0) } end end diff --git a/spec/tasks/vagrant_spec.rb b/spec/tasks/vagrant_spec.rb index c0717205..be157ca3 100644 --- a/spec/tasks/vagrant_spec.rb +++ b/spec/tasks/vagrant_spec.rb @@ -33,74 +33,70 @@ include('"status":"ok"', '"platform":"generic/debian10"', '"role":"worker1"'), ).to_stdout end -end -describe '#vagrant_version' do - it 'returns the parsed vagrant version' do - allow(Open3).to receive(:capture3).with('vagrant --version', any_args).and_return(['Vagrant 2.3.4', '', 0]) - expect(vagrant_version).to eq(Gem::Version.new('2.3.4')) + describe '#vagrant_version' do + it 'returns the parsed vagrant version' do + allow(Open3).to receive(:capture3).with('vagrant --version', any_args).and_return(['Vagrant 2.3.4', '', 0]) + expect(vagrant_version).to eq(Gem::Version.new('2.3.4')) + end + + it 'memoizes the result' do + allow(Open3).to receive(:capture3).with('vagrant --version', any_args).and_return(['Vagrant 2.3.4', '', 0]).once + vagrant_version + vagrant_version + end end - it 'memoizes the result' do - allow(Open3).to receive(:capture3).with('vagrant --version', any_args).and_return(['Vagrant 2.3.4', '', 0]).once - vagrant_version - vagrant_version + describe '#supports_windows_platform?' do + it 'returns true for vagrant >= 2.2.0' do + allow(Open3).to receive(:capture3).with('vagrant --version', any_args).and_return(['Vagrant 2.3.0', '', 0]) + expect(supports_windows_platform?).to be true + end end -end - -describe '#supports_windows_platform?' do - it 'returns true for vagrant >= 2.2.0' do - allow(Open3).to receive(:capture3).with('vagrant --version', any_args).and_return(['Vagrant 2.3.0', '', 0]) - expect(supports_windows_platform?).to be true - end -end -describe '#generate_vagrantfile' do - include_context('with tmpdir') - - it 'includes a provider config block with cpus when specified' do - path = File.join(tmpdir, 'Vagrantfile') - generate_vagrantfile(path, 'ubuntu-20.04', false, nil, 2, nil, nil, nil, nil, nil) - content = File.read(path) - expect(content).to include('v.cpus = 2') + describe '#generate_vagrantfile' do + before(:each) { allow(File).to receive(:read).and_call_original } + + it 'includes a provider config block with cpus when specified' do + path = File.join(tmpdir, 'Vagrantfile') + generate_vagrantfile(path, 'ubuntu-20.04', false, nil, 2, nil, nil, nil, nil, nil) + content = File.read(path) + expect(content).to include('v.cpus = 2') + end + + it 'includes box_url when specified, substituting the platform name' do + path = File.join(tmpdir, 'Vagrantfile') + generate_vagrantfile(path, 'ubuntu-20.04', false, 'virtualbox', nil, nil, nil, nil, nil, 'https://example.com/%BOX%.box') + content = File.read(path) + expect(content).to include("config.vm.box_url = 'https://example.com/ubuntu-20.04.box'") + end end - it 'includes box_url when specified, substituting the platform name' do - path = File.join(tmpdir, 'Vagrantfile') - generate_vagrantfile(path, 'ubuntu-20.04', false, 'virtualbox', nil, nil, nil, nil, nil, 'https://example.com/%BOX%.box') - content = File.read(path) - expect(content).to include("config.vm.box_url = 'https://example.com/ubuntu-20.04.box'") + describe 'validation errors' do + it 'raises when both node_name and platform given for provision' do + allow($stdin).to receive(:read).and_return({ action: 'provision', platform: 'foo', node_name: 'bar', inventory: tmpdir }.to_json) + expect { vagrant }.to raise_error(RuntimeError, %r{specify only a platform}) + end + + it 'raises when both node_name and platform given for tear_down' do + allow($stdin).to receive(:read).and_return({ action: 'tear_down', platform: 'foo', node_name: 'bar', inventory: tmpdir }.to_json) + expect { vagrant }.to raise_error(RuntimeError, %r{specify only a node_name}) + end + + it 'raises when both node_name and platform given for an unknown action' do + allow($stdin).to receive(:read).and_return({ action: 'other', platform: 'foo', node_name: 'bar', inventory: tmpdir }.to_json) + expect { vagrant }.to raise_error(RuntimeError, %r{specify only one of}) + end end -end - -describe 'vagrant validation errors' do - include_context('with tmpdir') - - it 'raises when both node_name and platform given for provision' do - allow($stdin).to receive(:read).and_return({ action: 'provision', platform: 'foo', node_name: 'bar', inventory: tmpdir }.to_json) - expect { vagrant }.to raise_error(RuntimeError, /specify only a platform/) - end - - it 'raises when both node_name and platform given for tear_down' do - allow($stdin).to receive(:read).and_return({ action: 'tear_down', platform: 'foo', node_name: 'bar', inventory: tmpdir }.to_json) - expect { vagrant }.to raise_error(RuntimeError, /specify only a node_name/) - end - - it 'raises when both node_name and platform given for an unknown action' do - allow($stdin).to receive(:read).and_return({ action: 'other', platform: 'foo', node_name: 'bar', inventory: tmpdir }.to_json) - expect { vagrant }.to raise_error(RuntimeError, /specify only one of/) - end -end - -describe 'vagrant rescue' do - include_context('with tmpdir') - it 'exits 1 with an error JSON when provision raises' do - allow($stdin).to receive(:read).and_return({ platform: 'generic/ubuntu20', action: 'provision', inventory: tmpdir }.to_json) - allow(Open3).to receive(:capture3).with(%r{vagrant up}, any_args).and_return(['', 'vagrant failed!', 1]) - expect { vagrant }.to( - raise_error(SystemExit) { |e| expect(e.status).to eq(1) } - .and(output(/vagrant_failure/).to_stdout), - ) + describe 'rescue' do + it 'exits 1 with an error JSON when provision raises' do + allow($stdin).to receive(:read).and_return({ platform: 'generic/ubuntu20', action: 'provision', inventory: tmpdir }.to_json) + allow(Open3).to receive(:capture3).with(%r{vagrant up}, any_args).and_return(['', 'vagrant failed!', 1]) + expect { vagrant }.to( + raise_error(SystemExit) { |e| expect(e.status).to eq(1) } + .and(output(%r{vagrant_failure}).to_stdout), + ) + end end end diff --git a/spec/unit/task_helper_spec.rb b/spec/unit/task_helper_spec.rb index 7d531b61..873e2b66 100644 --- a/spec/unit/task_helper_spec.rb +++ b/spec/unit/task_helper_spec.rb @@ -25,14 +25,14 @@ it 'returns nil and prints a warning when the fog file does not exist' do allow(File).to receive(:file?).and_return(false) result = nil - expect { result = token_from_fogfile }.to output(/Cannot file fog file/).to_stdout + expect { result = token_from_fogfile }.to output(%r{Cannot file fog file}).to_stdout expect(result).to be_nil end it 'prints a warning and returns nil when reading the fog file raises an error' do allow(File).to receive(:file?).and_return(true) allow(YAML).to receive(:load_file).and_raise(StandardError, 'bad file') - expect { token_from_fogfile }.to output(/Failed to get token/).to_stdout + expect { token_from_fogfile }.to output(%r{Failed to get token}).to_stdout end end end From a3948208582a7b1526dae4cd447d526146a19c91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Br=C3=B3nach=20Falls?= <46711035+bronachfalls@users.noreply.github.com> Date: Mon, 7 Sep 2026 15:48:27 +0100 Subject: [PATCH 3/7] Update stub_request for AppVeyor CI job URL Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- spec/tasks/abs_spec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/tasks/abs_spec.rb b/spec/tasks/abs_spec.rb index 4e5b8f98..b61d2fc9 100644 --- a/spec/tasks/abs_spec.rb +++ b/spec/tasks/abs_spec.rb @@ -144,7 +144,8 @@ def with_env(env_vars) it 'raises an error if abs returns error response' it 'uses AppVeyor job URL when running in AppVeyor CI' do - stub_request(:post, 'https://abs-prod.k8s.infracore.puppet.net/api/v2/request') +stub_request(:post, 'https://abs-prod.k8s.infracore.puppet.net/api/v2/request') + .with { |request| JSON.parse(request.body).dig('job', 'tags', 'jenkins_build_url') == 'https://ci.appveyor.com/project/org/repo/build/job/123' } .to_return({ status: 202 }, { status: 200, body: response_body.to_json }) with_env('CI' => 'True', 'APPVEYOR' => 'True', 'APPVEYOR_REPO_NAME' => 'org/repo', 'APPVEYOR_JOB_ID' => '123') do expect(abs.task(**params)).to eq({ status: 'ok', nodes: 1 }) From 0fb01d2c1a06ddad87c074056a177545af5c9e5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Br=C3=B3nach=20Falls?= <46711035+bronachfalls@users.noreply.github.com> Date: Mon, 7 Sep 2026 15:51:22 +0100 Subject: [PATCH 4/7] Update expectation for provision method call Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- spec/tasks/provision_service_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/tasks/provision_service_spec.rb b/spec/tasks/provision_service_spec.rb index b2400e87..cf3fbabc 100644 --- a/spec/tasks/provision_service_spec.rb +++ b/spec/tasks/provision_service_spec.rb @@ -67,7 +67,7 @@ allow($stdin).to receive(:read).and_return('{"action":"provision","platform":"centos-8"}') runner = ProvisionService.new allow(ProvisionService).to receive(:new).and_return(runner) - allow(runner).to receive(:provision).and_return({ status: 'ok', node_name: 'centos-8' }) +expect(runner).to receive(:provision).with('centos-8', instance_of(InventoryHelper), nil, nil).and_return({ status: 'ok', node_name: 'centos-8' }) expect { ProvisionService.run }.to raise_error(SystemExit) { |e| expect(e.status).to eq(0) } end From 99ccaf7b27df318a440b2467799cb007d257c831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Br=C3=B3nach=20Falls?= <46711035+bronachfalls@users.noreply.github.com> Date: Mon, 7 Sep 2026 15:52:55 +0100 Subject: [PATCH 5/7] Enhance error handling test for non-200 responses Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- spec/tasks/provision_service_spec.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/tasks/provision_service_spec.rb b/spec/tasks/provision_service_spec.rb index cf3fbabc..ba846c5e 100644 --- a/spec/tasks/provision_service_spec.rb +++ b/spec/tasks/provision_service_spec.rb @@ -200,8 +200,9 @@ it 'exits 1 with parsed JSON error details on a non-200 response' do stub_request(:post, uri.to_s).to_return(status: 500, body: '{"error":"server error"}') - expect { svc.invoke_cloud_request({}, uri, nil, 'post', 0) }.to( - raise_error(SystemExit) { |e| expect(e.status).to eq(1) }, +expect { svc.invoke_cloud_request({}, uri, nil, 'post', 0) }.to( + raise_error(SystemExit) { |e| expect(e.status).to eq(1) } + .and(output(include('"body":{"error":"server error"}', '"body_json":true')).to_stdout), ) end From 23c39c4cddc2110a9644b17968d1e7ea04b85c1e Mon Sep 17 00:00:00 2001 From: bronachfalls Date: Mon, 7 Sep 2026 15:57:12 +0100 Subject: [PATCH 6/7] Fix indentation on reviewer-suggested changes Lines inserted at column 0 instead of matching surrounding indentation. Co-Authored-By: Claude Sonnet 4.6 --- spec/tasks/abs_spec.rb | 2 +- spec/tasks/provision_service_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/tasks/abs_spec.rb b/spec/tasks/abs_spec.rb index b61d2fc9..cda1b732 100644 --- a/spec/tasks/abs_spec.rb +++ b/spec/tasks/abs_spec.rb @@ -144,7 +144,7 @@ def with_env(env_vars) it 'raises an error if abs returns error response' it 'uses AppVeyor job URL when running in AppVeyor CI' do -stub_request(:post, 'https://abs-prod.k8s.infracore.puppet.net/api/v2/request') + stub_request(:post, 'https://abs-prod.k8s.infracore.puppet.net/api/v2/request') .with { |request| JSON.parse(request.body).dig('job', 'tags', 'jenkins_build_url') == 'https://ci.appveyor.com/project/org/repo/build/job/123' } .to_return({ status: 202 }, { status: 200, body: response_body.to_json }) with_env('CI' => 'True', 'APPVEYOR' => 'True', 'APPVEYOR_REPO_NAME' => 'org/repo', 'APPVEYOR_JOB_ID' => '123') do diff --git a/spec/tasks/provision_service_spec.rb b/spec/tasks/provision_service_spec.rb index ba846c5e..7695ed21 100644 --- a/spec/tasks/provision_service_spec.rb +++ b/spec/tasks/provision_service_spec.rb @@ -67,7 +67,7 @@ allow($stdin).to receive(:read).and_return('{"action":"provision","platform":"centos-8"}') runner = ProvisionService.new allow(ProvisionService).to receive(:new).and_return(runner) -expect(runner).to receive(:provision).with('centos-8', instance_of(InventoryHelper), nil, nil).and_return({ status: 'ok', node_name: 'centos-8' }) + expect(runner).to receive(:provision).with('centos-8', instance_of(InventoryHelper), nil, nil).and_return({ status: 'ok', node_name: 'centos-8' }) expect { ProvisionService.run }.to raise_error(SystemExit) { |e| expect(e.status).to eq(0) } end @@ -200,7 +200,7 @@ it 'exits 1 with parsed JSON error details on a non-200 response' do stub_request(:post, uri.to_s).to_return(status: 500, body: '{"error":"server error"}') -expect { svc.invoke_cloud_request({}, uri, nil, 'post', 0) }.to( + expect { svc.invoke_cloud_request({}, uri, nil, 'post', 0) }.to( raise_error(SystemExit) { |e| expect(e.status).to eq(1) } .and(output(include('"body":{"error":"server error"}', '"body_json":true')).to_stdout), ) From 893298bc66e8e78844986252b3c3eb9fd1b9a056 Mon Sep 17 00:00:00 2001 From: bronachfalls Date: Tue, 8 Sep 2026 15:49:08 +0100 Subject: [PATCH 7/7] Restore env vars to original values in with_env helper Previously the ensure block deleted every key unconditionally, removing pre-existing CI env vars for all subsequent examples. Now each key's original value is snapshotted before the block and restored after. Co-Authored-By: Claude Sonnet 4.6 --- spec/tasks/abs_spec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/tasks/abs_spec.rb b/spec/tasks/abs_spec.rb index cda1b732..8c858e49 100644 --- a/spec/tasks/abs_spec.rb +++ b/spec/tasks/abs_spec.rb @@ -26,10 +26,11 @@ include_context('with tmpdir') def with_env(env_vars) + original = env_vars.keys.each_with_object({}) { |k, h| h[k] = ENV.fetch(k, nil) } env_vars.each { |k, v| ENV[k] = v } yield ensure - env_vars.each { |k, _v| ENV.delete(k) } + original.each { |k, v| v.nil? ? ENV.delete(k) : ENV[k] = v } end before(:each) do