Http::response([ 'task' => [ 'id' => $taskId, 'title' => 'Real Title 200', 'description' => 'Real Description 200 Content', 'target_machine_id' => 'machine-205-dev', 'metadata' => [ 'opId' => 'op-123', 'expectedReport' => [ 'consolidated_unit_id' => 'UNIT-200', 'note_contains' => 'NOTE_200_OK' ] ] ] ], 200), 'http://192.168.0.53:4174/api/reports' => Http::response(['id' => 'rep-200'], 201), 'http://192.168.0.53:4174/api/tasks/status' => Http::response(['status' => 'done'], 200), 'http://192.168.0.53:4174/api/machines/heartbeat' => Http::response(['status' => 'online'], 200), ]); $mockScript = tempnam(sys_get_temp_dir(), 'mock_wrapper_real_') . '.sh'; $scriptContent = <<<'BASH' #!/usr/bin/env bash INPUT="$(cat -)" if [[ "$INPUT" != *"Real Description 200 Content"* ]]; then echo "ERROR: description missing from stdin" >&2 exit 1 fi if [[ "$INPUT" != *"EXPECTED_REPORT:"* ]]; then echo "ERROR: expectedReport missing from stdin" >&2 exit 1 fi if [[ "$INPUT" != *"UNIT-200"* ]]; then echo "ERROR: UNIT-200 missing from stdin" >&2 exit 1 fi cat << JSON { "ok": true, "parsed": { "task_id": "task-test-real-200", "esito_205": "riuscito", "repository": "ssh://git@192.168.0.53:2222/michele/netgescon-day0.git", "branch": "stabilization/205-zero", "commit": "0ac85f8c8502d9bb4e015ee6b15a452ef3fd1422", "consolidated_unit_id": "UNIT-200", "blocco_dati": "no", "dates_used": ["2026-08-10"], "test_eseguiti": ["real task test"], "note": ["NOTE_200_OK"], "absorbed_legacy_fragments": ["frag-200"], "open_legacy_fragments": [], "raw_text": "NOTE_200_OK" } } JSON exit 0 BASH; file_put_contents($mockScript, $scriptContent); chmod($mockScript, 0755); $exitCode = $this->artisan('netgescon:control-tower-poll', [ '--task-id' => $taskId, '--wrapper-script' => $mockScript, ])->run(); @unlink($mockScript); expect($exitCode)->toBe(Command::SUCCESS); Http::assertSent(function ($request) use ($taskId) { return str_contains($request->url(), '/api/tasks/' . $taskId); }); Http::assertSent(function ($request) use ($taskId) { return str_contains($request->url(), '/api/reports') && ($request['task_id'] ?? null) === $taskId && str_contains($request['raw_text'] ?? '', 'NOTE_200_OK'); }); }); it('fails and does not execute wrapper or publish report when task GET returns 403', function () { $taskId = 'task-test-403'; Http::fake([ 'http://192.168.0.53:4174/api/tasks/task-test-403*' => Http::response(['error' => 'Forbidden'], 403), ]); $mockScript = tempnam(sys_get_temp_dir(), 'mock_wrapper_403_') . '.sh'; file_put_contents($mockScript, "#!/usr/bin/env bash\necho 'Should not execute' >&2\nexit 1\n"); chmod($mockScript, 0755); $exitCode = $this->artisan('netgescon:control-tower-poll', [ '--task-id' => $taskId, '--wrapper-script' => $mockScript, ])->run(); @unlink($mockScript); expect($exitCode)->toBe(Command::FAILURE); Http::assertNotSent(function ($request) { return str_contains($request->url(), '/api/reports'); }); }); it('fails and does not execute wrapper or publish report when task GET returns 404', function () { $taskId = 'task-test-404'; Http::fake([ 'http://192.168.0.53:4174/api/tasks/task-test-404*' => Http::response(['error' => 'Not Found'], 404), ]); $mockScript = tempnam(sys_get_temp_dir(), 'mock_wrapper_404_') . '.sh'; file_put_contents($mockScript, "#!/usr/bin/env bash\necho 'Should not execute' >&2\nexit 1\n"); chmod($mockScript, 0755); $exitCode = $this->artisan('netgescon:control-tower-poll', [ '--task-id' => $taskId, '--wrapper-script' => $mockScript, ])->run(); @unlink($mockScript); expect($exitCode)->toBe(Command::FAILURE); Http::assertNotSent(function ($request) { return str_contains($request->url(), '/api/reports'); }); }); it('fails when returned task ID differs from requested task ID', function () { $taskId = 'task-test-mismatch'; Http::fake([ 'http://192.168.0.53:4174/api/tasks/task-test-mismatch*' => Http::response([ 'task' => [ 'id' => 'different-id-999', 'title' => 'Title', 'description' => 'Desc', 'target_machine_id' => 'machine-205-dev', 'metadata' => [] ] ], 200), ]); $mockScript = tempnam(sys_get_temp_dir(), 'mock_wrapper_mismatch_') . '.sh'; file_put_contents($mockScript, "#!/usr/bin/env bash\necho 'Should not execute' >&2\nexit 1\n"); chmod($mockScript, 0755); $exitCode = $this->artisan('netgescon:control-tower-poll', [ '--task-id' => $taskId, '--wrapper-script' => $mockScript, ])->run(); @unlink($mockScript); expect($exitCode)->toBe(Command::FAILURE); Http::assertNotSent(function ($request) { return str_contains($request->url(), '/api/reports'); }); }); it('does not publish report when wrapper exit code is non-zero and displays stdout and stderr', function () { $taskId = 'task-test-err-001'; Http::fake([ 'http://192.168.0.53:4174/api/tasks/task-test-err-001*' => Http::response([ 'task' => [ 'id' => $taskId, 'title' => 'Title Err', 'description' => 'Desc Err', 'target_machine_id' => 'machine-205-dev', 'metadata' => [] ] ], 200), ]); $mockScript = tempnam(sys_get_temp_dir(), 'mock_wrapper_err_') . '.sh'; $jsonErrorOutput = json_encode(['ok' => false, 'missing' => ['task_id']], JSON_PRETTY_PRINT); $scriptContent = <<&2 cat << JSON {$jsonErrorOutput} JSON exit 1 BASH; file_put_contents($mockScript, $scriptContent); chmod($mockScript, 0755); $this->artisan('netgescon:control-tower-poll', [ '--task-id' => $taskId, '--wrapper-script' => $mockScript, ]) ->expectsOutputToContain('WRAPPER AGY FALLITO') ->expectsOutputToContain('EXIT_CODE: 1') ->expectsOutputToContain('STDOUT:') ->expectsOutputToContain('"ok": false') ->expectsOutputToContain('STDERR:') ->expectsOutputToContain('Standard error log message') ->assertExitCode(Command::FAILURE); @unlink($mockScript); Http::assertNotSent(function ($request) { return str_contains($request->url(), '/api/reports'); }); });