Files
claude-mem/tests/cli/adapters/claude-code-subagent.test.ts
T
Alex Newman 789efe4234 feat: disable subagent summaries, label subagent observations (#2073)
* 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>
2026-04-19 14:58:01 -07:00

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();
});
});