789efe4234
* feat: disable subagent summaries and label subagent observations Detect Claude Code subagent hook context via `agent_id`/`agent_type` on stdin, short-circuit the Stop-hook summary path when present, and thread the subagent identity end-to-end onto observation rows (new `agent_type` and `agent_id` columns, migration 010 at version 27). Main-session rows remain NULL; content-hash dedup is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: address PR #2073 review feedback - Narrow summarize subagent guard to agentId only so --agent-started main sessions still own their summary (agentType alone is main-session). - Remove now-dead agentId/agentType spreads from the summarize POST body. - Always overwrite pendingAgentId/pendingAgentType in SDK/Gemini/OpenRouter agents (clears stale subagent identity on main-session messages after a subagent message in the same batch). - Add idx_observations_agent_id index in migration 010 + the mirror migration in SessionStore + the runner. - Replace console.log in migration010 with logger.debug. - Update summarize test: agentType alone no longer short-circuits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: address CodeRabbit + claude-review iteration 4 feedback - SessionRoutes.handleSummarizeByClaudeId: narrow worker-side guard to agentId only (matches hook-side). agentType alone = --agent main session, which still owns its summary. - ResponseProcessor: wrap storeObservations in try/finally so pendingAgentId/Type clear even if storage throws. Prevents stale subagent identity from leaking into the next batch on error. - SessionStore.importObservation + bulk.importObservation: persist agent_type/agent_id so backup/import round-trips preserve subagent attribution. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * polish: claude-review iteration 5 cleanup - Use ?? not || for nullable subagent fields in PendingMessageStore (prevents treating empty string as null). - Simplify observation.ts body spread — include fields unconditionally; JSON.stringify drops undefined anyway. - Narrow any[] to Array<{ name: string }> in migration010 column checks. - Add trailing newline to migrations.ts. - Document in observations/store.ts why the dedup hash intentionally excludes agent fields. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * polish: claude-review iteration 7 feedback - claude-code adapter: add 128-char safety cap on agent_id/agent_type so a malformed Claude Code payload cannot balloon DB rows. Empty strings now also treated as absent. - migration010: state-aware debug log lists only columns actually added; idempotent re-runs log "already present; ensured indexes". - Add 3 adapter tests covering the length cap boundary and empty-string rejection. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * perf: skip subagent summary before worker bootstrap Move the agentId short-circuit above ensureWorkerRunning() so a Stop hook fired inside a subagent does not trigger worker startup just to return early. Addresses CodeRabbit nit on summarize.ts:36-47. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
121 lines
3.7 KiB
TypeScript
121 lines
3.7 KiB
TypeScript
/**
|
|
* Tests for Claude Code adapter subagent field extraction.
|
|
*
|
|
* Validates that normalizeInput picks up the `agent_id` / `agent_type`
|
|
* fields from Claude Code hook stdin and that the type guard rejects
|
|
* non-string values. These fields are the discriminator for subagent
|
|
* context; they are undefined in main-session payloads.
|
|
*
|
|
* Sources:
|
|
* - Adapter: src/cli/adapters/claude-code.ts
|
|
* - Types: src/cli/types.ts
|
|
*/
|
|
import { describe, it, expect } from 'bun:test';
|
|
import { claudeCodeAdapter } from '../../../src/cli/adapters/claude-code.js';
|
|
|
|
describe('claudeCodeAdapter.normalizeInput — subagent fields', () => {
|
|
it('extracts agentId and agentType when both are present', () => {
|
|
const normalized = claudeCodeAdapter.normalizeInput({
|
|
session_id: 's1',
|
|
cwd: '/tmp',
|
|
agent_id: 'agent-abc',
|
|
agent_type: 'Explore',
|
|
});
|
|
|
|
expect(normalized.sessionId).toBe('s1');
|
|
expect(normalized.cwd).toBe('/tmp');
|
|
expect(normalized.agentId).toBe('agent-abc');
|
|
expect(normalized.agentType).toBe('Explore');
|
|
});
|
|
|
|
it('leaves agentId and agentType undefined when fields are absent (main-session payload)', () => {
|
|
const normalized = claudeCodeAdapter.normalizeInput({
|
|
session_id: 's1',
|
|
cwd: '/tmp',
|
|
});
|
|
|
|
expect(normalized.sessionId).toBe('s1');
|
|
expect(normalized.agentId).toBeUndefined();
|
|
expect(normalized.agentType).toBeUndefined();
|
|
});
|
|
|
|
it('rejects non-string agent_id via type guard (returns undefined)', () => {
|
|
const normalized = claudeCodeAdapter.normalizeInput({
|
|
session_id: 's1',
|
|
cwd: '/tmp',
|
|
agent_id: 42,
|
|
});
|
|
|
|
expect(normalized.agentId).toBeUndefined();
|
|
});
|
|
|
|
it('rejects non-string agent_type via type guard (returns undefined)', () => {
|
|
const normalized = claudeCodeAdapter.normalizeInput({
|
|
session_id: 's1',
|
|
cwd: '/tmp',
|
|
agent_type: { kind: 'Explore' },
|
|
});
|
|
|
|
expect(normalized.agentType).toBeUndefined();
|
|
});
|
|
|
|
it('extracts agentId alone even when agent_type is missing', () => {
|
|
const normalized = claudeCodeAdapter.normalizeInput({
|
|
session_id: 's1',
|
|
cwd: '/tmp',
|
|
agent_id: 'agent-only',
|
|
});
|
|
|
|
expect(normalized.agentId).toBe('agent-only');
|
|
expect(normalized.agentType).toBeUndefined();
|
|
});
|
|
|
|
it('handles null/undefined raw input gracefully (SessionStart hook)', () => {
|
|
const normalizedNull = claudeCodeAdapter.normalizeInput(null);
|
|
const normalizedUndef = claudeCodeAdapter.normalizeInput(undefined);
|
|
|
|
expect(normalizedNull.agentId).toBeUndefined();
|
|
expect(normalizedNull.agentType).toBeUndefined();
|
|
expect(normalizedUndef.agentId).toBeUndefined();
|
|
expect(normalizedUndef.agentType).toBeUndefined();
|
|
});
|
|
|
|
it('drops agent fields that exceed the 128-char safety cap', () => {
|
|
const oversized = 'a'.repeat(129);
|
|
const normalized = claudeCodeAdapter.normalizeInput({
|
|
session_id: 's1',
|
|
cwd: '/tmp',
|
|
agent_id: oversized,
|
|
agent_type: oversized,
|
|
});
|
|
|
|
expect(normalized.agentId).toBeUndefined();
|
|
expect(normalized.agentType).toBeUndefined();
|
|
});
|
|
|
|
it('keeps agent fields exactly at the 128-char boundary', () => {
|
|
const atLimit = 'a'.repeat(128);
|
|
const normalized = claudeCodeAdapter.normalizeInput({
|
|
session_id: 's1',
|
|
cwd: '/tmp',
|
|
agent_id: atLimit,
|
|
agent_type: atLimit,
|
|
});
|
|
|
|
expect(normalized.agentId).toBe(atLimit);
|
|
expect(normalized.agentType).toBe(atLimit);
|
|
});
|
|
|
|
it('drops empty-string agent fields (treat as absent)', () => {
|
|
const normalized = claudeCodeAdapter.normalizeInput({
|
|
session_id: 's1',
|
|
cwd: '/tmp',
|
|
agent_id: '',
|
|
agent_type: '',
|
|
});
|
|
|
|
expect(normalized.agentId).toBeUndefined();
|
|
expect(normalized.agentType).toBeUndefined();
|
|
});
|
|
});
|