basant307/AI_Governance_Project
048
1/**2 * @license3 * Copyright 2025 Qwen Team4 * SPDX-License-Identifier: Apache-2.05 */6 7import { describe, expect, it, vi } from 'vitest';8import { createLoadedSettingsAdapter } from './loadedSettingsAdapter.js';9import { SettingScope } from './settings.js';10 11// settingsUtils makes real fs calls in backup/restore — stub them out so the12// tests can focus on adapter behavior without touching disk.13vi.mock('../utils/settingsUtils.js', async (importOriginal) => {14 const actual =15 await importOriginal<typeof import('../utils/settingsUtils.js')>();16 return {17 ...actual,18 backupSettingsFile: vi.fn(),19 restoreSettingsFromBackup: vi.fn(),20 cleanupSettingsBackup: vi.fn(),21 };22});23 24// Named shape so dot-access on the known keys (`env`, `modelProviders`) is not25// treated as access through an index signature — keeps the strict TS option26// `noPropertyAccessFromIndexSignature` happy while still allowing arbitrary27// extra keys via the index signature.28interface SettingsShape {29 env?: Record<string, unknown>;30 modelProviders?: Record<string, unknown>;31 [key: string]: unknown;32}33 34interface MutableSettingsFile {35 settings: SettingsShape;36 originalSettings: SettingsShape;37 path: string;38}39 40function makeSettings(initial: SettingsShape = {}) {41 const file: MutableSettingsFile = {42 settings: structuredClone(initial),43 originalSettings: structuredClone(initial),44 path: '/tmp/qwen-test-settings.json',45 };46 const setValue = vi.fn(47 (_scope: SettingScope, key: string, value: unknown) => {48 const parts = key.split('.');49 let current: Record<string, unknown> = file.settings as Record<50 string,51 unknown52 >;53 for (let i = 0; i < parts.length; i++) {54 const part = parts[i]!;55 // Mirror setNestedPropertySafe's reserved-segment rejection. Inline56 // literal === comparisons (rather than e.g. Set.has) are what57 // CodeQL's prototype-pollution sanitiser recognises, so we use them58 // at the only step that actually writes to `current`.59 if (60 part === '__proto__' ||61 part === 'constructor' ||62 part === 'prototype'63 ) {64 throw new Error(`mock setValue refused reserved segment in: ${key}`);65 }66 if (i === parts.length - 1) {67 current[part] = value;68 } else {69 if (!current[part] || typeof current[part] !== 'object') {70 current[part] = {};71 }72 current = current[part] as Record<string, unknown>;73 }74 }75 file.originalSettings = structuredClone(file.settings);76 },77 );78 const recomputeMerged = vi.fn(() => {79 /* merged() is computed lazily via the getter below */80 });81 const settings = {82 get merged() {83 return file.settings;84 },85 forScope: vi.fn(() => file),86 setValue,87 recomputeMerged,88 };89 return { settings, file, setValue, recomputeMerged };90}91 92describe('createLoadedSettingsAdapter', () => {93 it('forwards setValue to LoadedSettings.setValue with the resolved scope', () => {94 const { settings, setValue } = makeSettings();95 const adapter = createLoadedSettingsAdapter(96 settings as never,97 SettingScope.User,98 );99 adapter.setValue('env.MY_KEY', 'val');100 expect(setValue).toHaveBeenCalledWith(101 SettingScope.User,102 'env.MY_KEY',103 'val',104 );105 });106 107 it('rejects prototype-pollution keys before reaching LoadedSettings', () => {108 const { settings, setValue } = makeSettings();109 const adapter = createLoadedSettingsAdapter(110 settings as never,111 SettingScope.User,112 );113 expect(() => adapter.setValue('__proto__.polluted', 'x')).toThrow(114 /reserved segment/,115 );116 expect(() => adapter.setValue('foo.constructor.bar', 'x')).toThrow(117 /reserved segment/,118 );119 expect(() => adapter.setValue('prototype.x', 'x')).toThrow(120 /reserved segment/,121 );122 // The guard short-circuits before delegating to LoadedSettings — that's the123 // contract this test exists to lock in.124 expect(setValue).not.toHaveBeenCalled();125 });126 127 it('getValue reads from settings.merged via dotted key', () => {128 const { settings } = makeSettings({129 env: { MY_KEY: 'from-merged' },130 modelProviders: { openai: [{ id: 'gpt' }] },131 });132 const adapter = createLoadedSettingsAdapter(133 settings as never,134 SettingScope.User,135 );136 expect(adapter.getValue('env.MY_KEY')).toBe('from-merged');137 expect(adapter.getValue('modelProviders.openai')).toEqual([{ id: 'gpt' }]);138 expect(adapter.getValue('missing.path')).toBeUndefined();139 });140 141 it('backup() snapshots in-memory state; restore() reverts and recomputes merged', () => {142 const { settings, file, recomputeMerged } = makeSettings({143 env: { ORIGINAL: '1' },144 });145 const adapter = createLoadedSettingsAdapter(146 settings as never,147 SettingScope.User,148 );149 150 // backup/restore/cleanupBackup are optional in the contract, but151 // createLoadedSettingsAdapter always installs them — assert + use !.152 expect(adapter.backup).toBeTypeOf('function');153 adapter.backup!();154 155 // Simulate mutations that would happen during an install plan apply.156 adapter.setValue('env.NEW_KEY', 'new-value');157 expect(file.settings.env).toEqual({158 ORIGINAL: '1',159 NEW_KEY: 'new-value',160 });161 162 expect(adapter.restore).toBeTypeOf('function');163 adapter.restore!();164 165 expect(file.settings).toEqual({ env: { ORIGINAL: '1' } });166 expect(file.originalSettings).toEqual({ env: { ORIGINAL: '1' } });167 expect(recomputeMerged).toHaveBeenCalled();168 });169 170 it('cleanupBackup() clears the in-memory snapshot so a later restore is a no-op', () => {171 const { settings, file } = makeSettings({ env: { K: 'v1' } });172 const adapter = createLoadedSettingsAdapter(173 settings as never,174 SettingScope.User,175 );176 expect(adapter.backup).toBeTypeOf('function');177 adapter.backup!();178 adapter.setValue('env.K', 'v2');179 expect(adapter.cleanupBackup).toBeTypeOf('function');180 adapter.cleanupBackup!();181 // restore after cleanup should not bring v1 back182 expect(adapter.restore).toBeTypeOf('function');183 adapter.restore!();184 expect(file.settings.env).toEqual({ K: 'v2' });185 });186});187 