diff --git a/src/cli/index.ts b/src/cli/index.ts index 6a9c488..c6dde62 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -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, diff --git a/tests/memory-cmd.test.ts b/tests/memory-cmd.test.ts new file mode 100644 index 0000000..9e9c997 --- /dev/null +++ b/tests/memory-cmd.test.ts @@ -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'); + }); +});