CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes45downloads
commentJson.test.ts485 linesDownload Raw Back to utils
1/**2 * @license3 * Copyright 2025 Google LLC4 * SPDX-License-Identifier: Apache-2.05 */6 7import { describe, it, expect, beforeEach, afterEach } from 'vitest';8import * as fs from 'node:fs';9import * as path from 'node:path';10import * as os from 'node:os';11import {12  updateSettingsFilePreservingFormat,13  applyUpdates,14} from './commentJson.js';15 16describe('commentJson', () => {17  let tempDir: string;18  let testFilePath: string;19 20  beforeEach(() => {21    // Create a temporary directory for test files22    tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'preserve-format-test-'));23    testFilePath = path.join(tempDir, 'settings.json');24  });25 26  afterEach(() => {27    // Clean up temporary directory28    if (fs.existsSync(tempDir)) {29      fs.rmSync(tempDir, { recursive: true, force: true });30    }31  });32 33  describe('updateSettingsFilePreservingFormat', () => {34    it('should preserve comments when updating settings', () => {35      const originalContent = `{36        // Model configuration37        "model": "gemini-2.5-pro",38        "ui": {39          // Theme setting40          "theme": "dark"41        }42      }`;43 44      fs.writeFileSync(testFilePath, originalContent, 'utf-8');45 46      updateSettingsFilePreservingFormat(testFilePath, {47        model: 'gemini-2.5-flash',48      });49 50      const updatedContent = fs.readFileSync(testFilePath, 'utf-8');51 52      expect(updatedContent).toContain('// Model configuration');53      expect(updatedContent).toContain('// Theme setting');54      expect(updatedContent).toContain('"model": "gemini-2.5-flash"');55      expect(updatedContent).toContain('"theme": "dark"');56    });57 58    it('should handle nested object updates', () => {59      const originalContent = `{60        "ui": {61          "theme": "dark",62          "showLineNumbers": true63        }64      }`;65 66      fs.writeFileSync(testFilePath, originalContent, 'utf-8');67 68      updateSettingsFilePreservingFormat(testFilePath, {69        ui: {70          theme: 'light',71          showLineNumbers: true,72        },73      });74 75      const updatedContent = fs.readFileSync(testFilePath, 'utf-8');76      expect(updatedContent).toContain('"theme": "light"');77      expect(updatedContent).toContain('"showLineNumbers": true');78    });79 80    it('should add new fields while preserving existing structure', () => {81      const originalContent = `{82        // Existing config83        "model": "gemini-2.5-pro"84      }`;85 86      fs.writeFileSync(testFilePath, originalContent, 'utf-8');87 88      updateSettingsFilePreservingFormat(testFilePath, {89        model: 'gemini-2.5-pro',90        newField: 'newValue',91      });92 93      const updatedContent = fs.readFileSync(testFilePath, 'utf-8');94      expect(updatedContent).toContain('// Existing config');95      expect(updatedContent).toContain('"newField": "newValue"');96    });97 98    it('should create file if it does not exist', () => {99      updateSettingsFilePreservingFormat(testFilePath, {100        model: 'gemini-2.5-pro',101      });102 103      expect(fs.existsSync(testFilePath)).toBe(true);104      const content = fs.readFileSync(testFilePath, 'utf-8');105      expect(content).toContain('"model": "gemini-2.5-pro"');106    });107 108    it('should handle complex real-world scenario', () => {109      const complexContent = `{110        // Settings111        "model": "gemini-2.5-pro",112        "mcpServers": {113          // Active server114          "context7": {115            "headers": {116              "API_KEY": "test-key" // API key117            }118          }119        }120      }`;121 122      fs.writeFileSync(testFilePath, complexContent, 'utf-8');123 124      updateSettingsFilePreservingFormat(testFilePath, {125        model: 'gemini-2.5-flash',126        mcpServers: {127          context7: {128            headers: {129              API_KEY: 'new-test-key',130            },131          },132        },133        newSection: {134          setting: 'value',135        },136      });137 138      const updatedContent = fs.readFileSync(testFilePath, 'utf-8');139 140      // Verify comments preserved141      expect(updatedContent).toContain('// Settings');142      expect(updatedContent).toContain('// Active server');143      expect(updatedContent).toContain('// API key');144 145      // Verify updates applied146      expect(updatedContent).toContain('"model": "gemini-2.5-flash"');147      expect(updatedContent).toContain('"newSection"');148      expect(updatedContent).toContain('"API_KEY": "new-test-key"');149    });150 151    it('should handle corrupted JSON files gracefully', () => {152      const corruptedContent = `{153        "model": "gemini-2.5-pro",154        "ui": {155          "theme": "dark"156        // Missing closing brace157      `;158 159      fs.writeFileSync(testFilePath, corruptedContent, 'utf-8');160 161      expect(() => {162        updateSettingsFilePreservingFormat(testFilePath, {163          model: 'gemini-2.5-flash',164        });165      }).not.toThrow();166 167      const unchangedContent = fs.readFileSync(testFilePath, 'utf-8');168      expect(unchangedContent).toBe(corruptedContent);169    });170  });171});172 173describe('applyUpdates', () => {174  it('should apply updates correctly', () => {175    const original = { a: 1, b: { c: 2 } };176    const updates = { b: { c: 3 } };177    const result = applyUpdates(original, updates);178    expect(result).toEqual({ a: 1, b: { c: 3 } });179  });180  it('should apply updates correctly when empty', () => {181    const original = { a: 1, b: { c: 2 } };182    const updates = { b: {} };183    const result = applyUpdates(original, updates);184    expect(result).toEqual({ a: 1, b: {} });185  });186 187  it('should replace the object at the exact replace path', () => {188    const original = {189      ui: { theme: 'dark' },190      mcpServers: {191        keep: { command: 'node' },192        remove: { command: 'python' },193      },194    };195    const updates = {196      mcpServers: {197        keep: { command: 'node' },198      },199    };200 201    const result = applyUpdates(original, updates, false, ['mcpServers']);202 203    expect(result).toEqual({204      ui: { theme: 'dark' },205      mcpServers: {206        keep: { command: 'node' },207      },208    });209  });210 211  it('should replace a nested object while preserving siblings', () => {212    const original = {213      ui: {214        theme: { color: 'red', mode: 'dark' },215        fontSize: 14,216      },217    };218    const updates = {219      ui: {220        theme: { color: 'blue' },221      },222    };223 224    const result = applyUpdates(original, updates, false, ['ui', 'theme']);225 226    expect(result).toEqual({227      ui: {228        theme: { color: 'blue' },229        fontSize: 14,230      },231    });232  });233 234  it('should ignore prototype-pollution keys in updates', () => {235    const original = {};236    const updates = JSON.parse(237      '{"safe":true,"__proto__":{"polluted":true},"constructor":{"prototype":{"polluted":true}},"nested":{"prototype":{"polluted":true},"keep":1}}',238    ) as Record<string, unknown>;239 240    const result = applyUpdates(original, updates);241 242    expect(result).toEqual({243      safe: true,244      nested: {245        keep: 1,246      },247    });248    expect(Object.prototype).not.toHaveProperty('polluted');249  });250});251 252describe('migration write-back via updateSettingsFilePreservingFormat', () => {253  let tempDir: string;254  let testFilePath: string;255 256  beforeEach(() => {257    tempDir = fs.mkdtempSync(258      path.join(os.tmpdir(), 'migration-writeback-test-'),259    );260    testFilePath = path.join(tempDir, 'settings.json');261  });262 263  afterEach(() => {264    if (fs.existsSync(tempDir)) {265      fs.rmSync(tempDir, { recursive: true, force: true });266    }267  });268 269  it('should preserve comments on keys that exist in both original and updates', () => {270    const original = `{271  // My model choice272  "model": "gemini-2.5-pro",273  "ui": {274    // Theme preference275    "theme": "dark"276  }277}`;278 279    fs.writeFileSync(testFilePath, original, 'utf-8');280 281    // Runtime update: only changes model, keeps ui282    const updates = {283      model: 'gemini-2.5-flash',284      ui: {285        theme: 'dark',286      },287    };288 289    updateSettingsFilePreservingFormat(testFilePath, updates);290 291    const result = fs.readFileSync(testFilePath, 'utf-8');292 293    // Comments on preserved keys survive294    expect(result).toContain('// My model choice');295    expect(result).toContain('// Theme preference');296    // Updated value applied297    expect(result).toContain('"model": "gemini-2.5-flash"');298  });299 300  it('should add new keys while preserving existing comments', () => {301    const original = `{302  // API configuration303  "model": "gemini-2.5-flash"304}`;305 306    fs.writeFileSync(testFilePath, original, 'utf-8');307 308    const updates = {309      model: 'gemini-2.5-flash',310      $version: 3,311    };312 313    updateSettingsFilePreservingFormat(testFilePath, updates);314 315    const result = fs.readFileSync(testFilePath, 'utf-8');316 317    // Original comment preserved318    expect(result).toContain('// API configuration');319    // New key added320    expect(result).toContain('$version');321  });322 323  it('should preserve inline comments and trailing commas', () => {324    const original = `{325  "model": "gemini-2.5-pro", // inline comment326  "ui": {327    "theme": "dark",328  },329}`;330 331    fs.writeFileSync(testFilePath, original, 'utf-8');332 333    const updates = {334      model: 'gemini-2.5-flash',335      ui: {336        theme: 'light',337      },338    };339 340    updateSettingsFilePreservingFormat(testFilePath, updates);341 342    const result = fs.readFileSync(testFilePath, 'utf-8');343 344    // Inline comment preserved345    expect(result).toContain('// inline comment');346    // Values updated347    expect(result).toContain('"model": "gemini-2.5-flash"');348    expect(result).toContain('"theme": "light"');349  });350 351  it('should remove nested zombie keys in sync mode', () => {352    // Simulate a V2 settings file with deprecated disable* keys inside nested objects353    const v2Settings = `{354  "general": {355    // Auto-update setting356    "disableAutoUpdate": true,357    "disableUpdateNag": true358  },359  "ui": {360    "theme": "dark"361  },362  "$version": 2363}`;364 365    fs.writeFileSync(testFilePath, v2Settings, 'utf-8');366 367    // Migrated V3 settings: disable* keys removed, enable* keys added368    const migratedSettings = {369      general: {370        enableAutoUpdate: false,371      },372      ui: {373        theme: 'dark',374      },375      $version: 3,376    };377 378    const result = updateSettingsFilePreservingFormat(379      testFilePath,380      migratedSettings,381      true,382    );383 384    expect(result).toBe(true);385    const content = fs.readFileSync(testFilePath, 'utf-8');386 387    // Deprecated nested keys must be removed388    expect(content).not.toContain('disableAutoUpdate');389    expect(content).not.toContain('disableUpdateNag');390    // New keys must be present391    expect(content).toContain('enableAutoUpdate');392    expect(content).toContain('$version');393    // Unrelated keys preserved394    expect(content).toContain('"theme": "dark"');395  });396 397  it('should remove top-level zombie keys in sync mode', () => {398    const original = `{399  "theme": "dark",400  "model": "gemini-2.5-pro",401  "deprecatedKey": "zombie"402}`;403 404    fs.writeFileSync(testFilePath, original, 'utf-8');405 406    const migratedSettings = {407      model: 'gemini-2.5-flash',408      $version: 3,409    };410 411    const result = updateSettingsFilePreservingFormat(412      testFilePath,413      migratedSettings,414      true,415    );416 417    expect(result).toBe(true);418    const content = fs.readFileSync(testFilePath, 'utf-8');419 420    // Top-level zombie removed421    expect(content).not.toContain('theme');422    expect(content).not.toContain('deprecatedKey');423    // Migrated keys present424    expect(content).toContain('"model": "gemini-2.5-flash"');425    expect(content).toContain('$version');426  });427 428  it('should preserve unrelated keys in nested objects during sync', () => {429    // The migrated object represents the full desired state — migrations430    // preserve unrelated keys, so they appear in the migrated output.431    const original = `{432  "general": {433    "disableAutoUpdate": true,434    "someOtherSetting": "keep-me"435  }436}`;437 438    fs.writeFileSync(testFilePath, original, 'utf-8');439 440    // After migration: disableAutoUpdate removed, enableAutoUpdate added,441    // someOtherSetting preserved (migrations carry forward unrelated keys)442    const migratedSettings = {443      general: {444        enableAutoUpdate: false,445        someOtherSetting: 'keep-me',446      },447    };448 449    const result = updateSettingsFilePreservingFormat(450      testFilePath,451      migratedSettings,452      true,453    );454 455    expect(result).toBe(true);456    const content = fs.readFileSync(testFilePath, 'utf-8');457 458    // Deprecated key removed459    expect(content).not.toContain('disableAutoUpdate');460    // New key added461    expect(content).toContain('enableAutoUpdate');462    // Unrelated key in same nested object preserved463    expect(content).toContain('someOtherSetting');464    expect(content).toContain('keep-me');465  });466 467  it('should remove all keys when sync=true with empty updates object', () => {468    // Documents the behavior: sync mode with empty updates wipes all keys.469    // This is intentional for migrations that restructure the entire file.470    const original = `{471  "a": 1,472  "b": { "c": 2 }473}`;474    fs.writeFileSync(testFilePath, original, 'utf-8');475 476    const result = updateSettingsFilePreservingFormat(testFilePath, {}, true);477 478    expect(result).toBe(true);479    const content = fs.readFileSync(testFilePath, 'utf-8');480    expect(content).not.toContain('"a"');481    expect(content).not.toContain('"b"');482    expect(content).not.toContain('"c"');483  });484});485 
basant307/AI_Governance_Project · CoolFace