- RMM Overshell (migration 077): admin page, dispatch UI, executor/worker, target resolver, script registry (AD/DHCP/DNS/event-log/services/software/network/loglift) - LogLift evidence pipeline (migration 078): upload webhook, B2 storage client, receiver/matcher, EventLogCollector PowerShell script - IT Glue audit + write-back (migrations 075, 076): asset-audit runner, ticket xrefs, applications/configurations browse pages + apply/revert/audit endpoints - Link-aware analyzer bundles (migration 073) + provider toggle (migration 074): link-discovery service, OpenRouter LLM provider, related-tickets/itglue-suggestion panels, analyze-bundle endpoint - Endpoint data model + device-link reconciliation (migrations 079, 080): conflicts admin page, reconciler service, resolve endpoints - Dashboard overhaul: integration-health service + alerts, overview/health endpoints - Permissions: add itglue + rmm scopes; middleware: public /api/rmm/loglift route Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
204 lines
6.3 KiB
TypeScript
204 lines
6.3 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { _ASSET_AUDIT_INTERNALS } from './data-builder';
|
|
import { fieldNameToTraitKey } from './persistence';
|
|
import { buildAssetAuditUserPayload, _PROMPT_INTERNALS } from './prompt';
|
|
import { AssetAuditResponse } from '@/lib/types/analyzer';
|
|
|
|
describe('fillCount', () => {
|
|
const { fillCount } = _ASSET_AUDIT_INTERNALS;
|
|
|
|
it('counts populated trait keys', () => {
|
|
expect(fillCount({ a: 'x', b: 'y' })).toBe(2);
|
|
});
|
|
|
|
it('drops empty strings, empty arrays, null, undefined', () => {
|
|
expect(
|
|
fillCount({
|
|
a: '',
|
|
b: ' ',
|
|
c: null,
|
|
d: undefined,
|
|
e: [],
|
|
f: 'real',
|
|
})
|
|
).toBe(1);
|
|
});
|
|
|
|
it('counts non-empty objects/arrays', () => {
|
|
expect(fillCount({ a: { values: [1] }, b: [1, 2] })).toBe(2);
|
|
});
|
|
|
|
it('returns 0 for null/undefined input', () => {
|
|
expect(fillCount(null)).toBe(0);
|
|
expect(fillCount(undefined)).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('fieldNameToTraitKey', () => {
|
|
it('matches IT Glue trait-key convention', () => {
|
|
expect(fieldNameToTraitKey('Name')).toBe('name');
|
|
expect(fieldNameToTraitKey('Wulf Application Champion(s)')).toBe(
|
|
'wulf-application-champion-s'
|
|
);
|
|
expect(fieldNameToTraitKey('Application on Device(s)')).toBe(
|
|
'application-on-device-s'
|
|
);
|
|
expect(fieldNameToTraitKey('Client/Server Software Installation Media Location'))
|
|
.toBe('client-server-software-installation-media-location');
|
|
});
|
|
|
|
it('strips leading/trailing/repeated hyphens', () => {
|
|
expect(fieldNameToTraitKey(' --Foo-- ')).toBe('foo');
|
|
expect(fieldNameToTraitKey('A & B')).toBe('a-b');
|
|
});
|
|
});
|
|
|
|
describe('AssetAuditResponse Zod schema', () => {
|
|
it('accepts a well-formed response', () => {
|
|
const ok = AssetAuditResponse.safeParse({
|
|
field_gaps: [
|
|
{
|
|
field_name: 'Wulf Application Champion(s)',
|
|
why_missing_matters: 'Jake Hammel is the SME but not recorded.',
|
|
suggested_value: 'Jake Hammel',
|
|
evidence_ticket_numbers: ['T20260502.0033'],
|
|
confidence: 'high',
|
|
},
|
|
],
|
|
notes_promotions: [
|
|
{
|
|
quoted_note_text: 'Per Jake if down send to Steve Cianflone',
|
|
target_field: 'Vendor Maintenance/Support',
|
|
suggested_value: 'Steve Cianflone (Kastech)',
|
|
confidence: 'medium',
|
|
},
|
|
],
|
|
contradictions: [
|
|
{
|
|
description: 'Notes say 2-3 VMs, Application-on-Device-s lists 1',
|
|
evidence: 'Notes field; application-on-device-s.values',
|
|
},
|
|
],
|
|
overall_score: 0.55,
|
|
});
|
|
expect(ok.success).toBe(true);
|
|
});
|
|
|
|
it('allows null suggested_value', () => {
|
|
const ok = AssetAuditResponse.safeParse({
|
|
field_gaps: [
|
|
{
|
|
field_name: 'URL',
|
|
why_missing_matters: 'Vendor admin console URL not recorded.',
|
|
suggested_value: null,
|
|
evidence_ticket_numbers: [],
|
|
confidence: 'low',
|
|
},
|
|
],
|
|
notes_promotions: [],
|
|
contradictions: [],
|
|
overall_score: 0.7,
|
|
});
|
|
expect(ok.success).toBe(true);
|
|
});
|
|
|
|
it('rejects out-of-range overall_score', () => {
|
|
const bad = AssetAuditResponse.safeParse({
|
|
field_gaps: [],
|
|
notes_promotions: [],
|
|
contradictions: [],
|
|
overall_score: 1.5,
|
|
});
|
|
expect(bad.success).toBe(false);
|
|
});
|
|
|
|
it('rejects bogus confidence values', () => {
|
|
const bad = AssetAuditResponse.safeParse({
|
|
field_gaps: [
|
|
{
|
|
field_name: 'X',
|
|
why_missing_matters: 'y',
|
|
suggested_value: null,
|
|
evidence_ticket_numbers: [],
|
|
confidence: 'sky-high',
|
|
},
|
|
],
|
|
notes_promotions: [],
|
|
contradictions: [],
|
|
overall_score: 0.5,
|
|
});
|
|
expect(bad.success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('buildAssetAuditUserPayload', () => {
|
|
function makeCtx(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
asset_type: 'flexible_asset',
|
|
asset: {
|
|
id: '1',
|
|
organization_id: '100',
|
|
organization_name: 'Acme',
|
|
type_id: '3790',
|
|
type_name: 'Applications',
|
|
name: 'MISYS',
|
|
traits: { name: 'MISYS', version: '6.3' },
|
|
},
|
|
type_id: 3790,
|
|
type_name: 'Applications',
|
|
fields: [
|
|
{ id: '1', name: 'Name', kind: 'Text', hint: null, required: true },
|
|
],
|
|
peer_same_client: [],
|
|
peer_global: [],
|
|
fill_rate_client: [{ field_name: 'Name', fill_rate: 1.0 }],
|
|
fill_rate_global: [{ field_name: 'Name', fill_rate: 0.95 }],
|
|
ticket_evidence: [],
|
|
ticket_scope: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
it('renders all sections (flexible asset)', () => {
|
|
const ctx = makeCtx();
|
|
const { payload, trimmed } = buildAssetAuditUserPayload(ctx as never);
|
|
expect(payload).toContain('=== FLEXIBLE ASSET UNDER AUDIT ===');
|
|
expect(payload).toContain('=== FIELD SCHEMA');
|
|
expect(payload).toContain('=== FILL-RATE STATS ===');
|
|
expect(payload).toContain('=== PEER EXEMPLARS — SAME CLIENT ===');
|
|
expect(payload).toContain('=== PEER EXEMPLARS — BEST-IN-CLASS');
|
|
expect(payload).toContain('=== TICKET EVIDENCE');
|
|
expect(trimmed.peer_global_dropped).toBe(0);
|
|
expect(trimmed.tickets_dropped).toBe(0);
|
|
});
|
|
|
|
it('renders configuration header when assetType is configuration', () => {
|
|
const ctx = makeCtx({ asset_type: 'configuration' });
|
|
const { payload } = buildAssetAuditUserPayload(ctx as never);
|
|
expect(payload).toContain('=== CONFIGURATION UNDER AUDIT ===');
|
|
});
|
|
|
|
it('renders ticket-scoped header when ticket_scope is set', () => {
|
|
const ctx = makeCtx({
|
|
ticket_scope: { analysis_id: 'a', ticket_number: 'T20260502.0033' },
|
|
});
|
|
const { payload } = buildAssetAuditUserPayload(ctx as never);
|
|
expect(payload).toContain('single ticket');
|
|
expect(payload).toContain('T20260502.0033');
|
|
});
|
|
|
|
it('trims peer_global before tickets when over the cap', () => {
|
|
const bigPeer = {
|
|
id: 'x',
|
|
organization_id: '99',
|
|
organization_name: 'Other',
|
|
type_id: '3790',
|
|
type_name: 'Applications',
|
|
name: 'BigPeer',
|
|
traits: { huge: 'x'.repeat(_PROMPT_INTERNALS.PAYLOAD_CHAR_CAP) },
|
|
};
|
|
const ctx = makeCtx({ peer_global: [bigPeer, bigPeer] });
|
|
const { trimmed } = buildAssetAuditUserPayload(ctx as never);
|
|
expect(trimmed.peer_global_dropped).toBeGreaterThan(0);
|
|
});
|
|
});
|