mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-31 21:30:51 +00:00
Trim dead resource graph surface
This commit is contained in:
parent
cb20115799
commit
cc806171dc
24 changed files with 100 additions and 360 deletions
|
|
@ -151,8 +151,8 @@ surfaces can keep row summaries aligned without re-inferring totals from
|
|||
consumer-local slices.
|
||||
That same shared facet bundle now also carries grouped `recentChangeKinds`
|
||||
counts by canonical change kind, so the lifecycle-adjacent detail surfaces can
|
||||
report restart, anomaly, relationship, and capability distribution without
|
||||
rebuilding timeline math in the browser.
|
||||
report restart, anomaly, and other timeline distribution without rebuilding
|
||||
timeline math in the browser.
|
||||
That same shared facet bundle now also carries grouped
|
||||
`recentChangeSourceTypes` counts by canonical source type, so the
|
||||
lifecycle-adjacent detail surfaces can distinguish platform events, pulse
|
||||
|
|
|
|||
|
|
@ -265,13 +265,13 @@ The canonical recent-change sentence formatting also lives in
|
|||
`internal/unifiedresources.FormatResourceChangeSummary`, so AI runtime prompt
|
||||
sections and Patrol seed context reuse the same change wording instead of
|
||||
keeping another lane-local formatter.
|
||||
The confidence percentage wording used by the drawer's relationship and
|
||||
change timeline rows also flows through a shared frontend formatter, so the
|
||||
same `50%`-style labels stay consistent across resource-graph surfaces
|
||||
instead of being re-derived in the component.
|
||||
The remaining fallback token humanization used by those same resource-graph
|
||||
surfaces also flows through one shared frontend helper, so the title-casing
|
||||
and underscore cleanup used for relationship, change, and drawer labels stay
|
||||
The confidence percentage wording used by the drawer's change timeline rows
|
||||
also flows through a shared frontend formatter, so the same `50%`-style
|
||||
labels stay consistent across timeline surfaces instead of being re-derived
|
||||
in the component.
|
||||
The remaining fallback token humanization used by those same timeline and
|
||||
drawer surfaces also flows through one shared frontend helper, so the
|
||||
title-casing and underscore cleanup used for change and drawer labels stay
|
||||
centralized instead of being reimplemented locally.
|
||||
The canonical recent-change section wrapper also lives in
|
||||
`internal/unifiedresources.FormatResourceRecentChangesContext`, so the AI
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ Own canonical runtime payload shapes between backend and frontend.
|
|||
3. Add dedicated contract tests for new stable payloads
|
||||
4. Route unified resource sensitivity, routing, and `aiSafeSummary` payload changes through `internal/api/resources.go`, `internal/api/contract_test.go`, and the canonical frontend resource consumer proofs together; resource governance metadata must not ship as an API-only or frontend-only heuristic
|
||||
5. Route unified-resource action, lifecycle, and export audit reads through `internal/api/activity_audit_handlers.go`, `internal/api/router_routes_licensing.go`, and `internal/api/contract_test.go` together so the control-plane execution trail stays on a governed API contract instead of a store-only shape
|
||||
6. Route dedicated unified-resource capability, relationship, timeline, and facet-bundle reads through `frontend-modern/src/api/resources.ts`, `internal/api/resources.go`, and `internal/api/contract_test.go` together so the backend facet contract and the frontend client stay aligned on one governed surface, including the backend-provided facet counts needed to distinguish loaded slices from total history
|
||||
6. Route dedicated unified-resource timeline and facet-bundle reads through `frontend-modern/src/api/resources.ts`, `internal/api/resources.go`, and `internal/api/contract_test.go` together so the backend facet contract and the frontend client stay aligned on one timeline-first surface, while capability and relationship detail stays backend-owned for AI correlation and change detection
|
||||
7. Route canonical AI intelligence summary and resource-intelligence reads through `frontend-modern/src/api/ai.ts`, `frontend-modern/src/stores/aiIntelligence.ts`, `frontend-modern/src/pages/AIIntelligence.tsx`, `internal/api/ai_handlers.go`, and `internal/api/contract_test.go` together so the summary card, store state, and backend payload stay aligned on one governed surface, including the canonical recent-changes slice
|
||||
and the shared `frontend-modern/src/components/Infrastructure/ResourceChangeSummary.tsx` card, so canonical recent-change timelines stay rendered through one governed frontend card instead of separate page-local list loops
|
||||
and the shared `frontend-modern/src/utils/resourceChangePresentation.ts` formatter used by the summary page and resource drawer, so canonical change wording does not drift across surfaces
|
||||
|
|
@ -132,8 +132,9 @@ The unified resource API payload now carries the richer domain facets directly
|
|||
through the owned backend response: resource objects can expose canonical
|
||||
`capabilities`, `relationships`, `recentChanges`, and derived `facetCounts`
|
||||
in addition to policy and identity metadata, so the backend payload contract
|
||||
stays aligned with the
|
||||
timeline and control-plane model instead of flattening those fields away.
|
||||
stays aligned with the timeline and control-plane model instead of flattening
|
||||
those fields away. The frontend consumer, however, only preserves the
|
||||
timeline-first `recentChanges` slice and its counts on the bundle contract.
|
||||
The same resource contract now also exposes a dedicated
|
||||
`/api/resources/{id}/timeline` history endpoint and bundled facet reads under
|
||||
`/api/resources/{id}/facets`, so operators can inspect change history without
|
||||
|
|
|
|||
|
|
@ -159,10 +159,10 @@ The Patrol page and resource drawer now also share the canonical
|
|||
`frontend-modern/src/utils/resourceChangePresentation.ts` formatter so
|
||||
recent-change kind and headline wording stays aligned wherever the canonical
|
||||
timeline is surfaced.
|
||||
The Patrol page and resource drawer now also share a frontend relationship
|
||||
presentation helper for graph labels and provenance wording, so the same
|
||||
canonical relationship semantics render consistently across the resource and
|
||||
intelligence surfaces.
|
||||
The Patrol page and resource drawer now keep canonical relationship semantics
|
||||
in the backend correlation path, while the visible frontend stays
|
||||
timeline-first and does not surface a separate relationship presentation
|
||||
helper.
|
||||
The backend Patrol and AI runtime summaries now also share
|
||||
`internal/unifiedresources/change_presentation.go` for the canonical
|
||||
change-kind and provenance mapping, so the same resource-model semantics
|
||||
|
|
|
|||
|
|
@ -167,8 +167,8 @@ pages do not hit a missing-provider 500 before the monitor is fully wired.
|
|||
The shared unified-resource consumer hook now also preserves `recentChanges`,
|
||||
`facetCounts`, `policy`, and `aiSafeSummary` fields when storage and recovery
|
||||
surfaces read unified resources, so those pages see the same control-plane
|
||||
timeline facets as the dedicated resource drawer instead of flattening them
|
||||
away locally.
|
||||
timeline facets and recent-change totals as the dedicated resource drawer
|
||||
instead of flattening them away locally.
|
||||
The same storage-facing runtime paths now also normalize org scope through
|
||||
`frontend-modern/src/utils/orgScope.ts` before building cache keys or
|
||||
multi-tenant fetch state, so Dashboard, StorageSummary, and other storage
|
||||
|
|
|
|||
|
|
@ -169,13 +169,12 @@ The canonical unified-resource change and relationship presenters now also
|
|||
share the same elapsed-time and "ago" wording utilities, so `observed`,
|
||||
`last seen`, and `ago` fragments stay consistent without each formatter
|
||||
maintaining its own "time ago" implementation.
|
||||
The drawer's relationship and change timeline confidence labels now also use
|
||||
a shared frontend formatter, so the same percentage wording is emitted across
|
||||
resource-graph surfaces instead of each consumer rounding confidence values on
|
||||
its own.
|
||||
Those same resource-graph surfaces now also share a token-humanization helper
|
||||
for fallback labels, so underscore cleanup and title-casing for relationship,
|
||||
change, and drawer labels stay aligned without local copies.
|
||||
The drawer's change timeline confidence labels now also use a shared frontend
|
||||
formatter, so the same percentage wording is emitted across timeline
|
||||
surfaces instead of each consumer rounding confidence values on its own.
|
||||
Those same timeline surfaces now also share a token-humanization helper for
|
||||
fallback labels, so underscore cleanup and title-casing for change and drawer
|
||||
labels stay aligned without local copies.
|
||||
The same resource-change contract now also owns the canonical filter parser
|
||||
used by `/api/resources/{id}/timeline`, so `kind`, `sourceType`, and
|
||||
`sourceAdapter` validation stays with the change model instead of being
|
||||
|
|
@ -213,15 +212,12 @@ drawer, which keeps the presentation surface aligned with the governed API
|
|||
contract instead of rebuilding the graph and timeline inline.
|
||||
The shared `ResourceFacetSummary` consumer now omits capability and
|
||||
relationship badges from the default table/detail surface entirely, while the
|
||||
backend contract still preserves those facet fields for governed consumers.
|
||||
backend contract keeps capability and relationship data on the owned resource
|
||||
model for governed AI and correlation consumers.
|
||||
That keeps the proven monitoring UX centered on factual timeline
|
||||
investigation while the richer facet payloads remain available as backend and
|
||||
AI-facing foundations instead of being presented as first-class product facts
|
||||
before they are fully populated.
|
||||
That drawer now also uses a shared frontend relationship-presentation helper
|
||||
for graph labels and provenance wording, so the UI stays aligned with the
|
||||
canonical relationship semantics instead of keeping drawer-local token
|
||||
humanization.
|
||||
The same facet bundle now also returns grouped recent-change counts by
|
||||
canonical change kind, so the detail drawer can surface the distribution of
|
||||
state transitions, restarts, config updates, and anomalies without
|
||||
|
|
@ -355,10 +351,10 @@ canonical `facetCounts` on the resource object when available, so the backend
|
|||
list/read shapes remain the source of truth instead of forcing the frontend to
|
||||
infer totals only from loaded slices. The drawer now fetches those facets
|
||||
through one backend bundle endpoint, and that shared facet bundle preserves
|
||||
backend counts for the hidden capability and relationship model alongside the
|
||||
timeline slice so the overview card and history summary can report the total
|
||||
facet history instead of collapsing to the currently loaded page when the
|
||||
timeline endpoint is paginated. Timeline references in that drawer now route
|
||||
the timeline slice plus recent-change counts so the overview card and history
|
||||
summary can report the loaded history instead of collapsing to the currently
|
||||
loaded page when the timeline endpoint is paginated. Timeline references in
|
||||
that drawer now route
|
||||
through the canonical infrastructure resource filter, so the resource history
|
||||
remains navigable from the history surface instead of being purely
|
||||
descriptive text.
|
||||
|
|
@ -366,10 +362,9 @@ descriptive text.
|
|||
`frontend-modern/src/utils/resourceChangePresentation.ts` label helper for
|
||||
canonical change kinds, source types, and adapter provenance, so the chip
|
||||
wording stays aligned across table, drawer, and intelligence surfaces.
|
||||
Relationship cards in that drawer also surface `lastSeenAt` freshness and
|
||||
optional metadata blocks, and timeline cards surface change metadata when it
|
||||
is present, so the graph history view preserves the richer provenance already
|
||||
carried by the unified-resource model instead of flattening those fields away.
|
||||
Timeline cards in that drawer surface change metadata when it is present, so
|
||||
the history view preserves the richer provenance already carried by the
|
||||
unified-resource model instead of flattening those fields away.
|
||||
The same Infrastructure resource-only links now also default through the
|
||||
shared `frontend-modern/src/components/Infrastructure/ResourceChangeSummary.tsx`
|
||||
and `frontend-modern/src/components/Infrastructure/ResourceCorrelationSummary.tsx`
|
||||
|
|
@ -539,15 +534,15 @@ They also own the canonical sensitivity and routing order used to format
|
|||
policy-posture count summaries with human-readable labels, so the AI summary
|
||||
and frontend policy card both read the same presentation sequence from the
|
||||
shared resource model.
|
||||
Canonical resources now carry first-class graph-expansion fields: `Capabilities`
|
||||
(bounded action definitions with approval levels), `Relationships` (typed
|
||||
inter-resource links with direction and confidence), and `RecentChanges` (typed
|
||||
change timeline entries with source, confidence, and related-resource
|
||||
references). These fields are defined in `capabilities.go`, `relationships.go`,
|
||||
`changes.go`, `privacy.go`, and `actions.go`. The frontend capability drawer
|
||||
now formats the shared approval-level vocabulary through a canonical
|
||||
presentation helper instead of a local switch, so the resource model and the
|
||||
rendered labels stay aligned. The store now also owns a
|
||||
Canonical resources now carry first-class graph-expansion fields:
|
||||
`Capabilities` (bounded action definitions with approval levels),
|
||||
`Relationships` (typed inter-resource links with direction and confidence),
|
||||
and `RecentChanges` (typed change timeline entries with source, confidence,
|
||||
and related-resource references). These fields are defined in
|
||||
`capabilities.go`, `relationships.go`, `changes.go`, `privacy.go`, and
|
||||
`actions.go`. The backend keeps those graph fields for AI and correlation use,
|
||||
while the frontend consumer stays timeline-first and only preserves the
|
||||
recent-change slice plus facet counts it actually renders. The store now also owns a
|
||||
`resource_changes` persistence table with `RecordChange` and `GetRecentChanges`
|
||||
methods so change history is queryable by canonical ID and time window.
|
||||
The shared change presentation helper also owns the canonical kind, source
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ const v2Resource = {
|
|||
status: 'online',
|
||||
lastSeen: '2026-02-06T12:00:00Z',
|
||||
sources: ['agent'],
|
||||
facetCounts: {
|
||||
recentChanges: 1,
|
||||
},
|
||||
metrics: {
|
||||
cpu: { percent: 15 },
|
||||
memory: { used: 4 * 1024 * 1024, total: 8 * 1024 * 1024, percent: 50 },
|
||||
|
|
@ -193,6 +196,9 @@ describe('useUnifiedResources', () => {
|
|||
resourceId: 'host-1',
|
||||
hostname: 'pve1',
|
||||
});
|
||||
expect(result!.resources()[0].facetCounts).toEqual({
|
||||
recentChanges: 1,
|
||||
});
|
||||
|
||||
dispose();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -138,14 +138,7 @@ export interface ResourcePolicy {
|
|||
routing: ResourceRoutingPolicy;
|
||||
}
|
||||
|
||||
export type ResourceCapabilityType = 'common' | 'native';
|
||||
export type ResourceApprovalLevel = 'none' | 'dry_run_only' | 'admin' | 'mfa';
|
||||
export type ResourceRelationshipType =
|
||||
| 'runs_on'
|
||||
| 'depends_on'
|
||||
| 'mounted_to'
|
||||
| 'exposed_by'
|
||||
| 'owned_by';
|
||||
export type ResourceChangeConfidence = 'high' | 'medium' | 'low';
|
||||
export type ResourceChangeKind =
|
||||
| 'state_transition'
|
||||
|
|
@ -173,26 +166,6 @@ export type ResourceFacetSourceAdapter =
|
|||
| 'truenas_adapter'
|
||||
| 'agent:ops-helper';
|
||||
|
||||
export interface ResourceCapabilityParam {
|
||||
name: string;
|
||||
type: string;
|
||||
required: boolean;
|
||||
enum?: string[];
|
||||
pattern?: string;
|
||||
defaultValue?: unknown;
|
||||
isSensitive: boolean;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface ResourceCapability {
|
||||
name: string;
|
||||
type: ResourceCapabilityType;
|
||||
description: string;
|
||||
minimumApprovalLevel: ResourceApprovalLevel;
|
||||
platform?: string;
|
||||
params?: ResourceCapabilityParam[];
|
||||
}
|
||||
|
||||
export interface ResourceFacetCounts {
|
||||
recentChanges: number;
|
||||
recentChangeKinds?: Partial<Record<ResourceChangeKind, number>>;
|
||||
|
|
@ -200,18 +173,6 @@ export interface ResourceFacetCounts {
|
|||
recentChangeSourceAdapters?: Partial<Record<ResourceFacetSourceAdapter, number>>;
|
||||
}
|
||||
|
||||
export interface ResourceRelationship {
|
||||
sourceId: string;
|
||||
targetId: string;
|
||||
type: ResourceRelationshipType;
|
||||
confidence: number;
|
||||
active: boolean;
|
||||
discoverer: string;
|
||||
observedAt: string;
|
||||
lastSeenAt: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ResourceChange {
|
||||
id: string;
|
||||
observedAt: string;
|
||||
|
|
|
|||
|
|
@ -73,7 +73,6 @@ import dashboardEmptyStatePresentationSource from '@/utils/dashboardEmptyStatePr
|
|||
import throughputPresentationSource from '@/utils/throughputPresentation.ts?raw';
|
||||
import resourceChangeSummarySource from '@/components/Infrastructure/ResourceChangeSummary.tsx?raw';
|
||||
import resourceChangePresentationSource from '@/utils/resourceChangePresentation.ts?raw';
|
||||
import resourceRelationshipPresentationSource from '@/utils/resourceRelationshipPresentation.ts?raw';
|
||||
import resourceCorrelationPresentationSource from '@/utils/resourceCorrelationPresentation.ts?raw';
|
||||
import confidencePresentationSource from '@/utils/confidencePresentation.ts?raw';
|
||||
import approvalPresentationSource from '@/utils/approvalPresentation.ts?raw';
|
||||
|
|
@ -1914,7 +1913,6 @@ describe('frontend resource type boundaries', () => {
|
|||
expect(resourceChangePresentationSource).toContain(
|
||||
'getResourceChangeSourceAdapterPresentation',
|
||||
);
|
||||
expect(resourceRelationshipPresentationSource).toContain('formatConfidencePercentage');
|
||||
expect(resourceCorrelationPresentationSource).toContain('formatConfidencePercentage');
|
||||
expect(resourceCorrelationPresentationSource).toContain('humanizeArrowDelimitedLabel');
|
||||
expect(resourceCorrelationPresentationSource).toContain('asTrimmedString');
|
||||
|
|
@ -1924,13 +1922,11 @@ describe('frontend resource type boundaries', () => {
|
|||
expect(throughputPresentationSource).toContain('formatThroughputRate');
|
||||
expect(resourceDetailDrawerSource).toContain('formatIdentifierLabel');
|
||||
expect(resourceChangePresentationSource).toContain('humanizeToken');
|
||||
expect(resourceRelationshipPresentationSource).toContain('humanizeToken');
|
||||
expect(textPresentationSource).toContain('humanizeArrowDelimitedLabel');
|
||||
expect(resourceCorrelationPresentationSource).not.toContain('formatResourceCorrelationEndpointLabel');
|
||||
expect(resourceCorrelationPresentationSource).not.toContain(
|
||||
"replace(/\\s*->\\s*/g, ' → ')",
|
||||
);
|
||||
expect(resourceDetailDrawerSource).toContain('humanizeToken');
|
||||
expect(textPresentationSource).toContain('humanizeToken');
|
||||
expect(textPresentationSource).toContain('formatIdentifierLabel');
|
||||
expect(resourceCorrelationPresentationSource).not.toContain('formatTrimmedLabel');
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { describeResourceRelationship, formatResourceRelationshipType } from '@/utils/resourceRelationshipPresentation';
|
||||
|
||||
describe('resourceRelationshipPresentation', () => {
|
||||
it('formats canonical relationship type labels', () => {
|
||||
expect(formatResourceRelationshipType('runs_on')).toBe('Runs on');
|
||||
expect(formatResourceRelationshipType('depends_on')).toBe('Depends on');
|
||||
expect(formatResourceRelationshipType('mounted_to')).toBe('Mounted to');
|
||||
expect(formatResourceRelationshipType('exposed_by')).toBe('Exposed by');
|
||||
expect(formatResourceRelationshipType('owned_by')).toBe('Owned by');
|
||||
expect(formatResourceRelationshipType('custom_link')).toBe('Custom Link');
|
||||
expect(formatResourceRelationshipType('')).toBe('Related to');
|
||||
});
|
||||
|
||||
it('describes canonical relationship context fragments', () => {
|
||||
const presentation = describeResourceRelationship({
|
||||
sourceId: 'node-1',
|
||||
targetId: 'vm-1',
|
||||
type: 'runs_on',
|
||||
confidence: 0.85,
|
||||
active: false,
|
||||
discoverer: 'proxmox_adapter',
|
||||
observedAt: '2026-03-18T12:00:00Z',
|
||||
lastSeenAt: '2026-03-18T12:05:00Z',
|
||||
metadata: {
|
||||
region: 'lab',
|
||||
},
|
||||
});
|
||||
|
||||
expect(presentation).toMatchObject({
|
||||
typeLabel: 'Runs on',
|
||||
direction: 'node-1 → vm-1',
|
||||
provenance: 'proxmox_adapter',
|
||||
stateLabel: 'Historical',
|
||||
confidence: '85%',
|
||||
hasMetadata: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
import type { ResourceRelationship, ResourceRelationshipType } from '@/types/resource';
|
||||
import { formatConfidencePercentage } from '@/utils/confidencePresentation';
|
||||
import { humanizeToken } from '@/utils/textPresentation';
|
||||
|
||||
export interface ResourceRelationshipPresentation {
|
||||
typeLabel: string;
|
||||
direction: string;
|
||||
provenance: string;
|
||||
stateLabel: string;
|
||||
confidence: string;
|
||||
hasMetadata: boolean;
|
||||
}
|
||||
|
||||
export function formatResourceRelationshipType(type: ResourceRelationshipType | string): string {
|
||||
switch (type) {
|
||||
case 'runs_on':
|
||||
return 'Runs on';
|
||||
case 'depends_on':
|
||||
return 'Depends on';
|
||||
case 'mounted_to':
|
||||
return 'Mounted to';
|
||||
case 'exposed_by':
|
||||
return 'Exposed by';
|
||||
case 'owned_by':
|
||||
return 'Owned by';
|
||||
default: {
|
||||
return humanizeToken(String(type), { fallback: 'Related to' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function describeResourceRelationship(
|
||||
relationship: ResourceRelationship,
|
||||
): ResourceRelationshipPresentation {
|
||||
return {
|
||||
typeLabel: formatResourceRelationshipType(relationship.type),
|
||||
direction: `${relationship.sourceId.trim()} → ${relationship.targetId.trim()}`,
|
||||
provenance: relationship.discoverer.trim(),
|
||||
stateLabel: relationship.active ? 'Active' : 'Historical',
|
||||
confidence: relationship.confidence > 0 ? formatConfidencePercentage(relationship.confidence) : '',
|
||||
hasMetadata: Boolean(relationship.metadata && Object.keys(relationship.metadata).length > 0),
|
||||
};
|
||||
}
|
||||
|
|
@ -51,7 +51,6 @@ type (
|
|||
MetadataUpdater = tools.MetadataUpdater
|
||||
IncidentRecorderProvider = tools.IncidentRecorderProvider
|
||||
EventCorrelatorProvider = tools.EventCorrelatorProvider
|
||||
TopologyProvider = tools.TopologyProvider
|
||||
KnowledgeStoreProvider = tools.KnowledgeStoreProvider
|
||||
MCPDiscoveryProvider = tools.DiscoveryProvider
|
||||
MCPUnifiedResourceProvider = tools.UnifiedResourceProvider
|
||||
|
|
@ -1218,14 +1217,6 @@ func (s *Service) SetEventCorrelatorProvider(provider EventCorrelatorProvider) {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *Service) SetTopologyProvider(provider TopologyProvider) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.executor != nil {
|
||||
s.executor.SetTopologyProvider(provider)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) SetKnowledgeStoreProvider(provider KnowledgeStoreProvider) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ func TestServiceSettersAndAutonomousMode(t *testing.T) {
|
|||
|
||||
service.SetIncidentRecorderProvider(nil)
|
||||
service.SetEventCorrelatorProvider(nil)
|
||||
service.SetTopologyProvider(nil)
|
||||
service.SetKnowledgeStoreProvider(nil)
|
||||
|
||||
service.SetAutonomousMode(true)
|
||||
|
|
|
|||
|
|
@ -362,7 +362,6 @@ type ExecutorConfig struct {
|
|||
// Optional providers - intelligence
|
||||
IncidentRecorderProvider IncidentRecorderProvider
|
||||
EventCorrelatorProvider EventCorrelatorProvider
|
||||
TopologyProvider TopologyProvider
|
||||
KnowledgeStoreProvider KnowledgeStoreProvider
|
||||
|
||||
// Optional providers - discovery
|
||||
|
|
@ -414,7 +413,6 @@ type PulseToolExecutor struct {
|
|||
// Intelligence providers
|
||||
incidentRecorderProvider IncidentRecorderProvider
|
||||
eventCorrelatorProvider EventCorrelatorProvider
|
||||
topologyProvider TopologyProvider
|
||||
knowledgeStoreProvider KnowledgeStoreProvider
|
||||
|
||||
// Discovery provider
|
||||
|
|
@ -493,7 +491,6 @@ func NewPulseToolExecutor(cfg ExecutorConfig) *PulseToolExecutor {
|
|||
agentProfileManager: cfg.AgentProfileManager,
|
||||
incidentRecorderProvider: cfg.IncidentRecorderProvider,
|
||||
eventCorrelatorProvider: cfg.EventCorrelatorProvider,
|
||||
topologyProvider: cfg.TopologyProvider,
|
||||
knowledgeStoreProvider: cfg.KnowledgeStoreProvider,
|
||||
discoveryProvider: cfg.DiscoveryProvider,
|
||||
unifiedResourceProvider: cfg.UnifiedResourceProvider,
|
||||
|
|
@ -684,11 +681,6 @@ func (e *PulseToolExecutor) SetEventCorrelatorProvider(provider EventCorrelatorP
|
|||
e.eventCorrelatorProvider = provider
|
||||
}
|
||||
|
||||
// SetTopologyProvider sets the topology provider for relationship graphs
|
||||
func (e *PulseToolExecutor) SetTopologyProvider(provider TopologyProvider) {
|
||||
e.topologyProvider = provider
|
||||
}
|
||||
|
||||
// SetKnowledgeStoreProvider sets the knowledge store provider for notes
|
||||
func (e *PulseToolExecutor) SetKnowledgeStoreProvider(provider KnowledgeStoreProvider) {
|
||||
e.knowledgeStoreProvider = provider
|
||||
|
|
@ -785,7 +777,7 @@ func (e *PulseToolExecutor) isToolAvailable(name string) bool {
|
|||
case "pulse_discovery":
|
||||
return e.discoveryProvider != nil
|
||||
case "pulse_knowledge":
|
||||
return e.knowledgeStoreProvider != nil || e.incidentRecorderProvider != nil || e.eventCorrelatorProvider != nil || e.topologyProvider != nil
|
||||
return e.knowledgeStoreProvider != nil || e.incidentRecorderProvider != nil || e.eventCorrelatorProvider != nil
|
||||
case "pulse_pmg":
|
||||
return e.hasReadState()
|
||||
case "patrol_report_finding", "patrol_resolve_finding", "patrol_get_findings":
|
||||
|
|
|
|||
|
|
@ -59,19 +59,6 @@ type EventCorrelation struct {
|
|||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// TopologyProvider provides access to resource relationships
|
||||
type TopologyProvider interface {
|
||||
GetRelatedResources(resourceID string, depth int) []RelatedResource
|
||||
}
|
||||
|
||||
// RelatedResource represents a resource related to another resource
|
||||
type RelatedResource struct {
|
||||
ResourceID string `json:"resource_id"`
|
||||
ResourceName string `json:"resource_name"`
|
||||
ResourceType string `json:"resource_type"`
|
||||
Relationship string `json:"relationship"`
|
||||
}
|
||||
|
||||
// KnowledgeStoreProvider provides access to stored knowledge/notes
|
||||
type KnowledgeStoreProvider interface {
|
||||
SaveNote(resourceID, note, category string) error
|
||||
|
|
@ -100,21 +87,19 @@ Actions:
|
|||
- recall: Retrieve saved notes about a resource
|
||||
- incidents: Get high-resolution incident recording data
|
||||
- correlate: Get correlated events around a timestamp
|
||||
- relationships: Get resource dependency graph
|
||||
|
||||
Examples:
|
||||
- Save note: action="remember", resource_id="101", note="Production database server", category="purpose"
|
||||
- Recall: action="recall", resource_id="101"
|
||||
- Get incidents: action="incidents", resource_id="101"
|
||||
- Correlate events: action="correlate", resource_id="101", window_minutes=30
|
||||
- Get relationships: action="relationships", resource_id="101"`,
|
||||
- Correlate events: action="correlate", resource_id="101", window_minutes=30`,
|
||||
InputSchema: InputSchema{
|
||||
Type: "object",
|
||||
Properties: map[string]PropertySchema{
|
||||
"action": {
|
||||
Type: "string",
|
||||
Description: "Knowledge action to perform",
|
||||
Enum: []string{"remember", "recall", "incidents", "correlate", "relationships"},
|
||||
Enum: []string{"remember", "recall", "incidents", "correlate"},
|
||||
},
|
||||
"resource_id": {
|
||||
Type: "string",
|
||||
|
|
@ -140,10 +125,6 @@ Examples:
|
|||
Type: "integer",
|
||||
Description: "For correlate: time window in minutes (default: 15)",
|
||||
},
|
||||
"depth": {
|
||||
Type: "integer",
|
||||
Description: "For relationships: levels to traverse (default: 1, max: 3)",
|
||||
},
|
||||
"limit": {
|
||||
Type: "integer",
|
||||
Description: "For incidents: max windows to return (default: 5)",
|
||||
|
|
@ -170,10 +151,8 @@ func (e *PulseToolExecutor) executeKnowledge(ctx context.Context, args map[strin
|
|||
return e.executeGetIncidentWindow(ctx, args)
|
||||
case "correlate":
|
||||
return e.executeCorrelateEvents(ctx, args)
|
||||
case "relationships":
|
||||
return e.executeGetRelationshipGraph(ctx, args)
|
||||
default:
|
||||
return NewErrorResult(fmt.Errorf("unknown action: %s. Use: remember, recall, incidents, correlate, relationships", action)), nil
|
||||
return NewErrorResult(fmt.Errorf("unknown action: %s. Use: remember, recall, incidents, correlate", action)), nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -258,39 +237,6 @@ func (e *PulseToolExecutor) executeCorrelateEvents(_ context.Context, args map[s
|
|||
}), nil
|
||||
}
|
||||
|
||||
func (e *PulseToolExecutor) executeGetRelationshipGraph(_ context.Context, args map[string]interface{}) (CallToolResult, error) {
|
||||
resourceID, _ := args["resource_id"].(string)
|
||||
depth := intArg(args, "depth", 1)
|
||||
|
||||
if resourceID == "" {
|
||||
return NewErrorResult(fmt.Errorf("resource_id is required")), nil
|
||||
}
|
||||
|
||||
// Cap depth to prevent excessive traversal
|
||||
if depth < 1 {
|
||||
depth = 1
|
||||
}
|
||||
if depth > 3 {
|
||||
depth = 3
|
||||
}
|
||||
|
||||
if e.topologyProvider == nil {
|
||||
return NewTextResult("Topology information not available."), nil
|
||||
}
|
||||
|
||||
related := e.topologyProvider.GetRelatedResources(resourceID, depth)
|
||||
if len(related) == 0 {
|
||||
return NewTextResult(fmt.Sprintf("No relationships found for resource '%s'.", resourceID)), nil
|
||||
}
|
||||
|
||||
return NewJSONResult(map[string]interface{}{
|
||||
"resource_id": resourceID,
|
||||
"depth": depth,
|
||||
"related_resources": related,
|
||||
"count": len(related),
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (e *PulseToolExecutor) executeRemember(_ context.Context, args map[string]interface{}) (CallToolResult, error) {
|
||||
resourceID, _ := args["resource_id"].(string)
|
||||
note, _ := args["note"].(string)
|
||||
|
|
|
|||
|
|
@ -64,7 +64,6 @@ type AIService interface {
|
|||
SetKnowledgeStoreProvider(provider chat.KnowledgeStoreProvider)
|
||||
SetIncidentRecorderProvider(provider chat.IncidentRecorderProvider)
|
||||
SetEventCorrelatorProvider(provider chat.EventCorrelatorProvider)
|
||||
SetTopologyProvider(provider chat.TopologyProvider)
|
||||
SetDiscoveryProvider(provider chat.MCPDiscoveryProvider)
|
||||
SetUnifiedResourceProvider(provider chat.MCPUnifiedResourceProvider)
|
||||
UpdateControlSettings(cfg *config.AIConfig)
|
||||
|
|
|
|||
|
|
@ -146,7 +146,6 @@ func (m *MockAIService) SetIncidentRecorderProvider(provider chat.IncidentRecord
|
|||
func (m *MockAIService) SetEventCorrelatorProvider(provider chat.EventCorrelatorProvider) {
|
||||
m.Called(provider)
|
||||
}
|
||||
func (m *MockAIService) SetTopologyProvider(provider chat.TopologyProvider) { m.Called(provider) }
|
||||
func (m *MockAIService) SetDiscoveryProvider(provider chat.MCPDiscoveryProvider) {
|
||||
m.Called(provider)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3810,8 +3810,6 @@ func TestContract_ResourceListCarriesTimelineAndCapabilityContracts(t *testing.T
|
|||
},
|
||||
},
|
||||
FacetCounts: unifiedresources.ResourceFacetCounts{
|
||||
Capabilities: 1,
|
||||
Relationships: 1,
|
||||
RecentChanges: 1,
|
||||
},
|
||||
},
|
||||
|
|
@ -3894,8 +3892,6 @@ func TestContract_ResourceListCarriesTimelineAndCapabilityContracts(t *testing.T
|
|||
}
|
||||
],
|
||||
"facetCounts":{
|
||||
"capabilities":1,
|
||||
"relationships":1,
|
||||
"recentChanges":1
|
||||
}
|
||||
}
|
||||
|
|
@ -4258,13 +4254,9 @@ func TestContract_ResourceTimelineAnomalyJSONSnapshot(t *testing.T) {
|
|||
func TestContract_ResourceFacetsJSONSnapshot(t *testing.T) {
|
||||
now := time.Date(2026, 3, 18, 17, 0, 0, 0, time.UTC)
|
||||
payload := struct {
|
||||
ResourceID string `json:"resourceId"`
|
||||
Capabilities []unifiedresources.ResourceCapability `json:"capabilities"`
|
||||
Relationships []unifiedresources.ResourceRelationship `json:"relationships"`
|
||||
RecentChanges []unifiedresources.ResourceChange `json:"recentChanges"`
|
||||
ResourceID string `json:"resourceId"`
|
||||
RecentChanges []unifiedresources.ResourceChange `json:"recentChanges"`
|
||||
Counts struct {
|
||||
Capabilities int `json:"capabilities"`
|
||||
Relationships int `json:"relationships"`
|
||||
RecentChanges int `json:"recentChanges"`
|
||||
RecentChangeKinds map[unifiedresources.ChangeKind]int `json:"recentChangeKinds"`
|
||||
RecentChangeSourceTypes map[unifiedresources.ChangeSourceType]int `json:"recentChangeSourceTypes"`
|
||||
|
|
@ -4272,30 +4264,6 @@ func TestContract_ResourceFacetsJSONSnapshot(t *testing.T) {
|
|||
} `json:"counts"`
|
||||
}{
|
||||
ResourceID: "vm:42",
|
||||
Capabilities: []unifiedresources.ResourceCapability{
|
||||
{
|
||||
Name: "restart",
|
||||
Type: unifiedresources.CapabilityTypeCommon,
|
||||
Description: "Restart the VM",
|
||||
MinimumApprovalLevel: unifiedresources.ApprovalAdmin,
|
||||
},
|
||||
},
|
||||
Relationships: []unifiedresources.ResourceRelationship{
|
||||
{
|
||||
SourceID: "vm:42",
|
||||
TargetID: "node-1",
|
||||
Type: unifiedresources.RelRunsOn,
|
||||
Confidence: 1,
|
||||
Active: true,
|
||||
Discoverer: "proxmox_adapter",
|
||||
ObservedAt: now,
|
||||
LastSeenAt: now,
|
||||
Metadata: map[string]any{
|
||||
"source": "live",
|
||||
"cluster": "pve-prod",
|
||||
},
|
||||
},
|
||||
},
|
||||
RecentChanges: []unifiedresources.ResourceChange{
|
||||
{
|
||||
ID: "chg-42",
|
||||
|
|
@ -4316,15 +4284,11 @@ func TestContract_ResourceFacetsJSONSnapshot(t *testing.T) {
|
|||
},
|
||||
},
|
||||
Counts: struct {
|
||||
Capabilities int `json:"capabilities"`
|
||||
Relationships int `json:"relationships"`
|
||||
RecentChanges int `json:"recentChanges"`
|
||||
RecentChangeKinds map[unifiedresources.ChangeKind]int `json:"recentChangeKinds"`
|
||||
RecentChangeSourceTypes map[unifiedresources.ChangeSourceType]int `json:"recentChangeSourceTypes"`
|
||||
RecentChangeSourceAdapters map[unifiedresources.ChangeSourceAdapter]int `json:"recentChangeSourceAdapters"`
|
||||
}{
|
||||
Capabilities: 1,
|
||||
Relationships: 1,
|
||||
RecentChanges: 3,
|
||||
RecentChangeKinds: map[unifiedresources.ChangeKind]int{unifiedresources.ChangeRestart: 1, unifiedresources.ChangeAnomaly: 2},
|
||||
RecentChangeSourceTypes: map[unifiedresources.ChangeSourceType]int{
|
||||
|
|
@ -4345,27 +4309,6 @@ func TestContract_ResourceFacetsJSONSnapshot(t *testing.T) {
|
|||
|
||||
const want = `{
|
||||
"resourceId":"vm:42",
|
||||
"capabilities":[
|
||||
{
|
||||
"name":"restart",
|
||||
"type":"common",
|
||||
"description":"Restart the VM",
|
||||
"minimumApprovalLevel":"admin"
|
||||
}
|
||||
],
|
||||
"relationships":[
|
||||
{
|
||||
"sourceId":"vm:42",
|
||||
"targetId":"node-1",
|
||||
"type":"runs_on",
|
||||
"confidence":1,
|
||||
"active":true,
|
||||
"discoverer":"proxmox_adapter",
|
||||
"observedAt":"2026-03-18T17:00:00Z",
|
||||
"lastSeenAt":"2026-03-18T17:00:00Z",
|
||||
"metadata":{"cluster":"pve-prod","source":"live"}
|
||||
}
|
||||
],
|
||||
"recentChanges":[
|
||||
{
|
||||
"id":"chg-42",
|
||||
|
|
@ -4383,8 +4326,6 @@ func TestContract_ResourceFacetsJSONSnapshot(t *testing.T) {
|
|||
}
|
||||
],
|
||||
"counts":{
|
||||
"capabilities":1,
|
||||
"relationships":1,
|
||||
"recentChanges":3,
|
||||
"recentChangeKinds":{
|
||||
"metric_anomaly":2,
|
||||
|
|
|
|||
|
|
@ -278,11 +278,9 @@ func (h *ResourceHandlers) HandleGetResource(w http.ResponseWriter, r *http.Requ
|
|||
type resourceFacetCountsResponse = unified.ResourceFacetCounts
|
||||
|
||||
type resourceFacetBundleResponse struct {
|
||||
ResourceID string `json:"resourceId"`
|
||||
Capabilities []unified.ResourceCapability `json:"capabilities"`
|
||||
Relationships []unified.ResourceRelationship `json:"relationships"`
|
||||
RecentChanges []unified.ResourceChange `json:"recentChanges"`
|
||||
Counts resourceFacetCountsResponse `json:"counts"`
|
||||
ResourceID string `json:"resourceId"`
|
||||
RecentChanges []unified.ResourceChange `json:"recentChanges"`
|
||||
Counts resourceFacetCountsResponse `json:"counts"`
|
||||
}
|
||||
|
||||
// HandleResourceRoutes dispatches nested resource routes.
|
||||
|
|
@ -346,7 +344,7 @@ func (h *ResourceHandlers) HandleGetResourceFacets(w http.ResponseWriter, r *htt
|
|||
return
|
||||
}
|
||||
|
||||
resource, ok := registry.Get(resourceID)
|
||||
_, ok := registry.Get(resourceID)
|
||||
if !ok {
|
||||
http.Error(w, "Resource not found", http.StatusNotFound)
|
||||
return
|
||||
|
|
@ -402,24 +400,11 @@ func (h *ResourceHandlers) HandleGetResourceFacets(w http.ResponseWriter, r *htt
|
|||
return
|
||||
}
|
||||
|
||||
capabilities := resource.Capabilities
|
||||
if capabilities == nil {
|
||||
capabilities = []unified.ResourceCapability{}
|
||||
}
|
||||
relationships := resource.Relationships
|
||||
if relationships == nil {
|
||||
relationships = []unified.ResourceRelationship{}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resourceFacetBundleResponse{
|
||||
ResourceID: resourceID,
|
||||
Capabilities: capabilities,
|
||||
Relationships: relationships,
|
||||
RecentChanges: recentChanges,
|
||||
Counts: resourceFacetCountsResponse{
|
||||
Capabilities: resource.FacetCounts.Capabilities,
|
||||
Relationships: resource.FacetCounts.Relationships,
|
||||
RecentChanges: changeCount,
|
||||
RecentChangeKinds: changeKindCounts,
|
||||
RecentChangeSourceTypes: sourceTypeCounts,
|
||||
|
|
|
|||
|
|
@ -246,8 +246,8 @@ func TestResourceListUsesUnifiedSeedProvider(t *testing.T) {
|
|||
if strings.TrimSpace(resp.Data[0].AISafeSummary) == "" {
|
||||
t.Fatal("expected aiSafeSummary on seeded resource")
|
||||
}
|
||||
if got := resp.Data[0].FacetCounts; got.Capabilities != 1 || got.Relationships != 1 || got.RecentChanges != 1 {
|
||||
t.Fatalf("facetCounts = %+v, want 1/1/1", got)
|
||||
if got := resp.Data[0].FacetCounts; got.RecentChanges != 1 {
|
||||
t.Fatalf("facetCounts = %+v, want recentChanges=1", got)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -981,13 +981,9 @@ func TestResourceGetFacetsAndTimeline(t *testing.T) {
|
|||
t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var payload struct {
|
||||
ResourceID string `json:"resourceId"`
|
||||
Capabilities []unified.ResourceCapability `json:"capabilities"`
|
||||
Relationships []unified.ResourceRelationship `json:"relationships"`
|
||||
RecentChanges []unified.ResourceChange `json:"recentChanges"`
|
||||
ResourceID string `json:"resourceId"`
|
||||
RecentChanges []unified.ResourceChange `json:"recentChanges"`
|
||||
Counts struct {
|
||||
Capabilities int `json:"capabilities"`
|
||||
Relationships int `json:"relationships"`
|
||||
RecentChanges int `json:"recentChanges"`
|
||||
RecentChangeKinds map[unified.ChangeKind]int `json:"recentChangeKinds"`
|
||||
RecentChangeSourceTypes map[unified.ChangeSourceType]int `json:"recentChangeSourceTypes"`
|
||||
|
|
@ -1000,9 +996,6 @@ func TestResourceGetFacetsAndTimeline(t *testing.T) {
|
|||
if payload.ResourceID != "vm:42" || payload.Counts.RecentChanges != 3 || len(payload.RecentChanges) != 1 {
|
||||
t.Fatalf("unexpected facets payload: %#v", payload)
|
||||
}
|
||||
if payload.Counts.Capabilities != 1 || payload.Counts.Relationships != 1 {
|
||||
t.Fatalf("unexpected facet counts: %#v", payload.Counts)
|
||||
}
|
||||
if got := payload.Counts.RecentChangeKinds; len(got) != 2 || got[unified.ChangeRestart] != 1 || got[unified.ChangeAnomaly] != 2 {
|
||||
t.Fatalf("unexpected recent change kind counts: %#v", got)
|
||||
}
|
||||
|
|
@ -1012,9 +1005,6 @@ func TestResourceGetFacetsAndTimeline(t *testing.T) {
|
|||
if got := payload.Counts.RecentChangeSourceAdapters; len(got) != 2 || got[unified.AdapterProxmox] != 2 || got[unified.AdapterDocker] != 1 {
|
||||
t.Fatalf("unexpected recent change source adapter counts: %#v", got)
|
||||
}
|
||||
if got := payload.Relationships[0].Metadata["cluster"]; got != "pve-prod" {
|
||||
t.Fatalf("unexpected relationship metadata: %#v", payload.Relationships[0].Metadata)
|
||||
}
|
||||
if got := payload.RecentChanges[0].Metadata["ticket"]; got != "INC-1234" {
|
||||
t.Fatalf("unexpected change metadata: %#v", payload.RecentChanges[0].Metadata)
|
||||
}
|
||||
|
|
@ -1106,12 +1096,9 @@ func TestResourceGetFacetsAndTimeline(t *testing.T) {
|
|||
t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var payload struct {
|
||||
ResourceID string `json:"resourceId"`
|
||||
RecentChanges []unified.ResourceChange `json:"recentChanges"`
|
||||
Relationships []unified.ResourceRelationship `json:"relationships"`
|
||||
ResourceID string `json:"resourceId"`
|
||||
RecentChanges []unified.ResourceChange `json:"recentChanges"`
|
||||
Counts struct {
|
||||
Capabilities int `json:"capabilities"`
|
||||
Relationships int `json:"relationships"`
|
||||
RecentChanges int `json:"recentChanges"`
|
||||
RecentChangeKinds map[unified.ChangeKind]int `json:"recentChangeKinds"`
|
||||
RecentChangeSourceTypes map[unified.ChangeSourceType]int `json:"recentChangeSourceTypes"`
|
||||
|
|
@ -1146,12 +1133,9 @@ func TestResourceGetFacetsAndTimeline(t *testing.T) {
|
|||
t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var payload struct {
|
||||
ResourceID string `json:"resourceId"`
|
||||
RecentChanges []unified.ResourceChange `json:"recentChanges"`
|
||||
Relationships []unified.ResourceRelationship `json:"relationships"`
|
||||
ResourceID string `json:"resourceId"`
|
||||
RecentChanges []unified.ResourceChange `json:"recentChanges"`
|
||||
Counts struct {
|
||||
Capabilities int `json:"capabilities"`
|
||||
Relationships int `json:"relationships"`
|
||||
RecentChanges int `json:"recentChanges"`
|
||||
RecentChangeKinds map[unified.ChangeKind]int `json:"recentChangeKinds"`
|
||||
RecentChangeSourceTypes map[unified.ChangeSourceType]int `json:"recentChangeSourceTypes"`
|
||||
|
|
|
|||
|
|
@ -2229,7 +2229,6 @@ func (r *Router) wireAIChatDependenciesForService(ctx context.Context, service A
|
|||
// Wire intelligence providers for MCP tools
|
||||
// - IncidentRecorderProvider: high-frequency incident data (pulse_get_incident_window)
|
||||
// - EventCorrelatorProvider: Proxmox events (pulse_correlate_events)
|
||||
// - TopologyProvider: relationship graph (pulse_get_relationship_graph)
|
||||
// - KnowledgeStoreProvider: notes (pulse_remember, pulse_recall)
|
||||
|
||||
// Wire incident recorder provider (high-frequency incident data)
|
||||
|
|
|
|||
|
|
@ -47,8 +47,6 @@ func cloneResource(in *Resource) Resource {
|
|||
|
||||
func resourceFacetCounts(resource Resource) ResourceFacetCounts {
|
||||
return ResourceFacetCounts{
|
||||
Capabilities: len(resource.Capabilities),
|
||||
Relationships: len(resource.Relationships),
|
||||
RecentChanges: len(resource.RecentChanges),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -804,6 +804,39 @@ func TestResourceFacetCountsAreCanonicalResourceFields(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestCanonicalIdentityIsCanonicalResourceField(t *testing.T) {
|
||||
typesData, err := os.ReadFile(filepath.Join("types.go"))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read types.go: %v", err)
|
||||
}
|
||||
typesSource := string(typesData)
|
||||
requiredSnippets := []string{
|
||||
"json:\"canonicalIdentity,omitempty\"",
|
||||
"type CanonicalIdentity struct {",
|
||||
"DisplayName string `json:\"displayName,omitempty\"`",
|
||||
"Aliases []string `json:\"aliases,omitempty\"`",
|
||||
}
|
||||
for _, snippet := range requiredSnippets {
|
||||
if !strings.Contains(typesSource, snippet) {
|
||||
t.Fatalf("internal/unifiedresources/types.go must keep the canonical identity contract snippet %q", snippet)
|
||||
}
|
||||
}
|
||||
|
||||
cloneData, err := os.ReadFile(filepath.Join("clone.go"))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read clone.go: %v", err)
|
||||
}
|
||||
cloneSource := string(cloneData)
|
||||
requiredCloneSnippets := []string{
|
||||
"RefreshCanonicalMetadata(&out)",
|
||||
}
|
||||
for _, snippet := range requiredCloneSnippets {
|
||||
if !strings.Contains(cloneSource, snippet) {
|
||||
t.Fatalf("internal/unifiedresources/clone.go must preserve canonical identity via %q", snippet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestNoLegacyHostResourceTypeSymbol prevents reintroducing the removed
|
||||
// ResourceTypeHost symbol. v6 code must use ResourceTypeAgent and
|
||||
// CanonicalResourceType() for legacy normalization.
|
||||
|
|
|
|||
|
|
@ -69,8 +69,6 @@ type Resource struct {
|
|||
// ResourceFacetCounts captures the total count of each resource facet that
|
||||
// may be surfaced in row summaries or detail drawers.
|
||||
type ResourceFacetCounts struct {
|
||||
Capabilities int `json:"capabilities"`
|
||||
Relationships int `json:"relationships"`
|
||||
RecentChanges int `json:"recentChanges"`
|
||||
RecentChangeKinds map[ChangeKind]int `json:"recentChangeKinds,omitempty"`
|
||||
RecentChangeSourceTypes map[ChangeSourceType]int `json:"recentChangeSourceTypes,omitempty"`
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue