Surface discovery abstention reason instead of an Unknown Service dead-end

When discovery has no in-guest command evidence, the backend abstains (empty
service, confidence 0) rather than confabulate a service, and explains why in
ai_reasoning (e.g. 'Discovery could not run commands on this resource... Enable
Pulse Commands (Settings → Infrastructure)...'). The Discovery tab discarded
that explanation: hasMeaningfulDiscoveryContext() ignores ai_reasoning, so an
abstention failed the render gate and fell into the generic 'Unknown Service /
Discovery completed but couldnt identify a known service' state with no reason
and no next step. After a 'Discovery complete' run, the user saw a dead-end.

The not-identified branch now surfaces ai_reasoning (the actionable
explanation) and a clickable 'Open Settings → Infrastructure' link when command
execution isn't confirmed enabled, with a neutral 'Service not identified'
heading. Identified guests are unaffected (they render via the valid-discovery
block). Regression test added; verified live against a mock guest that abstains.
This commit is contained in:
rcourtman
2026-06-09 16:08:39 +01:00
parent 16f791a656
commit a4602fe731
2 changed files with 68 additions and 3 deletions
@@ -700,10 +700,26 @@ export const DiscoveryTab: Component<DiscoveryTabProps> = (props) => {
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<p class="text-sm">Unknown Service</p>
<p class="text-xs text-muted mt-1">
Discovery completed but couldn't identify a known service.
<p class="text-sm">
{discovery()?.ai_reasoning ? 'Service not identified' : 'Unknown Service'}
</p>
<p class="text-xs text-muted mt-1">
{/* The backend abstains (empty service, confidence 0) rather than
confabulate a service when it has no in-guest command evidence,
and explains why in ai_reasoning. Surface that explanation
instead of a dead-end "couldn't identify a known service", so a
completed-but-empty scan tells the user what happened and how to
get real results. */}
{discovery()?.ai_reasoning ||
"Discovery completed but couldn't identify a known service."}
</p>
<Show when={discovery()?.ai_reasoning && props.commandsEnabled !== true}>
<p class="text-xs mt-2">
<a href={commandSettingsTarget.href} class="underline hover:no-underline">
Open {commandSettingsTarget.label}
</a>
</p>
</Show>
<Show when={discovery()?.updated_at}>
<p class="text-xs text-muted mt-2">
Last scanned: {formatDiscoveryAge(discovery()!.updated_at)}
@@ -139,6 +139,55 @@ describe('DiscoveryTab', () => {
expect(screen.queryByText('Unknown Container')).toBeNull();
});
it('surfaces the abstention reason and a settings link when a completed scan had no command evidence', async () => {
// The backend abstains (empty service, confidence 0) rather than confabulate
// a service when it has no in-guest command evidence, and explains why in
// ai_reasoning. The tab must surface that explanation + an actionable settings
// link instead of a dead-end "couldn't identify a known service".
vi.mocked(discoveryApi.getDiscovery).mockResolvedValue({
id: 'system-container:minipc:112',
resource_type: 'system-container',
resource_id: '112',
target_id: 'minipc',
hostname: 'debian-go',
service_type: '',
service_name: '',
service_version: '',
category: 'unknown',
cli_access: 'pct exec 112 -- /bin/bash',
facts: [],
config_paths: [],
data_paths: [],
log_paths: [],
ports: [],
user_notes: '',
user_secrets: {},
confidence: 0,
ai_reasoning:
'Discovery could not run commands on this resource, so its service was not identified. Enable "Pulse Commands" for the host agent (Settings → Infrastructure) to run real discovery instead of guessing.',
discovered_at: '2026-06-09T00:00:00Z',
updated_at: '2026-06-09T00:00:00Z',
scan_duration: 0,
});
render(() => (
<DiscoveryTab
resourceType="system-container"
agentId="minipc"
resourceId="112"
hostname="debian-go"
/>
));
expect(await screen.findByText('Service not identified')).toBeInTheDocument();
expect(screen.getByText(/Discovery could not run commands on this resource/i)).toBeInTheDocument();
const settingsLink = screen.getByRole('link', { name: /Open Settings → Infrastructure/i });
expect(settingsLink).toHaveAttribute('href', '/settings/infrastructure');
expect(
screen.queryByText("Discovery completed but couldn't identify a known service"),
).toBeNull();
});
it('shows a drawer run action and triggers discovery for the current resource', async () => {
const discovered: ResourceDiscovery = {
id: 'discovery-1',