Inspired by Copilot CLI: /context now lists each context file with load status and token cost
Copilot CLI 1.0.81-6 shows each user instruction file separately in /instructions. Hermes loaded AGENTS.md/.hermes.md/CLAUDE.md/.cursorrules/ SOUL.md through a priority ladder but gave the user no visibility into WHICH files were discovered, which one won, which were shadowed, or how much context each costs — the /context 'rules' category was one opaque number. - agent/prompt_builder.py: list_context_file_sources() — read-only manifest mirroring build_context_files_prompt discovery (priority ladder, AGENTS.md directory chain with AGENTS.override.md precedence, cwd-only CLAUDE.md/.cursorrules, SOUL.md from profile home) with per-file chars, est_tokens, and loaded/truncated/shadowed status - cli.py /context: 'Context files' section rendering the manifest with status glyphs and shadowing/truncation notes; zero prompt/cache impact - docs: reference/slash-commands.md /context row - tests/agent/test_context_file_sources.py: 11 tests incl. E2E parity with build_context_files_prompt shadowing
This commit is contained in:
112
tests/agent/test_context_file_sources.py
Normal file
112
tests/agent/test_context_file_sources.py
Normal file
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Per-file context source manifest (`list_context_file_sources`).
|
||||
|
||||
Read-only mirror of ``build_context_files_prompt`` discovery: one entry per
|
||||
candidate context/instruction file with size, token estimate, and whether it
|
||||
was loaded, truncated, or shadowed by a higher-priority context type.
|
||||
|
||||
Inspired by: GitHub Copilot CLI 1.0.81 — "Show each user instruction file
|
||||
separately in /instructions".
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from agent.prompt_builder import list_context_file_sources
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def project(tmp_path):
|
||||
(tmp_path / ".git").mkdir()
|
||||
return tmp_path
|
||||
|
||||
|
||||
def _by_label(sources):
|
||||
return {s["label"]: s for s in sources}
|
||||
|
||||
|
||||
class TestDiscovery:
|
||||
def test_agents_md_loaded(self, project):
|
||||
(project / "AGENTS.md").write_text("# rules\n" * 10)
|
||||
sources = list_context_file_sources(cwd=str(project))
|
||||
entry = _by_label(sources)["AGENTS.md"]
|
||||
assert entry["loaded"] is True
|
||||
assert entry["status"] == "loaded"
|
||||
assert entry["chars"] > 0
|
||||
assert entry["est_tokens"] == (entry["chars"] + 3) // 4
|
||||
|
||||
def test_empty_project_returns_empty(self, tmp_path, tmp_path_factory):
|
||||
(tmp_path / ".git").mkdir()
|
||||
empty_home = tmp_path_factory.mktemp("empty_home")
|
||||
assert (
|
||||
list_context_file_sources(cwd=str(tmp_path), home_override=empty_home)
|
||||
== []
|
||||
)
|
||||
|
||||
def test_priority_shadowing_hermes_md_over_agents_md(self, project):
|
||||
(project / ".hermes.md").write_text("hermes rules")
|
||||
(project / "AGENTS.md").write_text("agents rules")
|
||||
entries = _by_label(list_context_file_sources(cwd=str(project)))
|
||||
assert entries[".hermes.md"]["status"] == "loaded"
|
||||
assert entries["AGENTS.md"]["status"] == "shadowed"
|
||||
assert entries["AGENTS.md"]["loaded"] is False
|
||||
|
||||
def test_claude_md_shadowed_by_agents_md(self, project):
|
||||
(project / "AGENTS.md").write_text("agents rules")
|
||||
(project / "CLAUDE.md").write_text("claude rules")
|
||||
entries = _by_label(list_context_file_sources(cwd=str(project)))
|
||||
assert entries["AGENTS.md"]["status"] == "loaded"
|
||||
assert entries["CLAUDE.md"]["status"] == "shadowed"
|
||||
|
||||
def test_cursorrules_listed(self, project):
|
||||
(project / ".cursorrules").write_text("cursor rules")
|
||||
rules_dir = project / ".cursor" / "rules"
|
||||
rules_dir.mkdir(parents=True)
|
||||
(rules_dir / "a.mdc").write_text("rule a")
|
||||
entries = _by_label(list_context_file_sources(cwd=str(project)))
|
||||
assert entries[".cursorrules"]["status"] == "loaded"
|
||||
assert entries[".cursor/rules/a.mdc"]["status"] == "loaded"
|
||||
|
||||
def test_agents_override_wins_per_directory(self, project):
|
||||
(project / "AGENTS.md").write_text("committed")
|
||||
(project / "AGENTS.override.md").write_text("personal override")
|
||||
labels = [s["label"] for s in list_context_file_sources(cwd=str(project))]
|
||||
assert "AGENTS.override.md" in labels
|
||||
assert "AGENTS.md" not in labels # first name wins per directory
|
||||
|
||||
def test_directory_chain_lists_both_agents_files(self, project):
|
||||
(project / "AGENTS.md").write_text("root rules")
|
||||
sub = project / "pkg"
|
||||
sub.mkdir()
|
||||
(sub / "AGENTS.md").write_text("pkg rules")
|
||||
sources = list_context_file_sources(cwd=str(sub))
|
||||
agents_entries = [s for s in sources if "AGENTS.md" in s["label"]]
|
||||
assert len(agents_entries) == 2
|
||||
assert all(s["loaded"] for s in agents_entries)
|
||||
|
||||
def test_truncated_status_when_over_cap(self, project, monkeypatch):
|
||||
import agent.prompt_builder as pb
|
||||
|
||||
monkeypatch.setattr(pb, "_get_context_file_max_chars", lambda *_a: 10)
|
||||
(project / "AGENTS.md").write_text("x" * 100)
|
||||
entries = _by_label(list_context_file_sources(cwd=str(project)))
|
||||
assert entries["AGENTS.md"]["status"] == "truncated"
|
||||
assert entries["AGENTS.md"]["loaded"] is True
|
||||
|
||||
def test_soul_md_from_home_override(self, project, tmp_path_factory):
|
||||
home = tmp_path_factory.mktemp("hermes_home")
|
||||
(home / "SOUL.md").write_text("identity")
|
||||
entries = _by_label(
|
||||
list_context_file_sources(cwd=str(project), home_override=home)
|
||||
)
|
||||
assert entries["SOUL.md"]["status"] == "loaded"
|
||||
|
||||
def test_read_only_no_side_effects(self, project):
|
||||
(project / "AGENTS.md").write_text("rules")
|
||||
before = (project / "AGENTS.md").read_text()
|
||||
list_context_file_sources(cwd=str(project))
|
||||
assert (project / "AGENTS.md").read_text() == before
|
||||
|
||||
def test_accepts_path_object(self, project):
|
||||
(project / "AGENTS.md").write_text("rules")
|
||||
sources = list_context_file_sources(cwd=project)
|
||||
assert _by_label(sources)["AGENTS.md"]["loaded"] is True
|
||||
@@ -63,7 +63,7 @@ Type `/` in the CLI to open the autocomplete menu. Built-in commands are case-in
|
||||
| `/egress [status]` | Show Docker egress proxy status — enabled/configured/running state, credential source, token mappings, uncovered providers, and next remediation step. Works in CLI, TUI, Desktop chat, and messaging gateway. |
|
||||
| `/redraw` | Force a full UI repaint (recovers from terminal drift after tmux resize, mouse selection artifacts, etc.) |
|
||||
| `/status` | Show session info — model, provider, profile, session ID, working directory, title, created/updated timestamps, token totals, agent-running state — followed by a local **Session recap** block (recent user/assistant turn counts, tool result count, top tools used, last few files touched, the latest user prompt, and the latest assistant reply). The recap is computed locally from the in-memory conversation; no LLM call, no prompt-cache impact. |
|
||||
| `/context [all]` (alias: `/ctx`) | Visual context-window breakdown. On the CLI/TUI: a 5×20 glyph block grid (each cell ≈ 1% of the model window) plus an estimated per-category table — system prompt, tool definitions, rules, skills index, MCP, subagents, memory, conversation — versus free space. On messaging platforms: a usage gauge with auto-compression threshold/headroom, compression stats, cumulative throughput, and the same category table in plain text. `/context all` appends per-skill and per-toolset cost listings (index cost vs SKILL.md load cost; schema tokens per toolset). Read-only and computed locally — no LLM call, no prompt-cache impact. |
|
||||
| `/context [all]` (alias: `/ctx`) | Visual context-window breakdown. On the CLI/TUI: a 5×20 glyph block grid (each cell ≈ 1% of the model window) plus an estimated per-category table — system prompt, tool definitions, rules, skills index, MCP, subagents, memory, conversation — versus free space, followed by a per-file **Context files** listing (AGENTS.md chain, .hermes.md, CLAUDE.md, .cursorrules, SOUL.md) with each file's token estimate and whether it was loaded, truncated over `context_file_max_chars`, or shadowed by a higher-priority context type. On messaging platforms: a usage gauge with auto-compression threshold/headroom, compression stats, cumulative throughput, and the same category table in plain text. `/context all` appends per-skill and per-toolset cost listings (index cost vs SKILL.md load cost; schema tokens per toolset). Read-only and computed locally — no LLM call, no prompt-cache impact. |
|
||||
| `/agents` (alias: `/tasks`) | Show active agents and running tasks across the current session. |
|
||||
| `/bg <prompt>` | Run a prompt in a separate background session. The agent processes your prompt independently — your current session stays free for other work. Results appear as a panel when the task finishes. See [CLI Background Sessions](/user-guide/cli#background-sessions). |
|
||||
| `/btw <question>` | Ask a quick side question **about the current conversation** without interrupting it. A one-shot auxiliary LLM call answers from a read-only snapshot of the transcript — the live session's history and prompt cache are untouched, and the current turn keeps running. For independent work with a fresh context, use `/bg`. |
|
||||
|
||||
Reference in New Issue
Block a user