mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-17 08:17:22 +00:00
fix(persist): bound Codex translator receipt scan
This commit is contained in:
committed by
Tam Nhu Tran
parent
62a4d44b58
commit
5da6334736
@@ -1,5 +1,8 @@
|
||||
export const CODEX_TRANSLATOR_URL_MARKER = '/api/provider/codex';
|
||||
|
||||
const MAX_CODEX_TRANSLATOR_SCAN_DEPTH = 100;
|
||||
const MAX_CODEX_TRANSLATOR_SCAN_NODES = 10000;
|
||||
|
||||
function formatSettingsPathSegment(basePath: string, segment: string | number): string {
|
||||
if (typeof segment === 'number') {
|
||||
return `${basePath}[${segment}]`;
|
||||
@@ -13,23 +16,62 @@ function formatSettingsPathSegment(basePath: string, segment: string | number):
|
||||
}
|
||||
|
||||
export function findCodexTranslatorUrlPaths(value: unknown, path = ''): string[] {
|
||||
if (typeof value === 'string') {
|
||||
return value.includes(CODEX_TRANSLATOR_URL_MARKER) ? [path || '(root)'] : [];
|
||||
const matches: string[] = [];
|
||||
const stack: Array<{ value: unknown; path: string; depth: number }> = [{ value, path, depth: 0 }];
|
||||
const seen = new WeakSet<object>();
|
||||
let visitedNodes = 0;
|
||||
|
||||
while (stack.length > 0 && visitedNodes < MAX_CODEX_TRANSLATOR_SCAN_NODES) {
|
||||
const item = stack.pop();
|
||||
if (!item) {
|
||||
break;
|
||||
}
|
||||
|
||||
visitedNodes += 1;
|
||||
|
||||
if (typeof item.value === 'string') {
|
||||
if (item.value.includes(CODEX_TRANSLATOR_URL_MARKER)) {
|
||||
matches.push(item.path || '(root)');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof item.value !== 'object' || item.value === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (seen.has(item.value)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(item.value);
|
||||
|
||||
if (item.depth >= MAX_CODEX_TRANSLATOR_SCAN_DEPTH) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Array.isArray(item.value)) {
|
||||
for (let index = item.value.length - 1; index >= 0; index -= 1) {
|
||||
stack.push({
|
||||
value: item.value[index],
|
||||
path: formatSettingsPathSegment(item.path, index),
|
||||
depth: item.depth + 1,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const entries = Object.entries(item.value);
|
||||
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
||||
const [key, child] = entries[index];
|
||||
stack.push({
|
||||
value: child,
|
||||
path: formatSettingsPathSegment(item.path, key),
|
||||
depth: item.depth + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.flatMap((item, index) =>
|
||||
findCodexTranslatorUrlPaths(item, formatSettingsPathSegment(path, index))
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
return Object.entries(value).flatMap(([key, item]) =>
|
||||
findCodexTranslatorUrlPaths(item, formatSettingsPathSegment(path, key))
|
||||
);
|
||||
}
|
||||
|
||||
return [];
|
||||
return matches;
|
||||
}
|
||||
|
||||
export function formatSettingsPathList(paths: string[]): string {
|
||||
|
||||
@@ -415,6 +415,43 @@ describe('persist command Claude extension parity', () => {
|
||||
expect(renderedLogs).toContain('Native Codex target: ccsxp or ccs codex --target codex');
|
||||
});
|
||||
|
||||
it('does not fail after writing settings when a cleared env value is deeply nested', async () => {
|
||||
await writeUnifiedConfig();
|
||||
|
||||
const settingsPath = path.join(tempRoot, '.claude', 'settings.json');
|
||||
await fs.promises.mkdir(path.dirname(settingsPath), { recursive: true });
|
||||
const deepValue = `${'{"nested":'.repeat(20000)}"leaf"${'}'.repeat(20000)}`;
|
||||
await fs.promises.writeFile(
|
||||
settingsPath,
|
||||
`{"env":{"KEEP_ME":"still-here","ANTHROPIC_AUTH_TOKEN":${deepValue}}}\n`,
|
||||
'utf8'
|
||||
);
|
||||
|
||||
const originalConsoleLog = console.log;
|
||||
const capturedLogs: string[] = [];
|
||||
console.log = (...args: unknown[]) => {
|
||||
capturedLogs.push(args.map((arg) => String(arg)).join(' '));
|
||||
};
|
||||
|
||||
try {
|
||||
await withScopedHome(() => handlePersistCommand(['default', '--yes']));
|
||||
} finally {
|
||||
console.log = originalConsoleLog;
|
||||
}
|
||||
|
||||
const persisted = JSON.parse(await fs.promises.readFile(settingsPath, 'utf8')) as {
|
||||
env: Record<string, string>;
|
||||
};
|
||||
|
||||
expect(persisted.env.KEEP_ME).toBe('still-here');
|
||||
expect(persisted.env.ANTHROPIC_AUTH_TOKEN).toBeUndefined();
|
||||
const renderedLogs = capturedLogs.join('\n');
|
||||
expect(renderedLogs).toContain("Profile 'default' written to");
|
||||
expect(renderedLogs).toContain('Config Receipt');
|
||||
expect(renderedLogs).toContain('Codex translator URL: not found');
|
||||
expect(renderedLogs).not.toContain('Failed to write settings');
|
||||
});
|
||||
|
||||
it('warns in the persist receipt when a Codex translator URL remains in settings', async () => {
|
||||
await writeUnifiedConfig();
|
||||
|
||||
|
||||
@@ -31,4 +31,14 @@ describe('stale Codex translator settings scanner', () => {
|
||||
'["custom-key"][0]',
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not overflow the stack on deeply nested local settings values', () => {
|
||||
let settings: Record<string, unknown> = { value: 'leaf' };
|
||||
for (let depth = 0; depth < 20000; depth += 1) {
|
||||
settings = { nested: settings };
|
||||
}
|
||||
|
||||
expect(() => findCodexTranslatorUrlPaths(settings)).not.toThrow();
|
||||
expect(findCodexTranslatorUrlPaths(settings)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user