script('wg pubkey', CommandResult::success('HOSTPUBKEY0000=')); $s['shell']->script('uname -r', CommandResult::success('6.8.12-4-pve')); $s['shell']->script('pveum user token add', CommandResult::success( json_encode(['full-tokenid' => 'automation@pve!clupilot', 'value' => 'tok-secret-123']) )); $host = app(StartHostOnboarding::class)->run([ 'name' => 'pve-fsn-9', 'datacenter' => 'fsn', 'public_ip' => '203.0.113.9', 'root_password' => 'rootpw', ]); $run = ProvisioningRun::query()->where('subject_id', $host->id)->firstOrFail(); $runner = app(RunRunner::class); for ($i = 0; $i < 40; $i++) { $run->refresh(); if (in_array($run->status, ['completed', 'failed'], true)) { break; } $runner->advance($run); } $run->refresh(); $host->refresh(); expect($run->status)->toBe('completed') ->and($run->error)->toBeNull() ->and($host->status)->toBe('active') ->and($host->wg_ip)->not->toBeNull() ->and($host->total_gb)->toBe(1024) ->and($host->cpu_cores)->toBe(16) ->and($host->api_token_ref)->toContain('automation@pve') // external resources created exactly once ->and(RunResource::where('run_id', $run->id)->where('kind', 'wg_peer')->count())->toBe(1) ->and(RunResource::where('run_id', $run->id)->where('kind', 'pve_token')->count())->toBe(1) // one-time root password scrubbed from state ->and($run->context('root_password'))->toBeNull(); }); it('does not duplicate external resources when a step re-runs after a crash', function () { Queue::fake(); $s = fakeServices(); $s['shell']->script('wg pubkey', CommandResult::success('HOSTPUBKEY0000=')); $s['shell']->script('uname -r', CommandResult::success('6.8.12-4-pve')); $s['shell']->script('pveum user token add', CommandResult::success( json_encode(['full-tokenid' => 'automation@pve!clupilot', 'value' => 'tok-secret-123']) )); $host = app(StartHostOnboarding::class)->run([ 'name' => 'pve-fsn-10', 'datacenter' => 'fsn', 'public_ip' => '203.0.113.10', 'root_password' => 'rootpw', ]); $run = ProvisioningRun::query()->where('subject_id', $host->id)->firstOrFail(); $runner = app(RunRunner::class); // Advance a few steps, then simulate a crash by rewinding the run to the // WireGuard step and replaying it — the breadcrumb must prevent duplicates. for ($i = 0; $i < 6; $i++) { $run->refresh(); $runner->advance($run); } $run->refresh(); $wgIndex = array_search( \App\Provisioning\Steps\Host\ConfigureWireguard::class, config('provisioning.pipelines.host'), true, ); $run->update(['current_step' => $wgIndex, 'status' => 'running']); $runner->advance($run->fresh()); expect(RunResource::where('run_id', $run->id)->where('kind', 'wg_peer')->count())->toBe(1); });