Add delivery metadata support, preferred channels, and enhanced agent validation
This commit introduces delivery metadata tracking for gateway commands, updates, and shells. Adds preferred delivery channel handling, refined validation for edge agents, improved relay management logic, and broker presence reporting. Includes schema changes, enhanced shell handling, and test coverage.
This commit is contained in:
@@ -19,6 +19,10 @@ it('queues admin commands, exposes agent poll/result handlers, and keeps heartbe
|
||||
expect($source)->toContain("\$gateway->discovery_status->set('READY');");
|
||||
expect($source)->toContain("\$gateway->discovery_status->set('FAILED');");
|
||||
expect($source)->toContain("\$this->buildUpdateCommandPayload(\$targetVersion, \$releaseChannel)");
|
||||
expect($source)->toContain("'UNINSTALL_AGENT'");
|
||||
expect($source)->toContain("public function queueUninstall");
|
||||
expect($source)->toContain("public function deleteGateway");
|
||||
expect($source)->toContain("private function softDeleteGatewayRelations");
|
||||
expect($source)->toContain("\$updateJob->status->set('DISPATCHING');");
|
||||
expect($source)->toContain("\$updateJob->status->set('VERIFYING');");
|
||||
expect($source)->toContain("\$updateJob->status->set(\$finalStatus ?? (\$ok ? 'COMPLETED' : 'FAILED'));");
|
||||
@@ -33,6 +37,8 @@ it('defines the dispatchable gateway guard and api-polled shell queue on the loa
|
||||
expect($reflection->getMethod('requireDispatchableGateway')->isPrivate())->toBeTrue();
|
||||
expect($reflection->hasMethod('queueDiscovery'))->toBeTrue();
|
||||
expect($reflection->hasMethod('queueUpdate'))->toBeTrue();
|
||||
expect($reflection->hasMethod('queueUninstall'))->toBeTrue();
|
||||
expect($reflection->hasMethod('deleteGateway'))->toBeTrue();
|
||||
expect($reflection->hasMethod('buildUpdateCommandPayload'))->toBeTrue();
|
||||
expect($reflection->hasMethod('applyHeartbeatUpdateLifecycle'))->toBeTrue();
|
||||
expect($reflection->hasMethod('dispatchRelayStatus'))->toBeTrue();
|
||||
|
||||
@@ -67,3 +67,75 @@ it('merges incoming heartbeat metadata with existing gateway metadata', function
|
||||
expect($source)->toContain("\$existingMetadata = (array)(\$gateway->metadata_json->value() ?? []);");
|
||||
expect($source)->toContain("\$gateway->metadata_json->set(array_merge(\$existingMetadata, (array)(\$payload['metadata'] ?? [])));");
|
||||
});
|
||||
|
||||
it('derives relay fallback and transport health details for degraded hybrid control planes', function (): void {
|
||||
$gateway = edge_gateway_manager::deriveGatewayRuntimeState([
|
||||
'status' => edge_gateway_manager::STATUS_ONLINE,
|
||||
'last_heartbeat_at' => '2026-04-08 10:04:30',
|
||||
'department_transport_mode' => edge_gateway_manager::TRANSPORT_MODE_GATEWAY,
|
||||
'metadata' => [
|
||||
'broker_presence' => [
|
||||
'connected' => false,
|
||||
'last_seen_at' => '2026-04-08 10:03:00',
|
||||
'last_error' => 'broker timeout',
|
||||
],
|
||||
],
|
||||
'operational_snapshot' => [
|
||||
'command_backlog' => 2,
|
||||
'shell_backlog' => 1,
|
||||
'update_backlog' => 1,
|
||||
],
|
||||
'bindings' => [
|
||||
[
|
||||
'id' => 1,
|
||||
'relay_id' => 'M-7',
|
||||
'device_id' => 'shelly-plus-01',
|
||||
'fallback_mode' => edge_gateway_manager::RELAY_FALLBACK_PREFER_LOCAL,
|
||||
],
|
||||
[
|
||||
'id' => 2,
|
||||
'relay_id' => 'M-7-CANARY',
|
||||
'device_id' => 'missing-device',
|
||||
'fallback_mode' => edge_gateway_manager::RELAY_FALLBACK_LOCAL_ONLY,
|
||||
],
|
||||
],
|
||||
'inventory' => [
|
||||
[
|
||||
'device_id' => 'shelly-plus-01',
|
||||
'online' => true,
|
||||
'last_seen_at' => '2026-04-08 09:55:00',
|
||||
],
|
||||
],
|
||||
'recent_commands' => [
|
||||
[
|
||||
'status' => 'COMPLETED',
|
||||
'command_type' => 'DISCOVER_SHELLY',
|
||||
'completed_at' => '2026-04-08 10:02:00',
|
||||
],
|
||||
],
|
||||
'recent_shell_sessions' => [
|
||||
[
|
||||
'opened_at' => '2026-04-08 10:03:00',
|
||||
],
|
||||
],
|
||||
], strtotime('2026-04-08 10:05:00'));
|
||||
|
||||
expect($gateway['channel_status']['command']['active'])->toBe(edge_gateway_manager::DELIVERY_CHANNEL_API);
|
||||
expect($gateway['channel_status']['broker']['state'])->toBe(edge_gateway_manager::STATUS_OFFLINE);
|
||||
expect($gateway['relay_health'][0]['execution_path'])->toBe('cloud');
|
||||
expect($gateway['relay_health'][0]['reason'])->toBe('device_stale');
|
||||
expect($gateway['relay_health'][0]['recommended_action'])->toBe('retry_discovery');
|
||||
expect($gateway['relay_health'][1]['execution_path'])->toBe('local');
|
||||
expect($gateway['relay_health'][1]['reason'])->toBe('device_missing');
|
||||
expect($gateway['fallback_summary']['cloud_relays'])->toBe(1);
|
||||
expect($gateway['fallback_summary']['local_only_relays'])->toBe(1);
|
||||
expect($gateway['transport_health']['status'])->toBe(edge_gateway_manager::STATUS_DEGRADED);
|
||||
expect($gateway['transport_health']['recommended_action'])->toBe('retry_discovery');
|
||||
expect($gateway['last_successful_discovery_at'])->toBe('2026-04-08 10:02:00');
|
||||
expect($gateway['last_successful_shell_at'])->toBe('2026-04-08 10:03:00');
|
||||
expect($gateway['backlog_depth'])->toBe([
|
||||
'commands' => 2,
|
||||
'shell_actions' => 1,
|
||||
'updates' => 1,
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ function with_edge_gateway_server_state(array $server, callable $callback): void
|
||||
{
|
||||
$originalServer = $_SERVER;
|
||||
$originalPublicApiUrl = getenv('EDGE_PUBLIC_API_URL');
|
||||
$originalPublicBrokerUrl = getenv('EDGE_PUBLIC_BROKER_URL');
|
||||
|
||||
$_SERVER = $server;
|
||||
|
||||
@@ -27,6 +28,11 @@ function with_edge_gateway_server_state(array $server, callable $callback): void
|
||||
} else {
|
||||
putenv('EDGE_PUBLIC_API_URL=' . $originalPublicApiUrl);
|
||||
}
|
||||
if ($originalPublicBrokerUrl === false) {
|
||||
putenv('EDGE_PUBLIC_BROKER_URL');
|
||||
} else {
|
||||
putenv('EDGE_PUBLIC_BROKER_URL=' . $originalPublicBrokerUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +58,7 @@ it('builds install script urls with forwarded https scheme when proxied', functi
|
||||
expect($script)->toContain('"commandPollTimeoutSeconds":20');
|
||||
expect($script)->toContain('"shellActionPollTimeoutSeconds":20');
|
||||
expect($script)->toContain('"updateVerificationTimeoutSeconds":45');
|
||||
expect($script)->not->toContain('"brokerUrl"');
|
||||
expect($script)->toContain('"brokerUrl":"https://api.truckwash.io:4300"');
|
||||
expect($script)->not->toContain('Undefined variable $INSTALL_DIR');
|
||||
});
|
||||
});
|
||||
@@ -80,7 +86,7 @@ it('infers https for the staging api host when only the https port is present',
|
||||
expect($manager->getApiBaseUrl())->toBe('https://api.truckwash.io:4433');
|
||||
expect($script)->toContain('"apiUrl":"https://api.truckwash.io:4433"');
|
||||
expect($script)->toContain('"serviceName":"truckwash-edge-agent.service"');
|
||||
expect($script)->not->toContain('"brokerUrl"');
|
||||
expect($script)->toContain('"brokerUrl":"https://api.truckwash.io:4300"');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -90,11 +96,13 @@ it('prefers EDGE_PUBLIC_API_URL when explicitly configured', function (): void {
|
||||
'HTTP_X_FORWARDED_PROTO' => 'http',
|
||||
], function (): void {
|
||||
putenv('EDGE_PUBLIC_API_URL=https://edge.example.test/api');
|
||||
putenv('EDGE_PUBLIC_BROKER_URL=https://broker.edge.example.test');
|
||||
|
||||
$manager = new EdgeGatewayManagerUrlHarness();
|
||||
|
||||
expect($manager->getApiBaseUrl())->toBe('https://edge.example.test/api');
|
||||
expect($manager->buildInstallScriptUrl('token-1'))->toBe('https://edge.example.test/api/edge-agent/install.sh?token=token-1');
|
||||
expect($manager->buildInstallScript('token-1'))->toContain('"brokerUrl":"https://broker.edge.example.test"');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -10,12 +10,14 @@ it('registers the edge gateway management REST endpoints', function (): void {
|
||||
expect($route)->toContain("'/edge-gateways/{id}/discovery'");
|
||||
expect($route)->toContain("'/edge-gateways/{id}/bindings'");
|
||||
expect($route)->toContain("'/edge-gateways/{id}/update-jobs'");
|
||||
expect($route)->toContain("'/edge-gateways/{id}/uninstall'");
|
||||
expect($route)->toContain("'/edge-gateways/{id}/shell-sessions'");
|
||||
expect($route)->toContain("'/edge-gateways/{id}/shell-sessions/{sessionId}/events'");
|
||||
expect($route)->toContain("'/edge-gateways/{id}/shell-sessions/{sessionId}/input'");
|
||||
expect($route)->toContain("'/edge-gateways/{id}/shell-sessions/{sessionId}/resize'");
|
||||
expect($route)->toContain("'/edge-gateways/{id}/shell-sessions/{sessionId}/close'");
|
||||
expect($route)->toContain("'/edge-gateways/{id}/rotate-credentials'");
|
||||
expect($route)->toContain("'/edge-gateways/{id}'");
|
||||
expect($route)->toContain("'/departments/{id}/gateway-cutover'");
|
||||
});
|
||||
|
||||
@@ -34,3 +36,15 @@ it('registers public installer, claim, heartbeat, command polling, and shell pol
|
||||
expect($route)->toContain("'/edge-agent/gateways/{id}/shell-actions/{actionId}/result'");
|
||||
expect($route)->toContain("'/edge-agent/gateways/{id}/shell-sessions/{sessionId}/events'");
|
||||
});
|
||||
|
||||
it('registers authenticated internal broker validation and presence endpoints', function (): void {
|
||||
$route = file_get_contents(app_path('routes/edgeGatewaysRoute.php'));
|
||||
|
||||
expect($route)->not->toBeFalse();
|
||||
expect($route)->toContain("'/edge-agent/internal/gateways/{id}/validate'");
|
||||
expect($route)->toContain("'/edge-agent/internal/shell-sessions/validate'");
|
||||
expect($route)->toContain("'/edge-agent/internal/gateways/{id}/presence'");
|
||||
expect($route)->toContain("'/edge-agent/internal/shell-sessions/close'");
|
||||
expect($route)->toContain('HTTP_X_EDGE_BROKER_SECRET');
|
||||
expect($route)->toContain('EDGE_BROKER_SHARED_SECRET');
|
||||
});
|
||||
|
||||
@@ -27,4 +27,6 @@ it('stores edge gateway heartbeats, bindings, shell transcripts, and shell polli
|
||||
expect($bootstrapContent)->toContain('payload_json JSON NULL');
|
||||
expect($bootstrapContent)->toContain('event_type VARCHAR(32) NOT NULL');
|
||||
expect($bootstrapContent)->toContain('context_json JSON NULL');
|
||||
expect(substr_count($bootstrapContent, 'delivery_json JSON NULL'))->toBeGreaterThanOrEqual(3);
|
||||
expect($bootstrapContent)->toContain('ADD COLUMN delivery_json JSON NULL');
|
||||
});
|
||||
|
||||
@@ -7,7 +7,10 @@ it('rewrites edge gateway shell transport to API polling queues', function (): v
|
||||
expect($managerSource)->not->toBeFalse();
|
||||
expect($routeSource)->not->toBeFalse();
|
||||
|
||||
expect($managerSource)->toContain("'transport' => 'API_POLLING'");
|
||||
expect($managerSource)->toContain("'transport' => self::DELIVERY_CHANNEL_API");
|
||||
expect($managerSource)->toContain("'transport_path' => self::DELIVERY_CHANNEL_API");
|
||||
expect($managerSource)->toContain("'reconnect_state' => 'PENDING'");
|
||||
expect($managerSource)->toContain("'preferred_channel' => self::DELIVERY_CHANNEL_API");
|
||||
expect($managerSource)->toContain('private function createShellActionJob');
|
||||
expect($managerSource)->toContain('private function claimNextShellActionJob');
|
||||
expect($managerSource)->toContain('private function appendShellEvent');
|
||||
@@ -25,6 +28,9 @@ it('rewrites edge gateway shell transport to API polling queues', function (): v
|
||||
expect($routeSource)->toContain("'/edge-agent/gateways/{id}/shell-actions/poll'");
|
||||
expect($routeSource)->toContain("'/edge-agent/gateways/{id}/shell-actions/{actionId}/result'");
|
||||
expect($routeSource)->toContain("'/edge-agent/gateways/{id}/shell-sessions/{sessionId}/events'");
|
||||
expect($routeSource)->not->toContain("'/edge-agent/internal/agent/auth'");
|
||||
expect($routeSource)->not->toContain("'/edge-agent/internal/shell/auth'");
|
||||
expect($routeSource)->toContain("'/edge-agent/internal/gateways/{id}/validate'");
|
||||
expect($routeSource)->toContain("'/edge-agent/internal/shell-sessions/validate'");
|
||||
expect($routeSource)->toContain("'/edge-agent/internal/gateways/{id}/presence'");
|
||||
expect($routeSource)->toContain("'/edge-agent/internal/shell-sessions/close'");
|
||||
expect($routeSource)->toContain('requireBrokerSharedSecret');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user