mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 04:14:57 +00:00
fix(persist): bound Codex translator receipt scan
This commit is contained in:
1 parent
62a4d44b58
commit
5da6334736
3 files changed
+104
-15
No files matched your search
@@ -1,5 +1,8 @@
|
|||||||
export const CODEX_TRANSLATOR_URL_MARKER = '/api/provider/codex';
|
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 {
|
function formatSettingsPathSegment(basePath: string, segment: string | number): string {
|
||||||
if (typeof segment === 'number') {
|
if (typeof segment === 'number') {
|
||||||
return `${basePath}[${segment}]`;
|
return `${basePath}[${segment}]`;
|
||||||
@@ -13,23 +16,62 @@ function formatSettingsPathSegment(basePath: string, segment: string | number):
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function findCodexTranslatorUrlPaths(value: unknown, path = ''): string[] {
|
export function findCodexTranslatorUrlPaths(value: unknown, path = ''): string[] {
|
||||||
if (typeof value === 'string') {
|
const matches: string[] = [];
|
||||||
return value.includes(CODEX_TRANSLATOR_URL_MARKER) ? [path || '(root)'] : [];
|
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 matches;
|
||||||
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 [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatSettingsPathList(paths: string[]): string {
|
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');
|
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 () => {
|
it('warns in the persist receipt when a Codex translator URL remains in settings', async () => {
|
||||||
await writeUnifiedConfig();
|
await writeUnifiedConfig();
|
||||||
|
|
||||||
|
|||||||
@@ -31,4 +31,14 @@ describe('stale Codex translator settings scanner', () => {
|
|||||||
'["custom-key"][0]',
|
'["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