From 86928f069bd5c4d46fbf3823c8bff91c3bc78dc5 Mon Sep 17 00:00:00 2001 From: michele Date: Mon, 10 Aug 2026 09:16:21 +0200 Subject: [PATCH] fix(ops): fetch real task details via GET /api/tasks/{taskId} and prevent synthetic task fallbacks --- .../Commands/ControlTowerPollCommand.php | 74 ++++++- tests/Feature/ControlTowerPollCommandTest.php | 197 ++++++++++++------ 2 files changed, 195 insertions(+), 76 deletions(-) diff --git a/app/Console/Commands/ControlTowerPollCommand.php b/app/Console/Commands/ControlTowerPollCommand.php index 39bca30..cf71a6a 100644 --- a/app/Console/Commands/ControlTowerPollCommand.php +++ b/app/Console/Commands/ControlTowerPollCommand.php @@ -31,7 +31,11 @@ public function handle(): int $task = null; if ($taskIdOpt) { - $task = ['id' => $taskIdOpt, 'title' => "Task {$taskIdOpt}", 'description' => "Esecuzione manuale task {$taskIdOpt}"]; + $task = $this->fetchTaskById($towerBase, $taskIdOpt, $machineId, $token); + if (! $task) { + $this->error("❌ RECUPERO TASK REALE FALLITO: Task {$taskIdOpt} non trovato o errore HTTP. Nessun fallback sintetico ammesso."); + return self::FAILURE; + } } else { $task = $this->fetchNextTask($towerBase, $machineId, $token); if (! $task) { @@ -39,10 +43,37 @@ public function handle(): int $task = $this->fetchCurrentTaskFromPacket($towerBase, $machineId, $token); } } - $taskId = $task['id'] ?? 'task-simulated-runner-test'; - $title = $task['title'] ?? 'Task di Test Audit Cespiti'; - $desc = $task['description'] ?? 'Esecuzione automatica via Control Tower Poller'; - $meta = $task['metadata'] ?? []; + + if (! $task || ! is_array($task)) { + $this->error('❌ TASK NON VALIDO O MANCANTE.'); + return self::FAILURE; + } + + $taskId = $task['id'] ?? null; + if (! $taskId) { + $this->error('❌ TASK NON VALIDO: Campo id mancante.'); + return self::FAILURE; + } + + if ($taskIdOpt && $taskId !== $taskIdOpt) { + $this->error("❌ DISALLINEAMENTO TASK_ID: ID restituito '{$taskId}' non coincide con ID richiesto '{$taskIdOpt}'."); + return self::FAILURE; + } + + $assignedMachine = $task['target_machine_id'] ?? $task['assigned_machine_id'] ?? $task['machine_id'] ?? null; + if ($assignedMachine !== null && $assignedMachine !== $machineId && $assignedMachine !== 'machine-205-dev') { + $this->error("❌ MACCHINA DISALLINEATA: Task {$taskId} e assegnato a '{$assignedMachine}', non a '{$machineId}'."); + return self::FAILURE; + } + + $title = $task['title'] ?? null; + $desc = $task['description'] ?? null; + $meta = $task['metadata'] ?? null; + + if ($title === null || $desc === null || $meta === null) { + $this->error("❌ TASK INCOMPLETO: title, description o metadata mancanti per task {$taskId}."); + return self::FAILURE; + } if (isset($task['status']) && in_array($task['status'], ['done', 'blocked'], true)) { $this->info("ℹ️ Task {$taskId} e gia in stato '{$task['status']}'. Esecuzione ignorata (idempotenza)."); @@ -60,12 +91,20 @@ public function handle(): int return self::SUCCESS; } - $promptInput = implode("\n", [ + $expectedReport = is_array($meta) ? ($meta['expectedReport'] ?? $meta['expected_report'] ?? null) : null; + + $promptParts = [ "TASK_ID: {$taskId}", "TITOLO: {$title}", "DESCRIZIONE:\n{$desc}", - "METADATA:\n" . json_encode($meta, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), - ]); + "METADATA:\n" . (is_array($meta) ? json_encode($meta, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) : $meta), + ]; + + if ($expectedReport !== null) { + $promptParts[] = "EXPECTED_REPORT:\n" . (is_array($expectedReport) || is_object($expectedReport) ? json_encode($expectedReport, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) : $expectedReport); + } + + $promptInput = implode("\n\n", $promptParts); $defaultWrapperScript = '/home/michele/netgescon-day0-backup/scripts/ops/antigravity-cli/run_205_followup_via_agy.sh'; $wrapperScript = (string) ($this->option('wrapper-script') ?: $defaultWrapperScript); @@ -201,6 +240,25 @@ private function loadMachineEnv(): array return $vars; } + private function fetchTaskById(string $towerBase, string $taskId, string $machineId, string $token): ?array + { + try { + $response = Http::timeout(10)->get("{$towerBase}/api/tasks/{$taskId}", [ + 'machine_id' => $machineId, + 'token' => $token, + ]); + if ($response->successful()) { + $data = $response->json(); + return $data['task'] ?? ($data['id'] ?? null ? $data : null); + } else { + $this->error("❌ HTTP {$response->status()} durante il recupero del task {$taskId}"); + } + } catch (Throwable $e) { + $this->error("❌ Errore eccezione recupero task {$taskId}: {$e->getMessage()}"); + } + return null; + } + private function fetchNextTask(string $towerBase, string $machineId, string $token): ?array { try { diff --git a/tests/Feature/ControlTowerPollCommandTest.php b/tests/Feature/ControlTowerPollCommandTest.php index ce0337c..743ef49 100644 --- a/tests/Feature/ControlTowerPollCommandTest.php +++ b/tests/Feature/ControlTowerPollCommandTest.php @@ -7,30 +7,45 @@ Http::preventStrayRequests(); }); -it('verifies process environment HOME PATH AGY_MODE, absolute path, and preserves stdout/stderr', function () { - $taskId = 'task-test-env-001'; +it('fetches real task via GET 200 and passes description, metadata, and expectedReport to wrapper', function () { + $taskId = 'task-test-real-200'; Http::fake([ - 'http://192.168.0.53:4174/api/tasks/next*' => Http::response(['task' => ['id' => $taskId, 'title' => 'Test Env', 'description' => 'Desc']], 200), - 'http://192.168.0.53:4174/api/reports' => Http::response(['id' => 'rep-env-123'], 201), + 'http://192.168.0.53:4174/api/tasks/task-test-real-200*' => 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_env_') . '.sh'; + $mockScript = tempnam(sys_get_temp_dir(), 'mock_wrapper_real_') . '.sh'; $scriptContent = <<<'BASH' #!/usr/bin/env bash -if [ -n "${ANTIGRAVITY_AGENT:-}" ] || [ -n "${ANTIGRAVITY_CSRF_TOKEN:-}" ]; then - echo "ERROR: ANTIGRAVITY_ variables found" >&2 +INPUT="$(cat -)" +if [[ "$INPUT" != *"Real Description 200 Content"* ]]; then + echo "ERROR: description missing from stdin" >&2 exit 1 fi -if [ "$AGY_MODE" != "plan" ]; then - echo "ERROR: AGY_MODE != plan" >&2 +if [[ "$INPUT" != *"EXPECTED_REPORT:"* ]]; then + echo "ERROR: expectedReport missing from stdin" >&2 exit 1 fi -if [ -z "$HOME" ] || [ -z "$PATH" ]; then - echo "ERROR: HOME or PATH missing" >&2 +if [[ "$INPUT" != *"UNIT-200"* ]]; then + echo "ERROR: UNIT-200 missing from stdin" >&2 exit 1 fi @@ -38,19 +53,19 @@ { "ok": true, "parsed": { - "task_id": "task-test-env-001", + "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": "SMOKE-24825CBB", + "consolidated_unit_id": "UNIT-200", "blocco_dati": "no", "dates_used": ["2026-08-10"], - "test_eseguiti": ["env test"], - "note": ["ENV_OK"], - "absorbed_legacy_fragments": ["frag-001"], + "test_eseguiti": ["real task test"], + "note": ["NOTE_200_OK"], + "absorbed_legacy_fragments": ["frag-200"], "open_legacy_fragments": [], - "raw_text": "ENV_OK" + "raw_text": "NOTE_200_OK" } } JSON @@ -69,10 +84,97 @@ 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'] ?? '', 'ENV_OK'); + 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'); }); }); @@ -80,7 +182,15 @@ $taskId = 'task-test-err-001'; Http::fake([ - 'http://192.168.0.53:4174/api/tasks/next*' => Http::response(['task' => ['id' => $taskId]], 200), + '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'; @@ -116,52 +226,3 @@ return str_contains($request->url(), '/api/reports'); }); }); - -it('publishes report when wrapper returns valid parsed payload', function () { - $taskId = 'task-test-001'; - - Http::fake([ - 'http://192.168.0.53:4174/api/tasks/next*' => Http::response(['task' => ['id' => $taskId, 'title' => 'Test Task', 'description' => 'Test Desc']], 200), - 'http://192.168.0.53:4174/api/reports' => Http::response(['id' => 'rep-123'], 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_') . '.sh'; - $jsonOutput = json_encode([ - 'ok' => true, - 'parsed' => [ - 'task_id' => $taskId, - 'esito_205' => 'riuscito', - 'repository' => 'ssh://git@192.168.0.53:2222/michele/netgescon-day0.git', - 'branch' => 'stabilization/205-zero', - 'commit' => 'a159c03799bd942bb6c8a74e50eb9ddf9a74aefb', - 'consolidated_unit_id' => '1545', - 'blocco_dati' => 'no', - 'dates_used' => ['2026-08-10'], - 'test_eseguiti' => ['NETTOWER_AGY_ULTRA_WRAPPER_OK'], - 'note' => ['Test OK'], - 'absorbed_legacy_fragments' => ['id_cond=12'], - 'open_legacy_fragments' => [], - 'raw_text' => 'NETTOWER_AGY_ULTRA_WRAPPER_OK', - ], - ], JSON_UNESCAPED_SLASHES); - - file_put_contents($mockScript, "#!/usr/bin/env bash\ncat - >/dev/null\necho '{$jsonOutput}'\nexit 0\n"); - 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/reports') && - ($request['task_id'] ?? null) === $taskId && - str_contains($request['raw_text'] ?? '', 'NETTOWER_AGY_ULTRA_WRAPPER_OK'); - }); -});