fix(cli): memory add no longer prompts when --category is supplied (non-interactive agents)

Local-mode branch of 'memory add' omitted category when calling memoryAdd, so it always fell through to an interactive select() prompt — which throws in non-TTY agent contexts and breaks the reporting loop. Forward category; add tests for the non-interactive flag path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-06-26 22:59:37 +02:00
parent 9c749beacb
commit 9f3dc28030
2 changed files with 87 additions and 1 deletions

View File

@ -1,7 +1,7 @@
import { Command } from 'commander';
import { init } from './commands/init.js';
import { status } from './commands/status.js';
import { memoryAdd, memorySearch, memoryList } from './commands/memory.js';
import { memoryAdd, memorySearch, memoryList, type MemoryAddOptions } from './commands/memory.js';
import { taskCreate, taskList, taskShow, taskClaim, taskDone } from './commands/task.js';
import { handoffCreate, handoffRead, handoffList } from './commands/handoff.js';
import { decisionCreate, decisionList } from './commands/decision.js';
@ -146,6 +146,7 @@ export function createProgram(cwd: string): Command {
} else {
await memoryAdd(projectCwd, {
title: options.title as string | undefined,
category: options.category as MemoryAddOptions['category'],
content: options.content as string | undefined,
relatedTasks,
tokens,

85
tests/memory-cmd.test.ts Normal file
View File

@ -0,0 +1,85 @@
/**
* Regression tests for `memory add` non-interactive path.
*
* Before the fix, `memoryAdd` called from the local-mode CLI branch omitted
* `category` in the options object. Because `memoryAdd` resolves category via
* `options.category ?? await select(...)`, the missing field caused it to open
* an @inquirer/prompts interactive select even when --category was supplied
* which throws "User force closed the prompt with 0 null" in any non-TTY
* (agent/background) context.
*
* These tests call `memoryAdd` directly with all fields provided and assert
* that it completes without prompting. If prompting is triggered the test will
* throw in the non-TTY vitest environment, reproducing the original failure.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync, readdirSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { memoryAdd } from '../src/cli/commands/memory.js';
import { init } from '../src/cli/commands/init.js';
import { listMemory } from '../src/core/services/memoryService.js';
import { readEntity } from '../src/core/files.js';
describe('memoryAdd — non-interactive (all flags supplied)', () => {
let cwd: string;
beforeEach(async () => {
cwd = mkdtempSync(join(tmpdir(), 'ah-mem-cmd-'));
await init(cwd, { yes: true, projectName: 'test' });
});
afterEach(() => {
rmSync(cwd, { recursive: true, force: true });
});
it('succeeds without prompting when title, category and content are provided', async () => {
// This must not throw — if it prompts it throws in non-TTY vitest.
await memoryAdd(cwd, {
title: 'Session summary',
category: 'implementation',
content: 'Completed TSK-0010 DNS cache work.',
});
const memories = listMemory(cwd);
expect(memories).toHaveLength(1);
expect(memories[0].title).toBe('Session summary');
// Verify category was persisted in the markdown frontmatter (index does
// not carry category, so we read the file directly).
const memDir = join(cwd, '.agenthub', 'memory');
const files = readdirSync(memDir).filter((f) => f.endsWith('.md'));
expect(files).toHaveLength(1);
const { frontmatter } = readEntity(join(memDir, files[0]));
expect(frontmatter.category).toBe('implementation');
});
it('succeeds with all activity-timeline flags (the exact agent invocation pattern)', async () => {
// Reproduces the exact flags agents pass after completing a task.
await memoryAdd(cwd, {
title: 'TSK-0010 implementation done',
category: 'implementation',
content: 'Implemented activity timeline. All tests green.',
relatedTasks: ['TSK-0010'],
tokens: 63576,
duration: 384987,
by: 'ops',
});
const memories = listMemory(cwd);
expect(memories).toHaveLength(1);
expect(memories[0].title).toBe('TSK-0010 implementation done');
// Verify category, relatedTasks, tokens, duration, by in the frontmatter.
const memDir = join(cwd, '.agenthub', 'memory');
const files = readdirSync(memDir).filter((f) => f.endsWith('.md'));
expect(files).toHaveLength(1);
const { frontmatter } = readEntity(join(memDir, files[0]));
expect(frontmatter.category).toBe('implementation');
expect(frontmatter.tokens).toBe(63576);
expect(frontmatter.duration).toBe(384987);
expect(frontmatter.by).toBe('ops');
// relatedTasks is a YAML sequence
expect(frontmatter.relatedTasks).toContain('TSK-0010');
});
});