mirror of
https://github.com/openai/codex.git
synced 2026-04-20 20:54:48 +00:00
Compare commits
1 Commits
codex-debu
...
joshka/ind
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a96d233ea5 |
@@ -4,6 +4,7 @@ In the codex-rs folder where the rust code lives:
|
||||
|
||||
- Crate names are prefixed with `codex-`. For example, the `core` folder's crate is named `codex-core`
|
||||
- When using format! and you can inline variables into {}, always do that.
|
||||
- Use `indoc!` for multi-line strings instead of embedding `\n` escapes or using awkwardly-wrapped raw strings.
|
||||
- Install any commands the repo relies on (for example `just`, `rg`, or `cargo-insta`) if they aren't already available before running instructions here.
|
||||
- Never add or modify any code related to `CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR` or `CODEX_SANDBOX_ENV_VAR`.
|
||||
- You operate in a sandbox where `CODEX_SANDBOX_NETWORK_DISABLED=1` will be set whenever you use the `shell` tool. Any existing code that uses `CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR` was authored with this fact in mind. It is often used to early exit out of tests that the author knew you would not be able to run given your sandbox limitations.
|
||||
|
||||
2
codex-rs/Cargo.lock
generated
2
codex-rs/Cargo.lock
generated
@@ -1496,6 +1496,7 @@ name = "codex-git"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"assert_matches",
|
||||
"indoc",
|
||||
"once_cell",
|
||||
"pretty_assertions",
|
||||
"regex",
|
||||
@@ -1764,6 +1765,7 @@ dependencies = [
|
||||
"dirs",
|
||||
"dunce",
|
||||
"image",
|
||||
"indoc",
|
||||
"insta",
|
||||
"itertools 0.14.0",
|
||||
"lazy_static",
|
||||
|
||||
@@ -89,6 +89,8 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
use core_test_support::test_path_buf;
|
||||
use indoc::formatdoc;
|
||||
use indoc::indoc;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn fake_shell() -> Shell {
|
||||
@@ -104,13 +106,13 @@ mod tests {
|
||||
let cwd = test_path_buf("/repo");
|
||||
let context = EnvironmentContext::new(Some(cwd.clone()), fake_shell());
|
||||
|
||||
let expected = format!(
|
||||
r#"<environment_context>
|
||||
<cwd>{cwd}</cwd>
|
||||
<shell>bash</shell>
|
||||
</environment_context>"#,
|
||||
let expected = formatdoc! {"
|
||||
<environment_context>
|
||||
<cwd>{cwd}</cwd>
|
||||
<shell>bash</shell>
|
||||
</environment_context>",
|
||||
cwd = cwd.display(),
|
||||
);
|
||||
};
|
||||
|
||||
assert_eq!(context.serialize_to_xml(), expected);
|
||||
}
|
||||
@@ -119,9 +121,10 @@ mod tests {
|
||||
fn serialize_read_only_environment_context() {
|
||||
let context = EnvironmentContext::new(None, fake_shell());
|
||||
|
||||
let expected = r#"<environment_context>
|
||||
<shell>bash</shell>
|
||||
</environment_context>"#;
|
||||
let expected = indoc! {"
|
||||
<environment_context>
|
||||
<shell>bash</shell>
|
||||
</environment_context>"};
|
||||
|
||||
assert_eq!(context.serialize_to_xml(), expected);
|
||||
}
|
||||
@@ -130,9 +133,10 @@ mod tests {
|
||||
fn serialize_external_sandbox_environment_context() {
|
||||
let context = EnvironmentContext::new(None, fake_shell());
|
||||
|
||||
let expected = r#"<environment_context>
|
||||
<shell>bash</shell>
|
||||
</environment_context>"#;
|
||||
let expected = indoc! {"
|
||||
<environment_context>
|
||||
<shell>bash</shell>
|
||||
</environment_context>"};
|
||||
|
||||
assert_eq!(context.serialize_to_xml(), expected);
|
||||
}
|
||||
@@ -141,9 +145,10 @@ mod tests {
|
||||
fn serialize_external_sandbox_with_restricted_network_environment_context() {
|
||||
let context = EnvironmentContext::new(None, fake_shell());
|
||||
|
||||
let expected = r#"<environment_context>
|
||||
<shell>bash</shell>
|
||||
</environment_context>"#;
|
||||
let expected = indoc! {"
|
||||
<environment_context>
|
||||
<shell>bash</shell>
|
||||
</environment_context>"};
|
||||
|
||||
assert_eq!(context.serialize_to_xml(), expected);
|
||||
}
|
||||
@@ -152,9 +157,10 @@ mod tests {
|
||||
fn serialize_full_access_environment_context() {
|
||||
let context = EnvironmentContext::new(None, fake_shell());
|
||||
|
||||
let expected = r#"<environment_context>
|
||||
<shell>bash</shell>
|
||||
</environment_context>"#;
|
||||
let expected = indoc! {"
|
||||
<environment_context>
|
||||
<shell>bash</shell>
|
||||
</environment_context>"};
|
||||
|
||||
assert_eq!(context.serialize_to_xml(), expected);
|
||||
}
|
||||
|
||||
@@ -320,13 +320,13 @@ fn gpt_52_codex_upgrade() -> ModelUpgrade {
|
||||
.to_string(),
|
||||
),
|
||||
migration_markdown: Some(
|
||||
indoc! {r#"
|
||||
**Codex just got an upgrade. Introducing {model_to}.**
|
||||
indoc! {"
|
||||
**Codex just got an upgrade. Introducing {model_to}.**
|
||||
|
||||
Codex is now powered by gpt-5.2-codex, our latest frontier agentic coding model. It is smarter and faster than its predecessors and capable of long-running project-scale work. Learn more about {model_to} at https://openai.com/index/introducing-gpt-5-2-codex
|
||||
Codex is now powered by gpt-5.2-codex, our latest frontier agentic coding model. It is smarter and faster than its predecessors and capable of long-running project-scale work. Learn more about {model_to} at https://openai.com/index/introducing-gpt-5-2-codex
|
||||
|
||||
You can continue using {model_from} if you prefer.
|
||||
"#}
|
||||
You can continue using {model_from} if you prefer.
|
||||
"}
|
||||
.to_string(),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -237,6 +237,8 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::config::ConfigBuilder;
|
||||
use crate::skills::load_skills;
|
||||
use indoc::formatdoc;
|
||||
use indoc::indoc;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::TempDir;
|
||||
@@ -516,10 +518,32 @@ mod tests {
|
||||
)
|
||||
.unwrap_or_else(|_| cfg.codex_home.join("skills/pdf-processing/SKILL.md"));
|
||||
let expected_path_str = expected_path.to_string_lossy().replace('\\', "/");
|
||||
let usage_rules = "- Discovery: The list above is the skills available in this session (name + description + file path). Skill bodies live on disk at the listed paths.\n- Trigger rules: If the user names a skill (with `$SkillName` or plain text) OR the task clearly matches a skill's description shown above, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned.\n- Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback.\n- How to use a skill (progressive disclosure):\n 1) After deciding to use a skill, open its `SKILL.md`. Read only enough to follow the workflow.\n 2) If `SKILL.md` points to extra folders such as `references/`, load only the specific files needed for the request; don't bulk-load everything.\n 3) If `scripts/` exist, prefer running or patching them instead of retyping large code blocks.\n 4) If `assets/` or templates exist, reuse them instead of recreating from scratch.\n- Coordination and sequencing:\n - If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.\n - Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.\n- Context hygiene:\n - Keep context small: summarize long sections instead of pasting them; only load extra files when needed.\n - Avoid deep reference-chasing: prefer opening only files directly linked from `SKILL.md` unless you're blocked.\n - When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.\n- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue.";
|
||||
let expected = format!(
|
||||
"base doc\n\n## Skills\nA skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list of skills that can be used. Each entry includes a name, description, and file path so you can open the source for full instructions when using a specific skill.\n### Available skills\n- pdf-processing: extract from pdfs (file: {expected_path_str})\n### How to use skills\n{usage_rules}"
|
||||
);
|
||||
let usage_rules = indoc! {"
|
||||
- Discovery: The list above is the skills available in this session (name + description + file path). Skill bodies live on disk at the listed paths.
|
||||
- Trigger rules: If the user names a skill (with `$SkillName` or plain text) OR the task clearly matches a skill's description shown above, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned.
|
||||
- Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback.
|
||||
- How to use a skill (progressive disclosure):
|
||||
1) After deciding to use a skill, open its `SKILL.md`. Read only enough to follow the workflow.
|
||||
2) If `SKILL.md` points to extra folders such as `references/`, load only the specific files needed for the request; don't bulk-load everything.
|
||||
3) If `scripts/` exist, prefer running or patching them instead of retyping large code blocks.
|
||||
4) If `assets/` or templates exist, reuse them instead of recreating from scratch.
|
||||
- Coordination and sequencing:
|
||||
- If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.
|
||||
- Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.
|
||||
- Context hygiene:
|
||||
- Keep context small: summarize long sections instead of pasting them; only load extra files when needed.
|
||||
- Avoid deep reference-chasing: prefer opening only files directly linked from `SKILL.md` unless you're blocked.
|
||||
- When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.
|
||||
- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue."};
|
||||
let expected = formatdoc! {"
|
||||
base doc
|
||||
|
||||
## Skills
|
||||
A skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list of skills that can be used. Each entry includes a name, description, and file path so you can open the source for full instructions when using a specific skill.
|
||||
### Available skills
|
||||
- pdf-processing: extract from pdfs (file: {expected_path_str})
|
||||
### How to use skills
|
||||
{usage_rules}"};
|
||||
assert_eq!(res, expected);
|
||||
}
|
||||
|
||||
@@ -540,17 +564,44 @@ mod tests {
|
||||
dunce::canonicalize(cfg.codex_home.join("skills/linting/SKILL.md").as_path())
|
||||
.unwrap_or_else(|_| cfg.codex_home.join("skills/linting/SKILL.md"));
|
||||
let expected_path_str = expected_path.to_string_lossy().replace('\\', "/");
|
||||
let usage_rules = "- Discovery: The list above is the skills available in this session (name + description + file path). Skill bodies live on disk at the listed paths.\n- Trigger rules: If the user names a skill (with `$SkillName` or plain text) OR the task clearly matches a skill's description shown above, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned.\n- Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback.\n- How to use a skill (progressive disclosure):\n 1) After deciding to use a skill, open its `SKILL.md`. Read only enough to follow the workflow.\n 2) If `SKILL.md` points to extra folders such as `references/`, load only the specific files needed for the request; don't bulk-load everything.\n 3) If `scripts/` exist, prefer running or patching them instead of retyping large code blocks.\n 4) If `assets/` or templates exist, reuse them instead of recreating from scratch.\n- Coordination and sequencing:\n - If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.\n - Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.\n- Context hygiene:\n - Keep context small: summarize long sections instead of pasting them; only load extra files when needed.\n - Avoid deep reference-chasing: prefer opening only files directly linked from `SKILL.md` unless you're blocked.\n - When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.\n- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue.";
|
||||
let expected = format!(
|
||||
"## Skills\nA skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list of skills that can be used. Each entry includes a name, description, and file path so you can open the source for full instructions when using a specific skill.\n### Available skills\n- linting: run clippy (file: {expected_path_str})\n### How to use skills\n{usage_rules}"
|
||||
);
|
||||
let usage_rules = indoc! {"
|
||||
- Discovery: The list above is the skills available in this session (name + description + file path). Skill bodies live on disk at the listed paths.
|
||||
- Trigger rules: If the user names a skill (with `$SkillName` or plain text) OR the task clearly matches a skill's description shown above, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned.
|
||||
- Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback.
|
||||
- How to use a skill (progressive disclosure):
|
||||
1) After deciding to use a skill, open its `SKILL.md`. Read only enough to follow the workflow.
|
||||
2) If `SKILL.md` points to extra folders such as `references/`, load only the specific files needed for the request; don't bulk-load everything.
|
||||
3) If `scripts/` exist, prefer running or patching them instead of retyping large code blocks.
|
||||
4) If `assets/` or templates exist, reuse them instead of recreating from scratch.
|
||||
- Coordination and sequencing:
|
||||
- If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.
|
||||
- Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.
|
||||
- Context hygiene:
|
||||
- Keep context small: summarize long sections instead of pasting them; only load extra files when needed.
|
||||
- Avoid deep reference-chasing: prefer opening only files directly linked from `SKILL.md` unless you're blocked.
|
||||
- When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.
|
||||
- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue."};
|
||||
let expected = formatdoc! {"
|
||||
## Skills
|
||||
A skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list of skills that can be used. Each entry includes a name, description, and file path so you can open the source for full instructions when using a specific skill.
|
||||
### Available skills
|
||||
- linting: run clippy (file: {expected_path_str})
|
||||
### How to use skills
|
||||
{usage_rules}"};
|
||||
assert_eq!(res, expected);
|
||||
}
|
||||
|
||||
fn create_skill(codex_home: PathBuf, name: &str, description: &str) {
|
||||
let skill_dir = codex_home.join(format!("skills/{name}"));
|
||||
fs::create_dir_all(&skill_dir).unwrap();
|
||||
let content = format!("---\nname: {name}\ndescription: {description}\n---\n\n# Body\n");
|
||||
let content = formatdoc! {"
|
||||
---
|
||||
name: {name}
|
||||
description: {description}
|
||||
---
|
||||
|
||||
# Body
|
||||
"};
|
||||
fs::write(skill_dir.join("SKILL.md"), content).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +118,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::config::ConfigBuilder;
|
||||
use crate::config::ConfigOverrides;
|
||||
use indoc::formatdoc;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::fs;
|
||||
use tempfile::TempDir;
|
||||
@@ -125,7 +126,14 @@ mod tests {
|
||||
fn write_user_skill(codex_home: &TempDir, dir: &str, name: &str, description: &str) {
|
||||
let skill_dir = codex_home.path().join("skills").join(dir);
|
||||
fs::create_dir_all(&skill_dir).unwrap();
|
||||
let content = format!("---\nname: {name}\ndescription: {description}\n---\n\n# Body\n");
|
||||
let content = formatdoc! {"
|
||||
---
|
||||
name: {name}
|
||||
description: {description}
|
||||
---
|
||||
|
||||
# Body
|
||||
"};
|
||||
fs::write(skill_dir.join("SKILL.md"), content).unwrap();
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ use async_trait::async_trait;
|
||||
use codex_apply_patch::ApplyPatchAction;
|
||||
use codex_apply_patch::ApplyPatchFileChange;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use indoc::indoc;
|
||||
|
||||
pub struct ApplyPatchHandler;
|
||||
|
||||
@@ -288,75 +289,74 @@ pub(crate) fn create_apply_patch_json_tool() -> ToolSpec {
|
||||
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: "apply_patch".to_string(),
|
||||
description: r#"Use the `apply_patch` tool to edit files.
|
||||
Your patch language is a stripped‑down, file‑oriented diff format designed to be easy to parse and safe to apply. You can think of it as a high‑level envelope:
|
||||
description: indoc! {r#"
|
||||
Use the `apply_patch` tool to edit files.
|
||||
Your patch language is a stripped‑down, file‑oriented diff format designed to be easy to parse and safe to apply. You can think of it as a high‑level envelope:
|
||||
|
||||
*** Begin Patch
|
||||
[ one or more file sections ]
|
||||
*** End Patch
|
||||
*** Begin Patch
|
||||
[ one or more file sections ]
|
||||
*** End Patch
|
||||
|
||||
Within that envelope, you get a sequence of file operations.
|
||||
You MUST include a header to specify the action you are taking.
|
||||
Each operation starts with one of three headers:
|
||||
Within that envelope, you get a sequence of file operations.
|
||||
You MUST include a header to specify the action you are taking.
|
||||
Each operation starts with one of three headers:
|
||||
|
||||
*** Add File: <path> - create a new file. Every following line is a + line (the initial contents).
|
||||
*** Delete File: <path> - remove an existing file. Nothing follows.
|
||||
*** Update File: <path> - patch an existing file in place (optionally with a rename).
|
||||
*** Add File: <path> - create a new file. Every following line is a + line (the initial contents).
|
||||
*** Delete File: <path> - remove an existing file. Nothing follows.
|
||||
*** Update File: <path> - patch an existing file in place (optionally with a rename).
|
||||
|
||||
May be immediately followed by *** Move to: <new path> if you want to rename the file.
|
||||
Then one or more “hunks”, each introduced by @@ (optionally followed by a hunk header).
|
||||
Within a hunk each line starts with:
|
||||
May be immediately followed by *** Move to: <new path> if you want to rename the file.
|
||||
Then one or more “hunks”, each introduced by @@ (optionally followed by a hunk header).
|
||||
Within a hunk each line starts with:
|
||||
|
||||
For instructions on [context_before] and [context_after]:
|
||||
- By default, show 3 lines of code immediately above and 3 lines immediately below each change. If a change is within 3 lines of a previous change, do NOT duplicate the first change’s [context_after] lines in the second change’s [context_before] lines.
|
||||
- If 3 lines of context is insufficient to uniquely identify the snippet of code within the file, use the @@ operator to indicate the class or function to which the snippet belongs. For instance, we might have:
|
||||
@@ class BaseClass
|
||||
[3 lines of pre-context]
|
||||
- [old_code]
|
||||
+ [new_code]
|
||||
[3 lines of post-context]
|
||||
For instructions on [context_before] and [context_after]:
|
||||
- By default, show 3 lines of code immediately above and 3 lines immediately below each change. If a change is within 3 lines of a previous change, do NOT duplicate the first change’s [context_after] lines in the second change’s [context_before] lines.
|
||||
- If 3 lines of context is insufficient to uniquely identify the snippet of code within the file, use the @@ operator to indicate the class or function to which the snippet belongs. For instance, we might have:
|
||||
@@ class BaseClass
|
||||
[3 lines of pre-context]
|
||||
- [old_code]
|
||||
+ [new_code]
|
||||
[3 lines of post-context]
|
||||
|
||||
- If a code block is repeated so many times in a class or function such that even a single `@@` statement and 3 lines of context cannot uniquely identify the snippet of code, you can use multiple `@@` statements to jump to the right context. For instance:
|
||||
- If a code block is repeated so many times in a class or function such that even a single `@@` statement and 3 lines of context cannot uniquely identify the snippet of code, you can use multiple `@@` statements to jump to the right context. For instance:
|
||||
|
||||
@@ class BaseClass
|
||||
@@ def method():
|
||||
[3 lines of pre-context]
|
||||
- [old_code]
|
||||
+ [new_code]
|
||||
[3 lines of post-context]
|
||||
@@ class BaseClass
|
||||
@@ def method():
|
||||
[3 lines of pre-context]
|
||||
- [old_code]
|
||||
+ [new_code]
|
||||
[3 lines of post-context]
|
||||
|
||||
The full grammar definition is below:
|
||||
Patch := Begin { FileOp } End
|
||||
Begin := "*** Begin Patch" NEWLINE
|
||||
End := "*** End Patch" NEWLINE
|
||||
FileOp := AddFile | DeleteFile | UpdateFile
|
||||
AddFile := "*** Add File: " path NEWLINE { "+" line NEWLINE }
|
||||
DeleteFile := "*** Delete File: " path NEWLINE
|
||||
UpdateFile := "*** Update File: " path NEWLINE [ MoveTo ] { Hunk }
|
||||
MoveTo := "*** Move to: " newPath NEWLINE
|
||||
Hunk := "@@" [ header ] NEWLINE { HunkLine } [ "*** End of File" NEWLINE ]
|
||||
HunkLine := (" " | "-" | "+") text NEWLINE
|
||||
The full grammar definition is below:
|
||||
Patch := Begin { FileOp } End
|
||||
Begin := "*** Begin Patch" NEWLINE
|
||||
End := "*** End Patch" NEWLINE
|
||||
FileOp := AddFile | DeleteFile | UpdateFile
|
||||
AddFile := "*** Add File: " path NEWLINE { "+" line NEWLINE }
|
||||
DeleteFile := "*** Delete File: " path NEWLINE
|
||||
UpdateFile := "*** Update File: " path NEWLINE [ MoveTo ] { Hunk }
|
||||
MoveTo := "*** Move to: " newPath NEWLINE
|
||||
Hunk := "@@" [ header ] NEWLINE { HunkLine } [ "*** End of File" NEWLINE ]
|
||||
HunkLine := (" " | "-" | "+") text NEWLINE
|
||||
|
||||
A full patch can combine several operations:
|
||||
*** Begin Patch
|
||||
*** Add File: hello.txt
|
||||
+Hello world
|
||||
*** Update File: src/app.py
|
||||
*** Move to: src/main.py
|
||||
@@ def greet():
|
||||
-print("Hi")
|
||||
+print("Hello, world!")
|
||||
*** Delete File: obsolete.txt
|
||||
*** End Patch
|
||||
|
||||
*** Begin Patch
|
||||
*** Add File: hello.txt
|
||||
+Hello world
|
||||
*** Update File: src/app.py
|
||||
*** Move to: src/main.py
|
||||
@@ def greet():
|
||||
-print("Hi")
|
||||
+print("Hello, world!")
|
||||
*** Delete File: obsolete.txt
|
||||
*** End Patch
|
||||
It is important to remember:
|
||||
|
||||
It is important to remember:
|
||||
|
||||
- You must include a header with your intended action (Add/Delete/Update)
|
||||
- You must prefix new lines with `+` even when creating a new file
|
||||
- File references can only be relative, NEVER ABSOLUTE.
|
||||
"#
|
||||
.to_string(),
|
||||
- You must include a header with your intended action (Add/Delete/Update)
|
||||
- You must prefix new lines with `+` even when creating a new file
|
||||
- File references can only be relative, NEVER ABSOLUTE.
|
||||
"#}
|
||||
.to_string(),
|
||||
strict: false,
|
||||
parameters: JsonSchema::Object {
|
||||
properties,
|
||||
@@ -370,6 +370,7 @@ It is important to remember:
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_apply_patch::MaybeApplyPatchVerified;
|
||||
use indoc::indoc;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
|
||||
@@ -380,13 +381,14 @@ mod tests {
|
||||
std::fs::create_dir_all(cwd.join("old")).expect("create old dir");
|
||||
std::fs::create_dir_all(cwd.join("renamed/dir")).expect("create dest dir");
|
||||
std::fs::write(cwd.join("old/name.txt"), "old content\n").expect("write old file");
|
||||
let patch = r#"*** Begin Patch
|
||||
*** Update File: old/name.txt
|
||||
*** Move to: renamed/dir/name.txt
|
||||
@@
|
||||
-old content
|
||||
+new content
|
||||
*** End Patch"#;
|
||||
let patch = indoc! {"
|
||||
*** Begin Patch
|
||||
*** Update File: old/name.txt
|
||||
*** Move to: renamed/dir/name.txt
|
||||
@@
|
||||
-old content
|
||||
+new content
|
||||
*** End Patch"};
|
||||
let argv = vec!["apply_patch".to_string(), patch.to_string()];
|
||||
let action = match codex_apply_patch::maybe_parse_apply_patch_verified(&argv, cwd) {
|
||||
MaybeApplyPatchVerified::Body(action) => action,
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::tools::spec::JsonSchema;
|
||||
use async_trait::async_trait;
|
||||
use codex_protocol::plan_tool::UpdatePlanArgs;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use indoc::indoc;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
@@ -45,10 +46,11 @@ pub static PLAN_TOOL: LazyLock<ToolSpec> = LazyLock::new(|| {
|
||||
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: "update_plan".to_string(),
|
||||
description: r#"Updates the task plan.
|
||||
Provide an optional explanation and a list of plan items, each with a step and status.
|
||||
At most one step can be in_progress at a time.
|
||||
"#
|
||||
description: indoc! {"
|
||||
Updates the task plan.
|
||||
Provide an optional explanation and a list of plan items, each with a step and status.
|
||||
At most one step can be in_progress at a time.
|
||||
"}
|
||||
.to_string(),
|
||||
strict: false,
|
||||
parameters: JsonSchema::Object {
|
||||
|
||||
@@ -12,6 +12,7 @@ use codex_protocol::models::VIEW_IMAGE_TOOL_NAME;
|
||||
use codex_protocol::openai_models::ApplyPatchToolType;
|
||||
use codex_protocol::openai_models::ConfigShellToolType;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use indoc::indoc;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value as JsonValue;
|
||||
@@ -301,22 +302,27 @@ fn create_shell_tool() -> ToolSpec {
|
||||
),
|
||||
]);
|
||||
|
||||
let description = if cfg!(windows) {
|
||||
r#"Runs a Powershell command (Windows) and returns its output. Arguments to `shell` will be passed to CreateProcessW(). Most commands should be prefixed with ["powershell.exe", "-Command"].
|
||||
|
||||
Examples of valid command strings:
|
||||
let description = if cfg!(windows) {
|
||||
indoc! {r#"
|
||||
Runs a Powershell command (Windows) and returns its output. Arguments to `shell` will be passed to CreateProcessW(). Most commands should be prefixed with ["powershell.exe", "-Command"].
|
||||
|
||||
- ls -a (show hidden): ["powershell.exe", "-Command", "Get-ChildItem -Force"]
|
||||
- recursive find by name: ["powershell.exe", "-Command", "Get-ChildItem -Recurse -Filter *.py"]
|
||||
- recursive grep: ["powershell.exe", "-Command", "Get-ChildItem -Path C:\\myrepo -Recurse | Select-String -Pattern 'TODO' -CaseSensitive"]
|
||||
- ps aux | grep python: ["powershell.exe", "-Command", "Get-Process | Where-Object { $_.ProcessName -like '*python*' }"]
|
||||
- setting an env var: ["powershell.exe", "-Command", "$env:FOO='bar'; echo $env:FOO"]
|
||||
- running an inline Python script: ["powershell.exe", "-Command", "@'\\nprint('Hello, world!')\\n'@ | python -"]"#
|
||||
Examples of valid command strings:
|
||||
|
||||
- ls -a (show hidden): ["powershell.exe", "-Command", "Get-ChildItem -Force"]
|
||||
- recursive find by name: ["powershell.exe", "-Command", "Get-ChildItem -Recurse -Filter *.py"]
|
||||
- recursive grep: ["powershell.exe", "-Command", "Get-ChildItem -Path C:\\myrepo -Recurse | Select-String -Pattern 'TODO' -CaseSensitive"]
|
||||
- ps aux | grep python: ["powershell.exe", "-Command", "Get-Process | Where-Object { $_.ProcessName -like '*python*' }"]
|
||||
- setting an env var: ["powershell.exe", "-Command", "$env:FOO='bar'; echo $env:FOO"]
|
||||
- running an inline Python script: ["powershell.exe", "-Command", "@'\nprint('Hello, world!')\n'@ | python -"]
|
||||
"#}
|
||||
.to_string()
|
||||
} else {
|
||||
r#"Runs a shell command and returns its output.
|
||||
- The arguments to `shell` will be passed to execvp(). Most terminal commands should be prefixed with ["bash", "-lc"].
|
||||
- Always set the `workdir` param when using the shell function. Do not use `cd` unless absolutely necessary."#
|
||||
}.to_string();
|
||||
indoc! {"
|
||||
Runs a shell command and returns its output.
|
||||
- The arguments to `shell` will be passed to execvp(). Most terminal commands should be prefixed with [\"bash\", \"-lc\"].
|
||||
- Always set the `workdir` param when using the shell function. Do not use `cd` unless absolutely necessary."}
|
||||
.to_string()
|
||||
};
|
||||
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: "shell".to_string(),
|
||||
@@ -376,20 +382,25 @@ fn create_shell_command_tool() -> ToolSpec {
|
||||
]);
|
||||
|
||||
let description = if cfg!(windows) {
|
||||
r#"Runs a Powershell command (Windows) and returns its output.
|
||||
|
||||
Examples of valid command strings:
|
||||
indoc! {r#"
|
||||
Runs a Powershell command (Windows) and returns its output.
|
||||
|
||||
- ls -a (show hidden): "Get-ChildItem -Force"
|
||||
- recursive find by name: "Get-ChildItem -Recurse -Filter *.py"
|
||||
- recursive grep: "Get-ChildItem -Path C:\\myrepo -Recurse | Select-String -Pattern 'TODO' -CaseSensitive"
|
||||
- ps aux | grep python: "Get-Process | Where-Object { $_.ProcessName -like '*python*' }"
|
||||
- setting an env var: "$env:FOO='bar'; echo $env:FOO"
|
||||
- running an inline Python script: "@'\\nprint('Hello, world!')\\n'@ | python -"#
|
||||
Examples of valid command strings:
|
||||
|
||||
- ls -a (show hidden): "Get-ChildItem -Force"
|
||||
- recursive find by name: "Get-ChildItem -Recurse -Filter *.py"
|
||||
- recursive grep: "Get-ChildItem -Path C:\\myrepo -Recurse | Select-String -Pattern 'TODO' -CaseSensitive"
|
||||
- ps aux | grep python: "Get-Process | Where-Object { $_.ProcessName -like '*python*' }"
|
||||
- setting an env var: "$env:FOO='bar'; echo $env:FOO"
|
||||
- running an inline Python script: "@'\nprint('Hello, world!')\n'@ | python -"
|
||||
"#}
|
||||
.to_string()
|
||||
} else {
|
||||
r#"Runs a shell command and returns its output.
|
||||
- Always set the `workdir` param when using the shell_command function. Do not use `cd` unless absolutely necessary."#
|
||||
}.to_string();
|
||||
indoc! {"
|
||||
Runs a shell command and returns its output.
|
||||
- Always set the `workdir` param when using the shell_command function. Do not use `cd` unless absolutely necessary."}
|
||||
.to_string()
|
||||
};
|
||||
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: "shell_command".to_string(),
|
||||
@@ -2148,21 +2159,26 @@ mod tests {
|
||||
assert_eq!(name, "shell");
|
||||
|
||||
let expected = if cfg!(windows) {
|
||||
r#"Runs a Powershell command (Windows) and returns its output. Arguments to `shell` will be passed to CreateProcessW(). Most commands should be prefixed with ["powershell.exe", "-Command"].
|
||||
|
||||
Examples of valid command strings:
|
||||
indoc! {r#"
|
||||
Runs a Powershell command (Windows) and returns its output. Arguments to `shell` will be passed to CreateProcessW(). Most commands should be prefixed with ["powershell.exe", "-Command"].
|
||||
|
||||
- ls -a (show hidden): ["powershell.exe", "-Command", "Get-ChildItem -Force"]
|
||||
- recursive find by name: ["powershell.exe", "-Command", "Get-ChildItem -Recurse -Filter *.py"]
|
||||
- recursive grep: ["powershell.exe", "-Command", "Get-ChildItem -Path C:\\myrepo -Recurse | Select-String -Pattern 'TODO' -CaseSensitive"]
|
||||
- ps aux | grep python: ["powershell.exe", "-Command", "Get-Process | Where-Object { $_.ProcessName -like '*python*' }"]
|
||||
- setting an env var: ["powershell.exe", "-Command", "$env:FOO='bar'; echo $env:FOO"]
|
||||
- running an inline Python script: ["powershell.exe", "-Command", "@'\\nprint('Hello, world!')\\n'@ | python -"]"#
|
||||
Examples of valid command strings:
|
||||
|
||||
- ls -a (show hidden): ["powershell.exe", "-Command", "Get-ChildItem -Force"]
|
||||
- recursive find by name: ["powershell.exe", "-Command", "Get-ChildItem -Recurse -Filter *.py"]
|
||||
- recursive grep: ["powershell.exe", "-Command", "Get-ChildItem -Path C:\\myrepo -Recurse | Select-String -Pattern 'TODO' -CaseSensitive"]
|
||||
- ps aux | grep python: ["powershell.exe", "-Command", "Get-Process | Where-Object { $_.ProcessName -like '*python*' }"]
|
||||
- setting an env var: ["powershell.exe", "-Command", "$env:FOO='bar'; echo $env:FOO"]
|
||||
- running an inline Python script: ["powershell.exe", "-Command", "@'\nprint('Hello, world!')\n'@ | python -"]
|
||||
"#}
|
||||
.to_string()
|
||||
} else {
|
||||
r#"Runs a shell command and returns its output.
|
||||
- The arguments to `shell` will be passed to execvp(). Most terminal commands should be prefixed with ["bash", "-lc"].
|
||||
- Always set the `workdir` param when using the shell function. Do not use `cd` unless absolutely necessary."#
|
||||
}.to_string();
|
||||
indoc! {"
|
||||
Runs a shell command and returns its output.
|
||||
- The arguments to `shell` will be passed to execvp(). Most terminal commands should be prefixed with [\"bash\", \"-lc\"].
|
||||
- Always set the `workdir` param when using the shell function. Do not use `cd` unless absolutely necessary."}
|
||||
.to_string()
|
||||
};
|
||||
assert_eq!(description, &expected);
|
||||
}
|
||||
|
||||
@@ -2178,19 +2194,24 @@ Examples of valid command strings:
|
||||
assert_eq!(name, "shell_command");
|
||||
|
||||
let expected = if cfg!(windows) {
|
||||
r#"Runs a Powershell command (Windows) and returns its output.
|
||||
|
||||
Examples of valid command strings:
|
||||
indoc! {r#"
|
||||
Runs a Powershell command (Windows) and returns its output.
|
||||
|
||||
- ls -a (show hidden): "Get-ChildItem -Force"
|
||||
- recursive find by name: "Get-ChildItem -Recurse -Filter *.py"
|
||||
- recursive grep: "Get-ChildItem -Path C:\\myrepo -Recurse | Select-String -Pattern 'TODO' -CaseSensitive"
|
||||
- ps aux | grep python: "Get-Process | Where-Object { $_.ProcessName -like '*python*' }"
|
||||
- setting an env var: "$env:FOO='bar'; echo $env:FOO"
|
||||
- running an inline Python script: "@'\\nprint('Hello, world!')\\n'@ | python -"#.to_string()
|
||||
Examples of valid command strings:
|
||||
|
||||
- ls -a (show hidden): "Get-ChildItem -Force"
|
||||
- recursive find by name: "Get-ChildItem -Recurse -Filter *.py"
|
||||
- recursive grep: "Get-ChildItem -Path C:\\myrepo -Recurse | Select-String -Pattern 'TODO' -CaseSensitive"
|
||||
- ps aux | grep python: "Get-Process | Where-Object { $_.ProcessName -like '*python*' }"
|
||||
- setting an env var: "$env:FOO='bar'; echo $env:FOO"
|
||||
- running an inline Python script: "@'\nprint('Hello, world!')\n'@ | python -"
|
||||
"#}
|
||||
.to_string()
|
||||
} else {
|
||||
r#"Runs a shell command and returns its output.
|
||||
- Always set the `workdir` param when using the shell_command function. Do not use `cd` unless absolutely necessary."#.to_string()
|
||||
indoc! {"
|
||||
Runs a shell command and returns its output.
|
||||
- Always set the `workdir` param when using the shell_command function. Do not use `cd` unless absolutely necessary."}
|
||||
.to_string()
|
||||
};
|
||||
assert_eq!(description, &expected);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ pub fn format_user_shell_command_record(
|
||||
turn_context: &TurnContext,
|
||||
) -> String {
|
||||
let body = format_user_shell_command_body(command, exec_output, turn_context);
|
||||
format!("{USER_SHELL_COMMAND_OPEN}\n{body}\n{USER_SHELL_COMMAND_CLOSE}")
|
||||
[USER_SHELL_COMMAND_OPEN, &body, USER_SHELL_COMMAND_CLOSE].join("\n")
|
||||
}
|
||||
|
||||
pub fn user_shell_command_record_item(
|
||||
@@ -70,13 +70,15 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::codex::make_session_and_context;
|
||||
use crate::exec::StreamOutput;
|
||||
use indoc::indoc;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn detects_user_shell_command_text_variants() {
|
||||
assert!(is_user_shell_command_text(
|
||||
"<user_shell_command>\necho hi\n</user_shell_command>"
|
||||
));
|
||||
assert!(is_user_shell_command_text(indoc! {"
|
||||
<user_shell_command>
|
||||
echo hi
|
||||
</user_shell_command>"}));
|
||||
assert!(!is_user_shell_command_text("echo hi"));
|
||||
}
|
||||
|
||||
@@ -100,7 +102,18 @@ mod tests {
|
||||
};
|
||||
assert_eq!(
|
||||
text,
|
||||
"<user_shell_command>\n<command>\necho hi\n</command>\n<result>\nExit code: 0\nDuration: 1.0000 seconds\nOutput:\nhi\n</result>\n</user_shell_command>"
|
||||
indoc! {"
|
||||
<user_shell_command>
|
||||
<command>
|
||||
echo hi
|
||||
</command>
|
||||
<result>
|
||||
Exit code: 0
|
||||
Duration: 1.0000 seconds
|
||||
Output:
|
||||
hi
|
||||
</result>
|
||||
</user_shell_command>"}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -118,7 +131,18 @@ mod tests {
|
||||
let record = format_user_shell_command_record("false", &exec_output, &turn_context);
|
||||
assert_eq!(
|
||||
record,
|
||||
"<user_shell_command>\n<command>\nfalse\n</command>\n<result>\nExit code: 42\nDuration: 0.1200 seconds\nOutput:\ncombined output wins\n</result>\n</user_shell_command>"
|
||||
indoc! {"
|
||||
<user_shell_command>
|
||||
<command>
|
||||
false
|
||||
</command>
|
||||
<result>
|
||||
Exit code: 42
|
||||
Duration: 0.1200 seconds
|
||||
Output:
|
||||
combined output wins
|
||||
</result>
|
||||
</user_shell_command>"}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use core_test_support::responses::ev_apply_patch_call;
|
||||
use core_test_support::responses::ev_apply_patch_custom_tool_call;
|
||||
use core_test_support::responses::ev_shell_command_call;
|
||||
use core_test_support::test_codex::ApplyPatchModelOutput;
|
||||
use indoc::indoc;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::fs;
|
||||
use std::sync::atomic::AtomicI32;
|
||||
@@ -102,7 +103,16 @@ async fn apply_patch_cli_multiple_operations_integration(
|
||||
fs::write(&modify_path, "line1\nline2\n")?;
|
||||
fs::write(&delete_path, "obsolete\n")?;
|
||||
|
||||
let patch = "*** Begin Patch\n*** Add File: nested/new.txt\n+created\n*** Delete File: delete.txt\n*** Update File: modify.txt\n@@\n-line2\n+changed\n*** End Patch";
|
||||
let patch = indoc! {"
|
||||
*** Begin Patch
|
||||
*** Add File: nested/new.txt
|
||||
+created
|
||||
*** Delete File: delete.txt
|
||||
*** Update File: modify.txt
|
||||
@@
|
||||
-line2
|
||||
+changed
|
||||
*** End Patch"};
|
||||
|
||||
let call_id = "apply-multi-ops";
|
||||
mount_apply_patch(&harness, call_id, patch, "done", output_type).await;
|
||||
@@ -145,7 +155,16 @@ async fn apply_patch_cli_multiple_chunks(model_output: ApplyPatchModelOutput) ->
|
||||
let target = harness.path("multi.txt");
|
||||
fs::write(&target, "line1\nline2\nline3\nline4\n")?;
|
||||
|
||||
let patch = "*** Begin Patch\n*** Update File: multi.txt\n@@\n-line2\n+changed2\n@@\n-line4\n+changed4\n*** End Patch";
|
||||
let patch = indoc! {"
|
||||
*** Begin Patch
|
||||
*** Update File: multi.txt
|
||||
@@
|
||||
-line2
|
||||
+changed2
|
||||
@@
|
||||
-line4
|
||||
+changed4
|
||||
*** End Patch"};
|
||||
let call_id = "apply-multi-chunks";
|
||||
mount_apply_patch(&harness, call_id, patch, "ok", model_output).await;
|
||||
|
||||
@@ -176,7 +195,14 @@ async fn apply_patch_cli_moves_file_to_new_directory(
|
||||
fs::create_dir_all(original.parent().expect("parent"))?;
|
||||
fs::write(&original, "old content\n")?;
|
||||
|
||||
let patch = "*** Begin Patch\n*** Update File: old/name.txt\n*** Move to: renamed/dir/name.txt\n@@\n-old content\n+new content\n*** End Patch";
|
||||
let patch = indoc! {"
|
||||
*** Begin Patch
|
||||
*** Update File: old/name.txt
|
||||
*** Move to: renamed/dir/name.txt
|
||||
@@
|
||||
-old content
|
||||
+new content
|
||||
*** End Patch"};
|
||||
let call_id = "apply-move";
|
||||
mount_apply_patch(&harness, call_id, patch, "ok", model_output).await;
|
||||
|
||||
@@ -203,7 +229,14 @@ async fn apply_patch_cli_updates_file_appends_trailing_newline(
|
||||
let target = harness.path("no_newline.txt");
|
||||
fs::write(&target, "no newline at end")?;
|
||||
|
||||
let patch = "*** Begin Patch\n*** Update File: no_newline.txt\n@@\n-no newline at end\n+first line\n+second line\n*** End Patch";
|
||||
let patch = indoc! {"
|
||||
*** Begin Patch
|
||||
*** Update File: no_newline.txt
|
||||
@@
|
||||
-no newline at end
|
||||
+first line
|
||||
+second line
|
||||
*** End Patch"};
|
||||
let call_id = "apply-append-nl";
|
||||
mount_apply_patch(&harness, call_id, patch, "ok", model_output).await;
|
||||
|
||||
@@ -231,7 +264,14 @@ async fn apply_patch_cli_insert_only_hunk_modifies_file(
|
||||
let target = harness.path("insert_only.txt");
|
||||
fs::write(&target, "alpha\nomega\n")?;
|
||||
|
||||
let patch = "*** Begin Patch\n*** Update File: insert_only.txt\n@@\n alpha\n+beta\n omega\n*** End Patch";
|
||||
let patch = indoc! {"
|
||||
*** Begin Patch
|
||||
*** Update File: insert_only.txt
|
||||
@@
|
||||
alpha
|
||||
+beta
|
||||
omega
|
||||
*** End Patch"};
|
||||
let call_id = "apply-insert-only";
|
||||
mount_apply_patch(&harness, call_id, patch, "ok", model_output).await;
|
||||
|
||||
@@ -261,7 +301,14 @@ async fn apply_patch_cli_move_overwrites_existing_destination(
|
||||
fs::write(&original, "from\n")?;
|
||||
fs::write(&destination, "existing\n")?;
|
||||
|
||||
let patch = "*** Begin Patch\n*** Update File: old/name.txt\n*** Move to: renamed/dir/name.txt\n@@\n-from\n+new\n*** End Patch";
|
||||
let patch = indoc! {"
|
||||
*** Begin Patch
|
||||
*** Update File: old/name.txt
|
||||
*** Move to: renamed/dir/name.txt
|
||||
@@
|
||||
-from
|
||||
+new
|
||||
*** End Patch"};
|
||||
let call_id = "apply-move-overwrite";
|
||||
mount_apply_patch(&harness, call_id, patch, "ok", model_output).await;
|
||||
|
||||
@@ -293,7 +340,13 @@ async fn apply_patch_cli_move_without_content_change_has_no_turn_diff(
|
||||
fs::create_dir_all(original.parent().expect("parent should exist"))?;
|
||||
fs::write(&original, "same\n")?;
|
||||
|
||||
let patch = "*** Begin Patch\n*** Update File: old/name.txt\n*** Move to: renamed/name.txt\n@@\n same\n*** End Patch";
|
||||
let patch = indoc! {"
|
||||
*** Begin Patch
|
||||
*** Update File: old/name.txt
|
||||
*** Move to: renamed/name.txt
|
||||
@@
|
||||
same
|
||||
*** End Patch"};
|
||||
let call_id = "apply-move-no-change";
|
||||
mount_apply_patch(&harness, call_id, patch, "ok", model_output).await;
|
||||
|
||||
@@ -346,7 +399,11 @@ async fn apply_patch_cli_add_overwrites_existing_file(
|
||||
let path = harness.path("duplicate.txt");
|
||||
fs::write(&path, "old content\n")?;
|
||||
|
||||
let patch = "*** Begin Patch\n*** Add File: duplicate.txt\n+new content\n*** End Patch";
|
||||
let patch = indoc! {"
|
||||
*** Begin Patch
|
||||
*** Add File: duplicate.txt
|
||||
+new content
|
||||
*** End Patch"};
|
||||
let call_id = "apply-add-overwrite";
|
||||
mount_apply_patch(&harness, call_id, patch, "ok", model_output).await;
|
||||
|
||||
@@ -369,7 +426,10 @@ async fn apply_patch_cli_rejects_invalid_hunk_header(
|
||||
|
||||
let harness = apply_patch_harness().await?;
|
||||
|
||||
let patch = "*** Begin Patch\n*** Frobnicate File: foo\n*** End Patch";
|
||||
let patch = indoc! {"
|
||||
*** Begin Patch
|
||||
*** Frobnicate File: foo
|
||||
*** End Patch"};
|
||||
let call_id = "apply-invalid-header";
|
||||
mount_apply_patch(&harness, call_id, patch, "ok", model_output).await;
|
||||
|
||||
@@ -435,7 +495,13 @@ async fn apply_patch_cli_reports_missing_target_file(
|
||||
|
||||
let harness = apply_patch_harness().await?;
|
||||
|
||||
let patch = "*** Begin Patch\n*** Update File: missing.txt\n@@\n-nope\n+better\n*** End Patch";
|
||||
let patch = indoc! {"
|
||||
*** Begin Patch
|
||||
*** Update File: missing.txt
|
||||
@@
|
||||
-nope
|
||||
+better
|
||||
*** End Patch"};
|
||||
let call_id = "apply-missing-file";
|
||||
mount_apply_patch(&harness, call_id, patch, "fail", model_output).await;
|
||||
|
||||
@@ -471,7 +537,10 @@ async fn apply_patch_cli_delete_missing_file_reports_error(
|
||||
|
||||
let harness = apply_patch_harness().await?;
|
||||
|
||||
let patch = "*** Begin Patch\n*** Delete File: missing.txt\n*** End Patch";
|
||||
let patch = indoc! {"
|
||||
*** Begin Patch
|
||||
*** Delete File: missing.txt
|
||||
*** End Patch"};
|
||||
let call_id = "apply-delete-missing";
|
||||
mount_apply_patch(&harness, call_id, patch, "fail", model_output).await;
|
||||
|
||||
@@ -506,7 +575,9 @@ async fn apply_patch_cli_rejects_empty_patch(model_output: ApplyPatchModelOutput
|
||||
|
||||
let harness = apply_patch_harness().await?;
|
||||
|
||||
let patch = "*** Begin Patch\n*** End Patch";
|
||||
let patch = indoc! {"
|
||||
*** Begin Patch
|
||||
*** End Patch"};
|
||||
let call_id = "apply-empty";
|
||||
mount_apply_patch(&harness, call_id, patch, "ok", model_output).await;
|
||||
|
||||
@@ -535,7 +606,10 @@ async fn apply_patch_cli_delete_directory_reports_verification_error(
|
||||
|
||||
fs::create_dir(harness.path("dir"))?;
|
||||
|
||||
let patch = "*** Begin Patch\n*** Delete File: dir\n*** End Patch";
|
||||
let patch = indoc! {"
|
||||
*** Begin Patch
|
||||
*** Delete File: dir
|
||||
*** End Patch"};
|
||||
let call_id = "apply-delete-dir";
|
||||
mount_apply_patch(&harness, call_id, patch, "ok", model_output).await;
|
||||
|
||||
@@ -569,7 +643,11 @@ async fn apply_patch_cli_rejects_path_traversal_outside_workspace(
|
||||
.join("escape.txt");
|
||||
let _ = fs::remove_file(&escape_path);
|
||||
|
||||
let patch = "*** Begin Patch\n*** Add File: ../escape.txt\n+outside\n*** End Patch";
|
||||
let patch = indoc! {"
|
||||
*** Begin Patch
|
||||
*** Add File: ../escape.txt
|
||||
+outside
|
||||
*** End Patch"};
|
||||
let call_id = "apply-path-traversal";
|
||||
mount_apply_patch(&harness, call_id, patch, "fail", model_output).await;
|
||||
|
||||
@@ -625,7 +703,14 @@ async fn apply_patch_cli_rejects_move_path_traversal_outside_workspace(
|
||||
let source = harness.path("stay.txt");
|
||||
fs::write(&source, "from\n")?;
|
||||
|
||||
let patch = "*** Begin Patch\n*** Update File: stay.txt\n*** Move to: ../escape-move.txt\n@@\n-from\n+to\n*** End Patch";
|
||||
let patch = indoc! {"
|
||||
*** Begin Patch
|
||||
*** Update File: stay.txt
|
||||
*** Move to: ../escape-move.txt
|
||||
@@
|
||||
-from
|
||||
+to
|
||||
*** End Patch"};
|
||||
let call_id = "apply-move-traversal";
|
||||
mount_apply_patch(&harness, call_id, patch, "fail", model_output).await;
|
||||
|
||||
@@ -674,7 +759,15 @@ async fn apply_patch_cli_verification_failure_has_no_side_effects(
|
||||
|
||||
// Compose a patch that would create a file, then fail verification on an update.
|
||||
let call_id = "apply-partial-no-side-effects";
|
||||
let patch = "*** Begin Patch\n*** Add File: created.txt\n+hello\n*** Update File: missing.txt\n@@\n-old\n+new\n*** End Patch";
|
||||
let patch = indoc! {"
|
||||
*** Begin Patch
|
||||
*** Add File: created.txt
|
||||
+hello
|
||||
*** Update File: missing.txt
|
||||
@@
|
||||
-old
|
||||
+new
|
||||
*** End Patch"};
|
||||
|
||||
mount_apply_patch(&harness, call_id, patch, "failed", model_output).await;
|
||||
|
||||
@@ -1042,7 +1135,14 @@ async fn apply_patch_cli_end_of_file_anchor(model_output: ApplyPatchModelOutput)
|
||||
let target = harness.path("tail.txt");
|
||||
fs::write(&target, "alpha\nlast\n")?;
|
||||
|
||||
let patch = "*** Begin Patch\n*** Update File: tail.txt\n@@\n-last\n+end\n*** End of File\n*** End Patch";
|
||||
let patch = indoc! {"
|
||||
*** Begin Patch
|
||||
*** Update File: tail.txt
|
||||
@@
|
||||
-last
|
||||
+end
|
||||
*** End of File
|
||||
*** End Patch"};
|
||||
let call_id = "apply-eof";
|
||||
mount_apply_patch(&harness, call_id, patch, "ok", model_output).await;
|
||||
|
||||
@@ -1164,7 +1264,14 @@ async fn apply_patch_turn_diff_for_rename_with_content_change(
|
||||
|
||||
// Patch: update + move
|
||||
let call_id = "apply-rename-change";
|
||||
let patch = "*** Begin Patch\n*** Update File: old.txt\n*** Move to: new.txt\n@@\n-old\n+new\n*** End Patch";
|
||||
let patch = indoc! {"
|
||||
*** Begin Patch
|
||||
*** Update File: old.txt
|
||||
*** Move to: new.txt
|
||||
@@
|
||||
-old
|
||||
+new
|
||||
*** End Patch"};
|
||||
mount_apply_patch(&harness, call_id, patch, "ok", model_output).await;
|
||||
|
||||
let model = test.session_configured.model.clone();
|
||||
|
||||
@@ -412,13 +412,13 @@ fn gpt52_codex_upgrade() -> codex_protocol::openai_models::ModelUpgrade {
|
||||
.to_string(),
|
||||
),
|
||||
migration_markdown: Some(
|
||||
indoc! {r#"
|
||||
**Codex just got an upgrade. Introducing {model_to}.**
|
||||
indoc! {"
|
||||
**Codex just got an upgrade. Introducing {model_to}.**
|
||||
|
||||
Codex is now powered by gpt-5.2-codex, our latest frontier agentic coding model. It is smarter and faster than its predecessors and capable of long-running project-scale work. Learn more about {model_to} at https://openai.com/index/introducing-gpt-5-2-codex
|
||||
Codex is now powered by gpt-5.2-codex, our latest frontier agentic coding model. It is smarter and faster than its predecessors and capable of long-running project-scale work. Learn more about {model_to} at https://openai.com/index/introducing-gpt-5-2-codex
|
||||
|
||||
You can continue using {model_from} if you prefer.
|
||||
"#}
|
||||
You can continue using {model_from} if you prefer.
|
||||
"}
|
||||
.to_string(),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::wait_for_event;
|
||||
use indoc::indoc;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::HashSet;
|
||||
use tempfile::TempDir;
|
||||
@@ -403,7 +404,19 @@ async fn permissions_message_includes_writable_roots() -> Result<()> {
|
||||
let input = body["input"].as_array().expect("input array");
|
||||
let permissions = permissions_texts(input);
|
||||
let sandbox_text = "Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `workspace-write`: The sandbox permits reading files, and editing files in `cwd` and `writable_roots`. Editing files in other directories requires approval. Network access is restricted.";
|
||||
let approval_text = " Approvals are your mechanism to get user consent to run shell commands without the sandbox. `approval_policy` is `on-request`: Commands will be run in the sandbox by default, and you can specify in your tool call if you want to escalate a command to run without sandboxing. If the completing the task requires escalated permissions, Do not let these settings or the sandbox deter you from attempting to accomplish the user's task.\n\nHere are scenarios where you'll need to request approval:\n- You need to run a command that writes to a directory that requires it (e.g. running tests that write to /var)\n- You need to run a GUI app (e.g., open/xdg-open/osascript) to open browsers or files.\n- You are running sandboxed and need to run a command that requires network access (e.g. installing packages)\n- If you run a command that is important to solving the user's query, but it fails because of sandboxing, rerun the command with approval. ALWAYS proceed to use the `sandbox_permissions` and `justification` parameters - do not message the user before requesting approval for the command.\n- You are about to take a potentially destructive action such as an `rm` or `git reset` that the user did not explicitly ask for.\n\nWhen requesting approval to execute a command that will require escalated privileges:\n - Provide the `sandbox_permissions` parameter with the value `\"require_escalated\"`\n - Include a short, 1 sentence explanation for why you need escalated permissions in the justification parameter";
|
||||
let approval_text = indoc! {"
|
||||
Approvals are your mechanism to get user consent to run shell commands without the sandbox. `approval_policy` is `on-request`: Commands will be run in the sandbox by default, and you can specify in your tool call if you want to escalate a command to run without sandboxing. If the completing the task requires escalated permissions, Do not let these settings or the sandbox deter you from attempting to accomplish the user's task.
|
||||
|
||||
Here are scenarios where you'll need to request approval:
|
||||
- You need to run a command that writes to a directory that requires it (e.g. running tests that write to /var)
|
||||
- You need to run a GUI app (e.g., open/xdg-open/osascript) to open browsers or files.
|
||||
- You are running sandboxed and need to run a command that requires network access (e.g. installing packages)
|
||||
- If you run a command that is important to solving the user's query, but it fails because of sandboxing, rerun the command with approval. ALWAYS proceed to use the `sandbox_permissions` and `justification` parameters - do not message the user before requesting approval for the command.
|
||||
- You are about to take a potentially destructive action such as an `rm` or `git reset` that the user did not explicitly ask for.
|
||||
|
||||
When requesting approval to execute a command that will require escalated privileges:
|
||||
- Provide the `sandbox_permissions` parameter with the value `\"require_escalated\"`
|
||||
- Include a short, 1 sentence explanation for why you need escalated permissions in the justification parameter"};
|
||||
// Normalize paths by removing trailing slashes to match AbsolutePathBuf behavior
|
||||
let normalize_path =
|
||||
|p: &std::path::Path| -> String { p.to_string_lossy().trim_end_matches('/').to_string() };
|
||||
|
||||
@@ -120,6 +120,7 @@ codex-utils-pty = { workspace = true }
|
||||
assert_matches = { workspace = true }
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
insta = { workspace = true }
|
||||
indoc = { workspace = true }
|
||||
pretty_assertions = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
serial_test = { workspace = true }
|
||||
|
||||
@@ -69,6 +69,7 @@ use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyModifiers;
|
||||
use indoc::indoc;
|
||||
use insta::assert_snapshot;
|
||||
use pretty_assertions::assert_eq;
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -3811,7 +3812,7 @@ async fn chatwidget_markdown_code_blocks_vt100_snapshot() {
|
||||
term.set_viewport_area(Rect::new(0, height - 1, width, 1));
|
||||
|
||||
// Simulate streaming via AgentMessageDelta in 2-character chunks (no final AgentMessage).
|
||||
let source: &str = r#"
|
||||
let source: &str = indoc! {r#"
|
||||
|
||||
-- Indented code block (4 spaces)
|
||||
SELECT *
|
||||
@@ -3831,7 +3832,7 @@ printf 'fenced within fenced\n'
|
||||
"regex": "^foo.*(bar)?$"
|
||||
}
|
||||
```
|
||||
"#;
|
||||
"#};
|
||||
|
||||
let mut it = source.chars();
|
||||
loop {
|
||||
|
||||
@@ -287,6 +287,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::markdown_render::render_markdown_text;
|
||||
use crate::test_backend::VT100Backend;
|
||||
use indoc::indoc;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Color;
|
||||
|
||||
@@ -480,7 +481,13 @@ mod tests {
|
||||
#[test]
|
||||
fn vt100_deep_nested_mixed_list_third_level_marker_is_colored() {
|
||||
// Markdown with five levels (ordered → unordered → ordered → unordered → unordered).
|
||||
let md = "1. First\n - Second level\n 1. Third level (ordered)\n - Fourth level (bullet)\n - Fifth level to test indent consistency\n";
|
||||
let md = indoc! {"
|
||||
1. First
|
||||
- Second level
|
||||
1. Third level (ordered)
|
||||
- Fourth level (bullet)
|
||||
- Fifth level to test indent consistency
|
||||
"};
|
||||
let text = render_markdown_text(md);
|
||||
let lines: Vec<Line<'static>> = text.lines.clone();
|
||||
|
||||
|
||||
@@ -189,6 +189,7 @@ pub(crate) mod announcement {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::tooltips::announcement::parse_announcement_tip_toml;
|
||||
use indoc::indoc;
|
||||
use rand::SeedableRng;
|
||||
use rand::rngs::StdRng;
|
||||
|
||||
@@ -211,40 +212,40 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn announcement_tip_toml_picks_last_matching() {
|
||||
let toml = r#"
|
||||
[[announcements]]
|
||||
content = "first"
|
||||
from_date = "2000-01-01"
|
||||
let toml = indoc! {r#"
|
||||
[[announcements]]
|
||||
content = "first"
|
||||
from_date = "2000-01-01"
|
||||
|
||||
[[announcements]]
|
||||
content = "latest match"
|
||||
version_regex = ".*"
|
||||
target_app = "cli"
|
||||
[[announcements]]
|
||||
content = "latest match"
|
||||
version_regex = ".*"
|
||||
target_app = "cli"
|
||||
|
||||
[[announcements]]
|
||||
content = "should not match"
|
||||
to_date = "2000-01-01"
|
||||
"#;
|
||||
[[announcements]]
|
||||
content = "should not match"
|
||||
to_date = "2000-01-01"
|
||||
"#};
|
||||
|
||||
assert_eq!(
|
||||
Some("latest match".to_string()),
|
||||
parse_announcement_tip_toml(toml)
|
||||
);
|
||||
|
||||
let toml = r#"
|
||||
[[announcements]]
|
||||
content = "first"
|
||||
from_date = "2000-01-01"
|
||||
target_app = "cli"
|
||||
let toml = indoc! {r#"
|
||||
[[announcements]]
|
||||
content = "first"
|
||||
from_date = "2000-01-01"
|
||||
target_app = "cli"
|
||||
|
||||
[[announcements]]
|
||||
content = "latest match"
|
||||
version_regex = ".*"
|
||||
[[announcements]]
|
||||
content = "latest match"
|
||||
version_regex = ".*"
|
||||
|
||||
[[announcements]]
|
||||
content = "should not match"
|
||||
to_date = "2000-01-01"
|
||||
"#;
|
||||
[[announcements]]
|
||||
content = "should not match"
|
||||
to_date = "2000-01-01"
|
||||
"#};
|
||||
|
||||
assert_eq!(
|
||||
Some("latest match".to_string()),
|
||||
@@ -254,54 +255,54 @@ to_date = "2000-01-01"
|
||||
|
||||
#[test]
|
||||
fn announcement_tip_toml_picks_no_match() {
|
||||
let toml = r#"
|
||||
[[announcements]]
|
||||
content = "first"
|
||||
from_date = "2000-01-01"
|
||||
to_date = "2000-01-05"
|
||||
let toml = indoc! {r#"
|
||||
[[announcements]]
|
||||
content = "first"
|
||||
from_date = "2000-01-01"
|
||||
to_date = "2000-01-05"
|
||||
|
||||
[[announcements]]
|
||||
content = "latest match"
|
||||
version_regex = "invalid_version_name"
|
||||
[[announcements]]
|
||||
content = "latest match"
|
||||
version_regex = "invalid_version_name"
|
||||
|
||||
[[announcements]]
|
||||
content = "should not match either "
|
||||
target_app = "vsce"
|
||||
"#;
|
||||
[[announcements]]
|
||||
content = "should not match either "
|
||||
target_app = "vsce"
|
||||
"#};
|
||||
|
||||
assert_eq!(None, parse_announcement_tip_toml(toml));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn announcement_tip_toml_bad_deserialization() {
|
||||
let toml = r#"
|
||||
[[announcements]]
|
||||
content = 123
|
||||
from_date = "2000-01-01"
|
||||
"#;
|
||||
let toml = indoc! {r#"
|
||||
[[announcements]]
|
||||
content = 123
|
||||
from_date = "2000-01-01"
|
||||
"#};
|
||||
|
||||
assert_eq!(None, parse_announcement_tip_toml(toml));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn announcement_tip_toml_parse_comments() {
|
||||
let toml = r#"
|
||||
# Example announcement tips for Codex TUI.
|
||||
# Each [[announcements]] entry is evaluated in order; the last matching one is shown.
|
||||
# Dates are UTC, formatted as YYYY-MM-DD. The from_date is inclusive and the to_date is exclusive.
|
||||
# version_regex matches against the CLI version (env!("CARGO_PKG_VERSION")); omit to apply to all versions.
|
||||
# target_app specify which app should display the announcement (cli, vsce, ...).
|
||||
let toml = indoc! {r#"
|
||||
# Example announcement tips for Codex TUI.
|
||||
# Each [[announcements]] entry is evaluated in order; the last matching one is shown.
|
||||
# Dates are UTC, formatted as YYYY-MM-DD. The from_date is inclusive and the to_date is exclusive.
|
||||
# version_regex matches against the CLI version (env!("CARGO_PKG_VERSION")); omit to apply to all versions.
|
||||
# target_app specify which app should display the announcement (cli, vsce, ...).
|
||||
|
||||
[[announcements]]
|
||||
content = "Welcome to Codex! Check out the new onboarding flow."
|
||||
from_date = "2024-10-01"
|
||||
to_date = "2024-10-15"
|
||||
target_app = "cli"
|
||||
version_regex = "^0\\.0\\.0$"
|
||||
[[announcements]]
|
||||
content = "Welcome to Codex! Check out the new onboarding flow."
|
||||
from_date = "2024-10-01"
|
||||
to_date = "2024-10-15"
|
||||
target_app = "cli"
|
||||
version_regex = "^0\\.0\\.0$"
|
||||
|
||||
[[announcements]]
|
||||
content = "This is a test announcement"
|
||||
"#;
|
||||
[[announcements]]
|
||||
content = "This is a test announcement"
|
||||
"#};
|
||||
|
||||
assert_eq!(
|
||||
Some("This is a test announcement".to_string()),
|
||||
|
||||
@@ -24,4 +24,5 @@ walkdir = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches = { workspace = true }
|
||||
indoc = { workspace = true }
|
||||
pretty_assertions = { workspace = true }
|
||||
|
||||
@@ -595,6 +595,7 @@ fn regex_ci(pat: &str) -> Regex {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use indoc::indoc;
|
||||
use std::path::Path;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::OnceLock;
|
||||
@@ -635,21 +636,42 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn extract_paths_handles_quoted_headers() {
|
||||
let diff = "diff --git \"a/hello world.txt\" \"b/hello world.txt\"\nnew file mode 100644\n--- /dev/null\n+++ b/hello world.txt\n@@ -0,0 +1 @@\n+hi\n";
|
||||
let diff = indoc! {r#"
|
||||
diff --git "a/hello world.txt" "b/hello world.txt"
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/hello world.txt
|
||||
@@ -0,0 +1 @@
|
||||
+hi
|
||||
"#};
|
||||
let paths = extract_paths_from_patch(diff);
|
||||
assert_eq!(paths, vec!["hello world.txt".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_paths_ignores_dev_null_header() {
|
||||
let diff = "diff --git a/dev/null b/ok.txt\nnew file mode 100644\n--- /dev/null\n+++ b/ok.txt\n@@ -0,0 +1 @@\n+hi\n";
|
||||
let diff = indoc! {r#"
|
||||
diff --git a/dev/null b/ok.txt
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/ok.txt
|
||||
@@ -0,0 +1 @@
|
||||
+hi
|
||||
"#};
|
||||
let paths = extract_paths_from_patch(diff);
|
||||
assert_eq!(paths, vec!["ok.txt".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_paths_unescapes_c_style_in_quoted_headers() {
|
||||
let diff = "diff --git \"a/hello\\tworld.txt\" \"b/hello\\tworld.txt\"\nnew file mode 100644\n--- /dev/null\n+++ b/hello\tworld.txt\n@@ -0,0 +1 @@\n+hi\n";
|
||||
let diff = indoc! {r#"
|
||||
diff --git "a/hello\tworld.txt" "b/hello\tworld.txt"
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/hello world.txt
|
||||
@@ -0,0 +1 @@
|
||||
+hi
|
||||
"#};
|
||||
let paths = extract_paths_from_patch(diff);
|
||||
assert_eq!(paths, vec!["hello\tworld.txt".to_string()]);
|
||||
}
|
||||
@@ -669,7 +691,15 @@ mod tests {
|
||||
let repo = init_repo();
|
||||
let root = repo.path();
|
||||
|
||||
let diff = "diff --git a/hello.txt b/hello.txt\nnew file mode 100644\n--- /dev/null\n+++ b/hello.txt\n@@ -0,0 +1,2 @@\n+hello\n+world\n";
|
||||
let diff = indoc! {r#"
|
||||
diff --git a/hello.txt b/hello.txt
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/hello.txt
|
||||
@@ -0,0 +1,2 @@
|
||||
+hello
|
||||
+world
|
||||
"#};
|
||||
let req = ApplyGitRequest {
|
||||
cwd: root.to_path_buf(),
|
||||
diff: diff.to_string(),
|
||||
@@ -694,7 +724,16 @@ mod tests {
|
||||
// local edit (unstaged)
|
||||
std::fs::write(root.join("file.txt"), "line1\nlocal2\nline3\n").unwrap();
|
||||
// patch wants to change the same line differently
|
||||
let diff = "diff --git a/file.txt b/file.txt\n--- a/file.txt\n+++ b/file.txt\n@@ -1,3 +1,3 @@\n line1\n-line2\n+remote2\n line3\n";
|
||||
let diff = indoc! {r#"
|
||||
diff --git a/file.txt b/file.txt
|
||||
--- a/file.txt
|
||||
+++ b/file.txt
|
||||
@@ -1,3 +1,3 @@
|
||||
line1
|
||||
-line2
|
||||
+remote2
|
||||
line3
|
||||
"#};
|
||||
let req = ApplyGitRequest {
|
||||
cwd: root.to_path_buf(),
|
||||
diff: diff.to_string(),
|
||||
@@ -711,7 +750,14 @@ mod tests {
|
||||
let repo = init_repo();
|
||||
let root = repo.path();
|
||||
// Try to modify a file that is not in the index
|
||||
let diff = "diff --git a/ghost.txt b/ghost.txt\n--- a/ghost.txt\n+++ b/ghost.txt\n@@ -1,1 +1,1 @@\n-old\n+new\n";
|
||||
let diff = indoc! {r#"
|
||||
diff --git a/ghost.txt b/ghost.txt
|
||||
--- a/ghost.txt
|
||||
+++ b/ghost.txt
|
||||
@@ -1,1 +1,1 @@
|
||||
-old
|
||||
+new
|
||||
"#};
|
||||
let req = ApplyGitRequest {
|
||||
cwd: root.to_path_buf(),
|
||||
diff: diff.to_string(),
|
||||
@@ -733,7 +779,14 @@ mod tests {
|
||||
let _ = run(root, &["git", "commit", "-m", "seed"]);
|
||||
|
||||
// Forward patch: orig -> ORIG
|
||||
let diff = "diff --git a/file.txt b/file.txt\n--- a/file.txt\n+++ b/file.txt\n@@ -1,1 +1,1 @@\n-orig\n+ORIG\n";
|
||||
let diff = indoc! {r#"
|
||||
diff --git a/file.txt b/file.txt
|
||||
--- a/file.txt
|
||||
+++ b/file.txt
|
||||
@@ -1,1 +1,1 @@
|
||||
-orig
|
||||
+ORIG
|
||||
"#};
|
||||
let apply_req = ApplyGitRequest {
|
||||
cwd: root.to_path_buf(),
|
||||
diff: diff.to_string(),
|
||||
@@ -768,7 +821,14 @@ mod tests {
|
||||
let _ = run(root, &["git", "add", "file.txt"]);
|
||||
let _ = run(root, &["git", "commit", "-m", "seed"]);
|
||||
|
||||
let diff = "diff --git a/file.txt b/file.txt\n--- a/file.txt\n+++ b/file.txt\n@@ -1,1 +1,1 @@\n-orig\n+ORIG\n";
|
||||
let diff = indoc! {r#"
|
||||
diff --git a/file.txt b/file.txt
|
||||
--- a/file.txt
|
||||
+++ b/file.txt
|
||||
@@ -1,1 +1,1 @@
|
||||
-orig
|
||||
+ORIG
|
||||
"#};
|
||||
let apply_req = ApplyGitRequest {
|
||||
cwd: root.to_path_buf(),
|
||||
diff: diff.to_string(),
|
||||
@@ -809,8 +869,22 @@ mod tests {
|
||||
let repo = init_repo();
|
||||
let root = repo.path();
|
||||
// Build a multi-file diff: one valid add (ok.txt) and one invalid modify (ghost.txt)
|
||||
let diff = "diff --git a/ok.txt b/ok.txt\nnew file mode 100644\n--- /dev/null\n+++ b/ok.txt\n@@ -0,0 +1,2 @@\n+alpha\n+beta\n\n\
|
||||
diff --git a/ghost.txt b/ghost.txt\n--- a/ghost.txt\n+++ b/ghost.txt\n@@ -1,1 +1,1 @@\n-old\n+new\n";
|
||||
let diff = indoc! {r#"
|
||||
diff --git a/ok.txt b/ok.txt
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/ok.txt
|
||||
@@ -0,0 +1,2 @@
|
||||
+alpha
|
||||
+beta
|
||||
|
||||
diff --git a/ghost.txt b/ghost.txt
|
||||
--- a/ghost.txt
|
||||
+++ b/ghost.txt
|
||||
@@ -1,1 +1,1 @@
|
||||
-old
|
||||
+new
|
||||
"#};
|
||||
|
||||
// 1) With preflight enabled, nothing should be changed (even though ok.txt could be added)
|
||||
let req1 = ApplyGitRequest {
|
||||
|
||||
Reference in New Issue
Block a user