feat: add missing analysis sections to session report
Add 7 missing sections and fix 4 incomplete ones that the Python analyzer generates but the TypeScript port was missing: New sections: skillsInvoked, bashCommands, lifecycleTasks, userQuestions, outOfScopeFindings, agentTree, subagentsList Fixed: compaction (was just count, now has summaryCount + note), serviceTiers removed (not available in parsed types), compactionCount replaced with compaction object Adds InsightsSection UI component and 8 new tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3f479dcf07
commit
ab1ad071fe
5 changed files with 609 additions and 7 deletions
|
|
@ -7,6 +7,7 @@ import { CostSection } from './sections/CostSection';
|
|||
import { ErrorSection } from './sections/ErrorSection';
|
||||
import { FrictionSection } from './sections/FrictionSection';
|
||||
import { GitSection } from './sections/GitSection';
|
||||
import { InsightsSection } from './sections/InsightsSection';
|
||||
import { OverviewSection } from './sections/OverviewSection';
|
||||
import { QualitySection } from './sections/QualitySection';
|
||||
import { SubagentSection } from './sections/SubagentSection';
|
||||
|
|
@ -63,6 +64,15 @@ export const SessionReportTab = ({ tab }: SessionReportTabProps) => {
|
|||
startup={report.startupOverhead}
|
||||
testProgression={report.testProgression}
|
||||
/>
|
||||
<InsightsSection
|
||||
skills={report.skillsInvoked}
|
||||
bash={report.bashCommands}
|
||||
lifecycleTasks={report.lifecycleTasks}
|
||||
userQuestions={report.userQuestions}
|
||||
outOfScope={report.outOfScopeFindings}
|
||||
agentTree={report.agentTree}
|
||||
subagentsList={report.subagentsList}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
202
src/renderer/components/report/sections/InsightsSection.tsx
Normal file
202
src/renderer/components/report/sections/InsightsSection.tsx
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
import { Lightbulb } from 'lucide-react';
|
||||
|
||||
import { ReportSection } from '../ReportSection';
|
||||
|
||||
import type {
|
||||
OutOfScopeFindings,
|
||||
ReportAgentTree,
|
||||
ReportBashCommands,
|
||||
SkillInvocation,
|
||||
SubagentBasicEntry,
|
||||
UserQuestion,
|
||||
} from '@renderer/types/sessionReport';
|
||||
|
||||
interface InsightsSectionProps {
|
||||
skills: SkillInvocation[];
|
||||
bash: ReportBashCommands;
|
||||
lifecycleTasks: string[];
|
||||
userQuestions: UserQuestion[];
|
||||
outOfScope: OutOfScopeFindings[];
|
||||
agentTree: ReportAgentTree;
|
||||
subagentsList: SubagentBasicEntry[];
|
||||
}
|
||||
|
||||
export const InsightsSection = ({
|
||||
skills,
|
||||
bash,
|
||||
lifecycleTasks,
|
||||
userQuestions,
|
||||
outOfScope,
|
||||
agentTree,
|
||||
subagentsList,
|
||||
}: InsightsSectionProps) => {
|
||||
return (
|
||||
<ReportSection title="Session Insights" icon={Lightbulb}>
|
||||
{/* Skills invoked */}
|
||||
{skills.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<div className="mb-2 text-xs font-medium text-text-muted">
|
||||
Skills Invoked ({skills.length})
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
{skills.map((s, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2 px-2 py-0.5 text-xs">
|
||||
<span className="font-mono text-text">{s.skill}</span>
|
||||
{s.argsPreview && <span className="truncate text-text-muted">{s.argsPreview}</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bash commands */}
|
||||
<div className="mb-4">
|
||||
<div className="mb-2 text-xs font-medium text-text-muted">Bash Commands</div>
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
|
||||
<div>
|
||||
<div className="text-xs text-text-muted">Total</div>
|
||||
<div className="text-sm font-medium text-text">{bash.total}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-text-muted">Unique</div>
|
||||
<div className="text-sm font-medium text-text">{bash.unique}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-text-muted">Repeated</div>
|
||||
<div className="text-sm font-medium text-text">{Object.keys(bash.repeated).length}</div>
|
||||
</div>
|
||||
</div>
|
||||
{Object.keys(bash.repeated).length > 0 && (
|
||||
<div className="mt-2 flex flex-col gap-1">
|
||||
{Object.entries(bash.repeated)
|
||||
.slice(0, 10)
|
||||
.map(([cmd, count], idx) => (
|
||||
<div key={idx} className="flex items-center gap-2 px-2 py-0.5 text-xs">
|
||||
<span className="font-mono text-text-muted">{count}x</span>
|
||||
<span className="truncate font-mono text-text-secondary">{cmd}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Task tool subagent list */}
|
||||
{subagentsList.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<div className="mb-2 text-xs font-medium text-text-muted">
|
||||
Task Dispatches ({subagentsList.length})
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
{subagentsList.map((s, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2 px-2 py-0.5 text-xs">
|
||||
<span className="rounded bg-surface-raised px-1.5 py-0.5 text-text-muted">
|
||||
{s.subagentType}
|
||||
</span>
|
||||
<span className="truncate text-text">{s.description}</span>
|
||||
{s.runInBackground && <span className="text-text-muted">(background)</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Lifecycle tasks */}
|
||||
{lifecycleTasks.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<div className="mb-2 text-xs font-medium text-text-muted">
|
||||
Tasks Created ({lifecycleTasks.length})
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
{lifecycleTasks.map((task, idx) => (
|
||||
<div key={idx} className="px-2 py-0.5 text-xs text-text-secondary">
|
||||
{task}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* User questions */}
|
||||
{userQuestions.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<div className="mb-2 text-xs font-medium text-text-muted">
|
||||
Questions Asked ({userQuestions.length})
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{userQuestions.map((q, idx) => (
|
||||
<div key={idx} className="rounded-md bg-surface-raised px-3 py-2">
|
||||
<div className="text-xs text-text">{q.question}</div>
|
||||
{q.options.length > 0 && (
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
{q.options.map((opt, optIdx) => (
|
||||
<span
|
||||
key={optIdx}
|
||||
className="rounded px-1.5 py-0.5 text-xs text-text-muted"
|
||||
style={{ backgroundColor: 'var(--color-surface-overlay)' }}
|
||||
>
|
||||
{opt}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Agent tree */}
|
||||
{agentTree.agentCount > 0 && (
|
||||
<div className="mb-4">
|
||||
<div className="mb-2 text-xs font-medium text-text-muted">
|
||||
Agent Tree ({agentTree.agentCount} agent{agentTree.agentCount !== 1 ? 's' : ''})
|
||||
{agentTree.hasTeamMode && (
|
||||
<span className="ml-2 rounded px-1.5 py-0.5 text-xs" style={{ color: '#60a5fa' }}>
|
||||
Team Mode
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{agentTree.teamNames.length > 0 && (
|
||||
<div className="mb-2 text-xs text-text-muted">
|
||||
Teams: {agentTree.teamNames.join(', ')}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-1">
|
||||
{agentTree.agents.map((agent, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2 px-2 py-0.5 text-xs">
|
||||
<span className="rounded bg-surface-raised px-1.5 py-0.5 text-text-muted">
|
||||
{agent.agentType}
|
||||
</span>
|
||||
<span className="truncate font-mono text-text-secondary">
|
||||
{agent.agentId.slice(0, 12)}...
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Out-of-scope findings */}
|
||||
{outOfScope.length > 0 && (
|
||||
<div>
|
||||
<div className="mb-2 text-xs font-medium text-text-muted">
|
||||
Out-of-Scope Findings ({outOfScope.length})
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{outOfScope.map((f, idx) => (
|
||||
<div key={idx} className="rounded-md bg-surface-raised px-3 py-2">
|
||||
<span
|
||||
className="mr-2 rounded px-1.5 py-0.5 text-xs"
|
||||
style={{ backgroundColor: '#fbbf2420', color: '#fbbf24' }}
|
||||
>
|
||||
{f.keyword}
|
||||
</span>
|
||||
<span className="text-xs text-text-secondary">{f.snippet}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</ReportSection>
|
||||
);
|
||||
};
|
||||
|
|
@ -273,6 +273,60 @@ export interface ReportFileReadRedundancy {
|
|||
redundantFiles: Record<string, number>;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Missing Sections (ported from Python analyzer)
|
||||
// =============================================================================
|
||||
|
||||
export interface SkillInvocation {
|
||||
skill: string;
|
||||
argsPreview: string;
|
||||
}
|
||||
|
||||
export interface ReportBashCommands {
|
||||
total: number;
|
||||
unique: number;
|
||||
repeated: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface UserQuestion {
|
||||
question: string;
|
||||
options: string[];
|
||||
}
|
||||
|
||||
export interface OutOfScopeFindings {
|
||||
keyword: string;
|
||||
messageIndex: number;
|
||||
snippet: string;
|
||||
}
|
||||
|
||||
export interface AgentTreeNode {
|
||||
agentId: string;
|
||||
agentType: string;
|
||||
teamName: string;
|
||||
parentToolUseId: string;
|
||||
messageIndex: number;
|
||||
}
|
||||
|
||||
export interface ReportAgentTree {
|
||||
agentCount: number;
|
||||
agents: AgentTreeNode[];
|
||||
hasTeamMode: boolean;
|
||||
teamNames: string[];
|
||||
}
|
||||
|
||||
export interface ReportCompaction {
|
||||
count: number;
|
||||
compactSummaryCount: number;
|
||||
note: string;
|
||||
}
|
||||
|
||||
export interface SubagentBasicEntry {
|
||||
description: string;
|
||||
subagentType: string;
|
||||
model: string;
|
||||
runInBackground: boolean;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Combined Report
|
||||
// =============================================================================
|
||||
|
|
@ -284,6 +338,7 @@ export interface SessionReport {
|
|||
cacheEconomics: ReportCacheEconomics;
|
||||
toolUsage: ReportToolUsage;
|
||||
subagentMetrics: ReportSubagentMetrics;
|
||||
subagentsList: SubagentBasicEntry[];
|
||||
errors: ReportErrors;
|
||||
gitActivity: ReportGitActivity;
|
||||
frictionSignals: ReportFrictionSignals;
|
||||
|
|
@ -299,8 +354,13 @@ export interface SessionReport {
|
|||
thinkingBlocks: ReportThinkingBlocks;
|
||||
keyEvents: KeyEvent[];
|
||||
messageTypes: Record<string, number>;
|
||||
serviceTiers: Record<string, number>;
|
||||
fileReadRedundancy: ReportFileReadRedundancy;
|
||||
compactionCount: number;
|
||||
compaction: ReportCompaction;
|
||||
gitBranches: string[];
|
||||
skillsInvoked: SkillInvocation[];
|
||||
bashCommands: ReportBashCommands;
|
||||
lifecycleTasks: string[];
|
||||
userQuestions: UserQuestion[];
|
||||
outOfScopeFindings: OutOfScopeFindings[];
|
||||
agentTree: ReportAgentTree;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
*/
|
||||
|
||||
import type {
|
||||
AgentTreeNode,
|
||||
FrictionCorrection,
|
||||
GitCommit,
|
||||
IdleGap,
|
||||
|
|
@ -16,11 +17,15 @@ import type {
|
|||
ModelPricing,
|
||||
ModelSwitch,
|
||||
ModelTokenStats,
|
||||
OutOfScopeFindings,
|
||||
SessionReport,
|
||||
SkillInvocation,
|
||||
SubagentBasicEntry,
|
||||
SubagentEntry,
|
||||
TestSnapshot,
|
||||
ThinkingBlockAnalysis,
|
||||
ToolError,
|
||||
UserQuestion,
|
||||
} from '@renderer/types/sessionReport';
|
||||
import type {
|
||||
ContentBlock,
|
||||
|
|
@ -367,6 +372,38 @@ export function analyzeSession(detail: SessionDetail): SessionReport {
|
|||
let firstUserMessageLength = 0;
|
||||
let firstUserSeen = false;
|
||||
|
||||
// Skills invoked
|
||||
const skillsInvoked: SkillInvocation[] = [];
|
||||
|
||||
// Bash commands
|
||||
const bashCmds: string[] = [];
|
||||
|
||||
// Subagents list (backward compat)
|
||||
const subagentsList: SubagentBasicEntry[] = [];
|
||||
|
||||
// Lifecycle tasks
|
||||
const lifecycleTasks: string[] = [];
|
||||
|
||||
// User questions
|
||||
const userQuestions: UserQuestion[] = [];
|
||||
|
||||
// Out-of-scope findings
|
||||
const OUT_OF_SCOPE_KEYWORDS = [
|
||||
'pre-existing',
|
||||
'out of scope',
|
||||
'tech debt',
|
||||
'anti-pattern',
|
||||
'existed before',
|
||||
];
|
||||
const outOfScopeFindings: OutOfScopeFindings[] = [];
|
||||
|
||||
// Agent tree metadata
|
||||
const agentTreeNodes: AgentTreeNode[] = [];
|
||||
|
||||
// Compact summary count
|
||||
|
||||
let compactSummaryCount = 0;
|
||||
|
||||
// ===================================================================
|
||||
// SINGLE PASS
|
||||
// ===================================================================
|
||||
|
|
@ -435,7 +472,21 @@ export function analyzeSession(detail: SessionDetail): SessionReport {
|
|||
// --- Git branches ---
|
||||
if (m.gitBranch) branches.add(m.gitBranch);
|
||||
|
||||
// --- Compact summaries (counted but not accumulated separately) ---
|
||||
// --- Compact summaries ---
|
||||
if (m.isCompactSummary) compactSummaryCount++;
|
||||
|
||||
// --- Agent tree metadata ---
|
||||
if (m.agentId) {
|
||||
// agentType/teamName/parentToolUseId may exist on raw data but not typed in ParsedMessage
|
||||
const raw = m as unknown as Record<string, unknown>;
|
||||
agentTreeNodes.push({
|
||||
agentId: m.agentId,
|
||||
agentType: (raw.agentType as string) ?? 'unknown',
|
||||
teamName: (raw.teamName as string) ?? '',
|
||||
parentToolUseId: (raw.parentToolUseId as string) ?? '',
|
||||
messageIndex: i,
|
||||
});
|
||||
}
|
||||
|
||||
// --- Thinking blocks (with content analysis) ---
|
||||
if (Array.isArray(m.content)) {
|
||||
|
|
@ -507,6 +558,8 @@ export function analyzeSession(detail: SessionDetail): SessionReport {
|
|||
// Bash commands
|
||||
if (toolName === 'Bash') {
|
||||
const cmd = typeof inp.command === 'string' ? inp.command : '';
|
||||
const cmdTrunc = cmd.slice(0, 200);
|
||||
bashCmds.push(cmdTrunc);
|
||||
// Thrashing: bash prefix groups
|
||||
const prefix = cmd.slice(0, 40);
|
||||
bashPrefixGroups.set(prefix, (bashPrefixGroups.get(prefix) ?? 0) + 1);
|
||||
|
|
@ -530,6 +583,42 @@ export function analyzeSession(detail: SessionDetail): SessionReport {
|
|||
}
|
||||
}
|
||||
|
||||
// Skills
|
||||
if (toolName === 'Skill') {
|
||||
skillsInvoked.push({
|
||||
skill: (inp.skill as string) ?? 'unknown',
|
||||
argsPreview: String(inp.args ?? '').slice(0, 120),
|
||||
});
|
||||
}
|
||||
|
||||
// Task (subagent list)
|
||||
if (toolName === 'Task') {
|
||||
subagentsList.push({
|
||||
description: (inp.description as string) ?? 'unknown',
|
||||
subagentType: (inp.subagent_type as string) ?? 'unknown',
|
||||
model: (inp.model as string) ?? 'default (inherits parent)',
|
||||
runInBackground: (inp.run_in_background as boolean) ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
// TaskCreate
|
||||
if (toolName === 'TaskCreate') {
|
||||
lifecycleTasks.push((inp.subject as string) ?? 'unknown');
|
||||
}
|
||||
|
||||
// AskUserQuestion
|
||||
if (toolName === 'AskUserQuestion') {
|
||||
const questions = inp.questions as { question?: string; options?: { label?: string }[] }[];
|
||||
if (Array.isArray(questions)) {
|
||||
for (const q of questions) {
|
||||
userQuestions.push({
|
||||
question: q.question ?? '',
|
||||
options: Array.isArray(q.options) ? q.options.map((o) => o.label ?? '') : [],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// File reads
|
||||
if (toolName === 'Read') {
|
||||
const filePath = (inp.file_path as string) ?? '';
|
||||
|
|
@ -619,6 +708,25 @@ export function analyzeSession(detail: SessionDetail): SessionReport {
|
|||
}
|
||||
}
|
||||
|
||||
// --- Out-of-scope findings (assistant messages) ---
|
||||
if (msgType === 'assistant') {
|
||||
const contentText = extractTextContent(m);
|
||||
const contentLower = contentText.toLowerCase();
|
||||
for (const kw of OUT_OF_SCOPE_KEYWORDS) {
|
||||
const kwIdx = contentLower.indexOf(kw.toLowerCase());
|
||||
if (kwIdx >= 0) {
|
||||
const start = Math.max(0, kwIdx - 80);
|
||||
const end = Math.min(contentText.length, kwIdx + 300);
|
||||
outOfScopeFindings.push({
|
||||
keyword: kw,
|
||||
messageIndex: i,
|
||||
snippet: contentText.slice(start, end).replace(/\n/g, ' ').trim(),
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Tool results ---
|
||||
for (const tr of m.toolResults) {
|
||||
const toolUseId = tr.toolUseId ?? '';
|
||||
|
|
@ -1040,6 +1148,8 @@ export function analyzeSession(detail: SessionDetail): SessionReport {
|
|||
|
||||
subagentMetrics: saFromProcesses,
|
||||
|
||||
subagentsList,
|
||||
|
||||
errors: {
|
||||
errors,
|
||||
permissionDenials: {
|
||||
|
|
@ -1140,8 +1250,6 @@ export function analyzeSession(detail: SessionDetail): SessionReport {
|
|||
|
||||
messageTypes,
|
||||
|
||||
serviceTiers: {},
|
||||
|
||||
fileReadRedundancy: {
|
||||
totalReads,
|
||||
uniqueFiles,
|
||||
|
|
@ -1149,9 +1257,51 @@ export function analyzeSession(detail: SessionDetail): SessionReport {
|
|||
redundantFiles,
|
||||
},
|
||||
|
||||
compactionCount: session.compactionCount ?? 0,
|
||||
compaction: {
|
||||
count: session.compactionCount ?? 0,
|
||||
compactSummaryCount,
|
||||
note:
|
||||
(session.compactionCount ?? 0) > 0
|
||||
? 'Session underwent compaction, which may have caused loss of earlier context. Check for repeated work after compaction events.'
|
||||
: 'No compaction occurred — session stayed within context limits.',
|
||||
},
|
||||
|
||||
gitBranches: [...branches],
|
||||
|
||||
skillsInvoked,
|
||||
|
||||
bashCommands: {
|
||||
total: bashCmds.length,
|
||||
unique: new Set(bashCmds).size,
|
||||
repeated: Object.fromEntries(
|
||||
[
|
||||
...bashCmds
|
||||
.reduce((acc, cmd) => acc.set(cmd, (acc.get(cmd) ?? 0) + 1), new Map<string, number>())
|
||||
.entries(),
|
||||
]
|
||||
.filter(([, count]) => count > 1)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
),
|
||||
},
|
||||
|
||||
lifecycleTasks,
|
||||
|
||||
userQuestions,
|
||||
|
||||
outOfScopeFindings,
|
||||
|
||||
agentTree: (() => {
|
||||
const uniqueAgents = new Map<string, AgentTreeNode>();
|
||||
for (const node of agentTreeNodes) {
|
||||
if (!uniqueAgents.has(node.agentId)) uniqueAgents.set(node.agentId, node);
|
||||
}
|
||||
return {
|
||||
agentCount: uniqueAgents.size,
|
||||
agents: [...uniqueAgents.values()],
|
||||
hasTeamMode: agentTreeNodes.some((n) => n.teamName),
|
||||
teamNames: [...new Set(agentTreeNodes.filter((n) => n.teamName).map((n) => n.teamName))],
|
||||
};
|
||||
})(),
|
||||
};
|
||||
|
||||
return report;
|
||||
|
|
|
|||
|
|
@ -110,8 +110,18 @@ describe('analyzeSession', () => {
|
|||
expect(report.tokenDensityTimeline.quartiles).toHaveLength(4);
|
||||
expect(report.tokenDensityTimeline.quartiles.every((q) => q.avgTokens === 0)).toBe(true);
|
||||
|
||||
expect(report.compactionCount).toBe(0);
|
||||
expect(report.compaction.count).toBe(0);
|
||||
expect(report.compaction.compactSummaryCount).toBe(0);
|
||||
expect(report.gitBranches).toEqual([]);
|
||||
|
||||
// New sections
|
||||
expect(report.skillsInvoked).toEqual([]);
|
||||
expect(report.bashCommands.total).toBe(0);
|
||||
expect(report.lifecycleTasks).toEqual([]);
|
||||
expect(report.userQuestions).toEqual([]);
|
||||
expect(report.outOfScopeFindings).toEqual([]);
|
||||
expect(report.agentTree.agentCount).toBe(0);
|
||||
expect(report.subagentsList).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -1001,4 +1011,174 @@ describe('analyzeSession', () => {
|
|||
expect(report.thrashingSignals.editReworkFiles[0].editIndices).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('skills invoked', () => {
|
||||
it('tracks Skill tool calls', () => {
|
||||
const messages: ParsedMessage[] = [
|
||||
createMockMessage({
|
||||
type: 'assistant',
|
||||
toolCalls: [
|
||||
{
|
||||
id: 'tc-1',
|
||||
name: 'Skill',
|
||||
input: { skill: 'brainstorming', args: '--verbose' },
|
||||
isTask: false,
|
||||
},
|
||||
{ id: 'tc-2', name: 'Skill', input: { skill: 'writing-plans' }, isTask: false },
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const report = analyzeSession(createMockDetail({ messages }));
|
||||
|
||||
expect(report.skillsInvoked).toHaveLength(2);
|
||||
expect(report.skillsInvoked[0].skill).toBe('brainstorming');
|
||||
expect(report.skillsInvoked[0].argsPreview).toBe('--verbose');
|
||||
expect(report.skillsInvoked[1].skill).toBe('writing-plans');
|
||||
});
|
||||
});
|
||||
|
||||
describe('bash commands', () => {
|
||||
it('tracks total, unique, and repeated commands', () => {
|
||||
const messages: ParsedMessage[] = [
|
||||
createMockMessage({
|
||||
type: 'assistant',
|
||||
toolCalls: [
|
||||
{ id: 'tc-1', name: 'Bash', input: { command: 'pnpm test' }, isTask: false },
|
||||
{ id: 'tc-2', name: 'Bash', input: { command: 'pnpm test' }, isTask: false },
|
||||
{ id: 'tc-3', name: 'Bash', input: { command: 'git status' }, isTask: false },
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const report = analyzeSession(createMockDetail({ messages }));
|
||||
|
||||
expect(report.bashCommands.total).toBe(3);
|
||||
expect(report.bashCommands.unique).toBe(2);
|
||||
expect(report.bashCommands.repeated['pnpm test']).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('subagents list', () => {
|
||||
it('tracks Task tool dispatches', () => {
|
||||
const messages: ParsedMessage[] = [
|
||||
createMockMessage({
|
||||
type: 'assistant',
|
||||
toolCalls: [
|
||||
{
|
||||
id: 'tc-1',
|
||||
name: 'Task',
|
||||
input: {
|
||||
description: 'explore auth',
|
||||
subagent_type: 'Explore',
|
||||
run_in_background: true,
|
||||
},
|
||||
isTask: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const report = analyzeSession(createMockDetail({ messages }));
|
||||
|
||||
expect(report.subagentsList).toHaveLength(1);
|
||||
expect(report.subagentsList[0].description).toBe('explore auth');
|
||||
expect(report.subagentsList[0].subagentType).toBe('Explore');
|
||||
expect(report.subagentsList[0].runInBackground).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('lifecycle tasks', () => {
|
||||
it('tracks TaskCreate subjects', () => {
|
||||
const messages: ParsedMessage[] = [
|
||||
createMockMessage({
|
||||
type: 'assistant',
|
||||
toolCalls: [
|
||||
{ id: 'tc-1', name: 'TaskCreate', input: { subject: 'Add login page' }, isTask: false },
|
||||
{ id: 'tc-2', name: 'TaskCreate', input: { subject: 'Write tests' }, isTask: false },
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const report = analyzeSession(createMockDetail({ messages }));
|
||||
|
||||
expect(report.lifecycleTasks).toEqual(['Add login page', 'Write tests']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('user questions', () => {
|
||||
it('tracks AskUserQuestion calls', () => {
|
||||
const messages: ParsedMessage[] = [
|
||||
createMockMessage({
|
||||
type: 'assistant',
|
||||
toolCalls: [
|
||||
{
|
||||
id: 'tc-1',
|
||||
name: 'AskUserQuestion',
|
||||
input: {
|
||||
questions: [
|
||||
{
|
||||
question: 'Which auth method?',
|
||||
options: [{ label: 'JWT' }, { label: 'OAuth' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
isTask: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const report = analyzeSession(createMockDetail({ messages }));
|
||||
|
||||
expect(report.userQuestions).toHaveLength(1);
|
||||
expect(report.userQuestions[0].question).toBe('Which auth method?');
|
||||
expect(report.userQuestions[0].options).toEqual(['JWT', 'OAuth']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('out-of-scope findings', () => {
|
||||
it('detects pre-existing and tech debt mentions', () => {
|
||||
const messages: ParsedMessage[] = [
|
||||
createMockMessage({
|
||||
type: 'assistant',
|
||||
content: 'This is a pre-existing issue that was there before our changes.',
|
||||
}),
|
||||
createMockMessage({
|
||||
type: 'assistant',
|
||||
content: 'I noticed some tech debt in the authentication module.',
|
||||
}),
|
||||
];
|
||||
|
||||
const report = analyzeSession(createMockDetail({ messages }));
|
||||
|
||||
expect(report.outOfScopeFindings).toHaveLength(2);
|
||||
expect(report.outOfScopeFindings[0].keyword).toBe('pre-existing');
|
||||
expect(report.outOfScopeFindings[1].keyword).toBe('tech debt');
|
||||
});
|
||||
});
|
||||
|
||||
describe('compaction', () => {
|
||||
it('tracks compaction count and summary messages', () => {
|
||||
const messages: ParsedMessage[] = [
|
||||
createMockMessage({ type: 'assistant', isCompactSummary: true }),
|
||||
createMockMessage({ type: 'assistant', isCompactSummary: true }),
|
||||
];
|
||||
|
||||
const session = createMockSession();
|
||||
session.compactionCount = 2;
|
||||
const report = analyzeSession(createMockDetail({ messages, session }));
|
||||
|
||||
expect(report.compaction.count).toBe(2);
|
||||
expect(report.compaction.compactSummaryCount).toBe(2);
|
||||
expect(report.compaction.note).toContain('underwent compaction');
|
||||
});
|
||||
|
||||
it('reports no compaction', () => {
|
||||
const report = analyzeSession(createMockDetail({}));
|
||||
|
||||
expect(report.compaction.count).toBe(0);
|
||||
expect(report.compaction.note).toContain('No compaction');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue