Files
llmx/codex-cli/tests/config.test.tsx
Tomas Cupr 295079cf33 feat: add command history persistence (#152)
This PR adds a command history persistence feature to Codex CLI that:

1. **Stores command history**: Commands are saved to
`~/.codex/history.json` and persist between CLI sessions.
2. **Navigates history**: Users can use the up/down arrow keys to
navigate through command history, similar to a traditional shell.
3. **Filters sensitive data**: Built-in regex patterns prevent commands
containing API keys, passwords, or tokens from being saved.
4. **Configurable**: Added configuration options for history size,
enabling/disabling history, and custom regex patterns for sensitive
content.
5. **New command**: Added `/clearhistory` command to clear command
history.

  ## Code Changes

- Added `src/utils/storage/command-history.ts` with functions for
history management
  - Extended config system to support history settings
  - Updated terminal input components to use persistent history
  - Added help text for the new `/clearhistory` command
  - Added CLAUDE.md file for guidance when working with the codebase

  ## Testing

  - All tests are passing
- Core functionality works with both input components (standard and
multiline)
- History navigation behaves correctly at line boundaries with the
multiline editor
2025-04-17 12:41:54 -07:00

109 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type * as fsType from "fs";
import { loadConfig, saveConfig } from "../src/utils/config.js"; // parent import first
import { tmpdir } from "os";
import { join } from "path";
import { test, expect, beforeEach, afterEach, vi } from "vitest";
// Inmemory FS store
let memfs: Record<string, string> = {};
// Mock out the parts of "fs" that our config module uses:
vi.mock("fs", async () => {
// now `real` is the actual fs module
const real = (await vi.importActual("fs")) as typeof fsType;
return {
...real,
existsSync: (path: string) => memfs[path] !== undefined,
readFileSync: (path: string) => {
if (memfs[path] === undefined) {
throw new Error("ENOENT");
}
return memfs[path];
},
writeFileSync: (path: string, data: string) => {
memfs[path] = data;
},
mkdirSync: () => {
// noop in inmemory store
},
rmSync: (path: string) => {
// recursively delete any key under this prefix
const prefix = path.endsWith("/") ? path : path + "/";
for (const key of Object.keys(memfs)) {
if (key === path || key.startsWith(prefix)) {
delete memfs[key];
}
}
},
};
});
let testDir: string;
let testConfigPath: string;
let testInstructionsPath: string;
beforeEach(() => {
memfs = {}; // reset inmemory store
testDir = tmpdir(); // use the OS temp dir as our "cwd"
testConfigPath = join(testDir, "config.json");
testInstructionsPath = join(testDir, "instructions.md");
});
afterEach(() => {
memfs = {};
});
test("loads default config if files don't exist", () => {
const config = loadConfig(testConfigPath, testInstructionsPath, {
disableProjectDoc: true,
});
// Keep the test focused on just checking that default model and instructions are loaded
// so we need to make sure we check just these properties
expect(config.model).toBe("o4-mini");
expect(config.instructions).toBe("");
});
test("saves and loads config correctly", () => {
const testConfig = {
model: "test-model",
instructions: "test instructions",
};
saveConfig(testConfig, testConfigPath, testInstructionsPath);
// Our inmemory fs should now contain those keys:
expect(memfs[testConfigPath]).toContain(`"model": "test-model"`);
expect(memfs[testInstructionsPath]).toBe("test instructions");
const loadedConfig = loadConfig(testConfigPath, testInstructionsPath, {
disableProjectDoc: true,
});
// Check just the specified properties that were saved
expect(loadedConfig.model).toBe(testConfig.model);
expect(loadedConfig.instructions).toBe(testConfig.instructions);
});
test("loads user instructions + project doc when codex.md is present", () => {
// 1) seed memfs: a config JSON, an instructions.md, and a codex.md in the cwd
const userInstr = "here are user instructions";
const projectDoc = "# Project Title\n\nSome projectspecific doc";
// first, make config so loadConfig will see storedConfig
memfs[testConfigPath] = JSON.stringify({ model: "mymodel" }, null, 2);
// then user instructions:
memfs[testInstructionsPath] = userInstr;
// and now our fake codex.md in the cwd:
const codexPath = join(testDir, "codex.md");
memfs[codexPath] = projectDoc;
// 2) loadConfig without disabling projectdoc, but with cwd=testDir
const cfg = loadConfig(testConfigPath, testInstructionsPath, {
cwd: testDir,
});
// 3) assert we got both pieces concatenated
expect(cfg.model).toBe("mymodel");
expect(cfg.instructions).toBe(
userInstr + "\n\n--- project-doc ---\n\n" + projectDoc,
);
});