/** * 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'); }); });