mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-25 04:33:03 +00:00
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:
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user