Skip to content
Closed
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 2 additions & 26 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 49 additions & 0 deletions packages/core/src/tools/write-file.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@ describe('WriteFileTool', () => {
abortSignal,
true,
true, // aggressiveUnescape
false, // isJsonLike
);
expect(result.correctedContent).toBe(correctedContent);
expect(result.originalContent).toBe('');
Expand Down Expand Up @@ -389,9 +390,50 @@ describe('WriteFileTool', () => {
abortSignal,
true,
false, // aggressiveUnescape
false, // isJsonLike
);
});

it('should set isJsonLike to true and aggressiveUnescape to false for .ipynb, .json, map, yaml, toml, and common JSON-based dotfiles', async () => {
const abortSignal = new AbortController().signal;
const filesToTest = [
'notebook.ipynb',
'data.json',
'bundle.js.map',
'workflow.yaml',
'config.yml',
'Cargo.toml',
'.eslintrc',
'.prettierrc',
'.env',
'.env.local',
'config.ini',
'.gitconfig',
];
Comment thread
amelidev marked this conversation as resolved.
Comment thread
amelidev marked this conversation as resolved.
Comment on lines +397 to +422

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

We should update the test suite to verify that the newly added structured formats (XML, SVG, CSV, TSV, Terraform/HCL, Terraform state, and Web App Manifests) are also correctly identified as isJsonLike and bypass aggressive unescaping.

    it('should set isJsonLike to true and aggressiveUnescape to false for .ipynb, .json, map, yaml, toml, and common JSON-based dotfiles', async () => {
      const abortSignal = new AbortController().signal;
      const filesToTest = [
        'notebook.ipynb',
        'data.json',
        'bundle.js.map',
        'workflow.yaml',
        'config.yml',
        'Cargo.toml',
        '.eslintrc',
        '.prettierrc',
        '.env',
        '.env.local',
        'config.ini',
        '.gitconfig',
        'data.xml',
        'image.svg',
        'data.csv',
        'data.tsv',
        'main.tf',
        'terraform.tfvars',
        'config.hcl',
        'terraform.tfstate',
        'manifest.webmanifest',
      ];


for (const file of filesToTest) {
mockEnsureCorrectFileContent.mockClear();
mockEnsureCorrectFileContent.mockResolvedValue('content');

const filePath = path.join(rootDir, file);
await getCorrectedFileContent(
mockConfig,
filePath,
'content',
abortSignal,
);

expect(mockEnsureCorrectFileContent).toHaveBeenCalledWith(
'content',
mockBaseLlmClientInstance,
abortSignal,
true,
false, // aggressiveUnescape
true, // isJsonLike
);
}
});

it('should call ensureCorrectFileContent for an existing file', async () => {
const filePath = path.join(rootDir, 'existing_corrected_file.txt');
const originalContent = 'Original existing content.';
Expand All @@ -416,6 +458,7 @@ describe('WriteFileTool', () => {
abortSignal,
true,
true, // aggressiveUnescape
false, // isJsonLike
);
expect(result.correctedContent).toBe(correctedProposedContent);
expect(result.originalContent).toBe(originalContent);
Expand Down Expand Up @@ -493,6 +536,7 @@ describe('WriteFileTool', () => {
abortSignal,
true,
true, // aggressiveUnescape
false, // isJsonLike
);
expect(confirmation).toEqual(
expect.objectContaining({
Expand Down Expand Up @@ -531,6 +575,7 @@ describe('WriteFileTool', () => {
abortSignal,
true,
true, // aggressiveUnescape
false, // isJsonLike
);
expect(confirmation).toEqual(
expect.objectContaining({
Expand Down Expand Up @@ -737,6 +782,7 @@ describe('WriteFileTool', () => {
abortSignal,
true,
true, // aggressiveUnescape
false, // isJsonLike
);
expect(result.llmContent).toMatch(
/Successfully created and wrote to new file/,
Expand Down Expand Up @@ -782,6 +828,7 @@ describe('WriteFileTool', () => {
abortSignal,
true,
true, // aggressiveUnescape
false, // isJsonLike
);
expect(result.llmContent).toMatch(/Successfully overwrote file/);
const writtenContent = await fsService.readTextFile(filePath);
Expand Down Expand Up @@ -1052,6 +1099,7 @@ describe('WriteFileTool', () => {
abortSignal,
true,
true, // aggressiveUnescape
false, // isJsonLike
);
expect(result.correctedContent).toBe(proposedContent);
expect(result.fileExists).toBe(false);
Expand Down Expand Up @@ -1080,6 +1128,7 @@ describe('WriteFileTool', () => {
abortSignal,
true,
true, // aggressiveUnescape
false, // isJsonLike
);
expect(result.correctedContent).toBe(proposedContent);
expect(result.originalContent).toBe(originalContent);
Expand Down
37 changes: 36 additions & 1 deletion packages/core/src/tools/write-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,14 +131,49 @@ export async function getCorrectedFileContent(
}
}

const aggressiveUnescape = !isGemini3Model(config.getActiveModel());
const fileExtension = path.extname(filePath).toLowerCase();
const fileName = path.basename(filePath).toLowerCase();

const isJsonLike =
fileExtension.includes('json') ||
fileExtension === '.ipynb' ||
fileExtension === '.map' ||
fileExtension === '.yaml' ||
fileExtension === '.yml' ||
fileExtension === '.toml' ||
fileExtension === '.lock' ||
fileExtension === '.ini' ||
Comment thread
amelidev marked this conversation as resolved.
fileExtension === '.properties' ||
fileExtension === '.cfg' ||
fileExtension === '.conf' ||
fileExtension === '.config' ||
fileName.startsWith('.env') ||
Comment thread
amelidev marked this conversation as resolved.
[
'.eslintrc',
'.babelrc',
'.prettierrc',
'.stylelintrc',
'.watchmanconfig',
'.nycrc',
'.releaserc',
'.gitconfig',
'.editorconfig',
'.npmrc',
'.yarnrc',
'.jshintrc',
'.markdownlintrc',
].includes(fileName);
Comment thread
amelidev marked this conversation as resolved.
Comment thread
amelidev marked this conversation as resolved.
Comment on lines +137 to +175

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

To prevent silent corruption of other highly structured data and configuration formats, we should expand the isJsonLike check to cover additional common formats such as XML, SVG, CSV, TSV, Terraform/HCL (.tf, .tfvars, .hcl), Terraform state (.tfstate), and Web App Manifests (.webmanifest). These formats are equally vulnerable to aggressive unescaping corruption (e.g., breaking escaped quotes in CSV/TSV or XML attributes).

  const isJsonLike =
    fileExtension.includes('json') ||
    fileExtension === '.ipynb' ||
    fileExtension === '.map' ||
    fileExtension === '.yaml' ||
    fileExtension === '.yml' ||
    fileExtension === '.toml' ||
    fileExtension === '.lock' ||
    fileExtension === '.ini' ||
    fileExtension === '.properties' ||
    fileExtension === '.cfg' ||
    fileExtension === '.conf' ||
    fileExtension === '.config' ||
    fileExtension === '.xml' ||
    fileExtension === '.svg' ||
    fileExtension === '.csv' ||
    fileExtension === '.tsv' ||
    fileExtension === '.tf' ||
    fileExtension === '.tfvars' ||
    fileExtension === '.hcl' ||
    fileExtension === '.tfstate' ||
    fileExtension === '.webmanifest' ||
    fileName.startsWith('.env') ||
    [
      '.eslintrc',
      '.babelrc',
      '.prettierrc',
      '.stylelintrc',
      '.watchmanconfig',
      '.nycrc',
      '.releaserc',
      '.gitconfig',
      '.editorconfig',
      '.npmrc',
      '.yarnrc',
      '.jshintrc',
      '.markdownlintrc',
    ].includes(fileName);


const aggressiveUnescape =
!isJsonLike && !isGemini3Model(config.getActiveModel());

correctedContent = await ensureCorrectFileContent(
proposedContent,
config.getBaseLlmClient(),
abortSignal,
config.getDisableLLMCorrection(),
aggressiveUnescape,
isJsonLike,
);

return { originalContent, correctedContent, fileExists };
Expand Down
Loading
Loading