mirror of
https://github.com/instructkr/claw-code.git
synced 2026-06-04 13:37:09 +08:00
fix: load common instruction files and typed unknown commands
This commit is contained in:
@@ -240,8 +240,10 @@ fn discover_instruction_files(cwd: &Path) -> std::io::Result<Vec<ContextFile>> {
|
||||
for dir in directories {
|
||||
for candidate in [
|
||||
dir.join("CLAUDE.md"),
|
||||
dir.join("AGENTS.md"),
|
||||
dir.join("CLAUDE.local.md"),
|
||||
dir.join(".claw").join("CLAUDE.md"),
|
||||
dir.join(".claude").join("CLAUDE.md"),
|
||||
dir.join(".claw").join("instructions.md"),
|
||||
] {
|
||||
push_context_file(&mut files, candidate)?;
|
||||
@@ -636,6 +638,63 @@ mod tests {
|
||||
fs::remove_dir_all(root).expect("cleanup temp dir");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discovers_agents_markdown_instruction_file() {
|
||||
let root = temp_dir();
|
||||
fs::create_dir_all(&root).expect("root dir");
|
||||
fs::write(root.join("AGENTS.md"), "agents-only instructions").expect("write AGENTS.md");
|
||||
|
||||
let context = ProjectContext::discover(&root, "2026-03-31").expect("context should load");
|
||||
|
||||
assert_eq!(context.instruction_files.len(), 1);
|
||||
assert!(context.instruction_files[0].path.ends_with("AGENTS.md"));
|
||||
assert!(render_instruction_files(&context.instruction_files)
|
||||
.contains("agents-only instructions"));
|
||||
fs::remove_dir_all(root).expect("cleanup temp dir");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discovers_scoped_dot_claude_claude_markdown_instruction_file() {
|
||||
let root = temp_dir();
|
||||
fs::create_dir_all(root.join(".claude")).expect("dot claude dir");
|
||||
fs::write(
|
||||
root.join(".claude").join("CLAUDE.md"),
|
||||
"dot-claude-only instructions",
|
||||
)
|
||||
.expect("write .claude/CLAUDE.md");
|
||||
|
||||
let context = ProjectContext::discover(&root, "2026-03-31").expect("context should load");
|
||||
|
||||
assert_eq!(context.instruction_files.len(), 1);
|
||||
assert!(context.instruction_files[0]
|
||||
.path
|
||||
.ends_with(".claude/CLAUDE.md"));
|
||||
assert!(render_instruction_files(&context.instruction_files)
|
||||
.contains("dot-claude-only instructions"));
|
||||
fs::remove_dir_all(root).expect("cleanup temp dir");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discovers_claude_agents_and_dot_claude_instruction_files_together() {
|
||||
let root = temp_dir();
|
||||
fs::create_dir_all(root.join(".claude")).expect("dot claude dir");
|
||||
fs::write(root.join("CLAUDE.md"), "claude instructions").expect("write CLAUDE.md");
|
||||
fs::write(root.join("AGENTS.md"), "agents instructions").expect("write AGENTS.md");
|
||||
fs::write(
|
||||
root.join(".claude").join("CLAUDE.md"),
|
||||
"dot claude instructions",
|
||||
)
|
||||
.expect("write .claude/CLAUDE.md");
|
||||
|
||||
let context = ProjectContext::discover(&root, "2026-03-31").expect("context should load");
|
||||
let rendered = render_instruction_files(&context.instruction_files);
|
||||
|
||||
assert!(rendered.contains("claude instructions"));
|
||||
assert!(rendered.contains("agents instructions"));
|
||||
assert!(rendered.contains("dot claude instructions"));
|
||||
fs::remove_dir_all(root).expect("cleanup temp dir");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dedupes_identical_instruction_content_across_scopes() {
|
||||
let root = temp_dir();
|
||||
|
||||
@@ -1381,13 +1381,14 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
||||
allow_broad_cwd,
|
||||
),
|
||||
other => {
|
||||
if rest.len() == 1 && looks_like_subcommand_typo(other) {
|
||||
// #825: always emit a command_not_found error for
|
||||
// single-word all-alpha/dash tokens that don't match any
|
||||
// known subcommand — with or without close suggestions.
|
||||
// Multi-word cases fall through to CliAction::Prompt so
|
||||
// natural language prompts like `claw explain this` work.
|
||||
// (#826 documents the multi-word gap as a known limitation.)
|
||||
if looks_like_subcommand_typo(other)
|
||||
&& (rest.len() == 1 || output_format == CliOutputFormat::Json)
|
||||
{
|
||||
// #825/#826: emit command_not_found before provider startup for
|
||||
// command-shaped tokens that do not match known subcommands.
|
||||
// Text-mode multi-word prompt shorthand remains available, but
|
||||
// JSON-mode automation must not turn an unknown command into a
|
||||
// credential-gated prompt request.
|
||||
let mut message = format!("command_not_found: unknown subcommand: {other}.");
|
||||
if let Some(suggestions) = suggest_similar_subcommand(other) {
|
||||
if let Some(line) = render_suggestion_line("Did you mean", &suggestions) {
|
||||
|
||||
@@ -3972,31 +3972,30 @@ fn unknown_subcommand_typo_with_suggestions_json_emits_command_not_found() {
|
||||
assert!(stderr.is_empty(), "typo JSON must have empty stderr (#825)");
|
||||
}
|
||||
|
||||
// #826: multi-word unknown subcommand is a known gap — falls through to
|
||||
// CliAction::Prompt (natural language prompt passthrough like `claw explain this`).
|
||||
// Single-word typos (#825) are caught; multi-word is documented as backlog.
|
||||
// This test documents the current behaviour (not the desired fix).
|
||||
// #826: JSON-mode multi-word unknown subcommands must not fall through to
|
||||
// CliAction::Prompt and hit the provider credential gate.
|
||||
#[test]
|
||||
fn multi_word_unknown_subcommand_falls_through_to_prompt_826() {
|
||||
let root = unique_temp_dir("multi-word-gap-826");
|
||||
fn multi_word_unknown_subcommand_json_emits_command_not_found_826() {
|
||||
let root = unique_temp_dir("multi-word-command-not-found-826");
|
||||
std::fs::create_dir_all(&root).expect("create temp dir");
|
||||
// "foobar baz" has no fuzzy suggestion → falls through to Prompt path
|
||||
// (hits missing_credentials since no API key is set, rc=1)
|
||||
let output = run_claw(&root, &["--output-format", "json", "foobar", "baz"], &[]);
|
||||
assert_eq!(output.status.code(), Some(1));
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
// Currently emits missing_credentials (fallthrough gap documented in #826)
|
||||
let j: serde_json::Value =
|
||||
serde_json::from_str(stdout.trim()).expect("multi-word fallthrough must emit JSON");
|
||||
serde_json::from_str(stdout.trim()).expect("multi-word unknown subcommand must emit JSON");
|
||||
assert_eq!(
|
||||
j["status"], "error",
|
||||
"multi-word fallthrough must be an error: {j}"
|
||||
j["error_kind"], "command_not_found",
|
||||
"multi-word unknown subcommand must emit command_not_found, not missing_credentials (#826): {j}"
|
||||
);
|
||||
let hint = j["hint"].as_str().unwrap_or_default();
|
||||
assert!(
|
||||
hint.contains("claw prompt") || hint.contains("--help"),
|
||||
"hint should explain prompt/command recovery, got: {hint:?}"
|
||||
);
|
||||
// stderr must be empty regardless (JSON mode)
|
||||
assert!(
|
||||
stderr.is_empty(),
|
||||
"multi-word fallthrough JSON must have empty stderr: {stderr:?}"
|
||||
"multi-word command_not_found JSON must have empty stderr: {stderr:?}"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user