mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 04:18:05 +00:00
fix(targets): snapshot active profiles during droid prune
- copy activeProfiles at call time before lock acquisition - validate profile names inside locked section - add tests for invalid names and post-call array mutation
This commit is contained in:
@@ -386,8 +386,8 @@ export async function listCcsModels(): Promise<Map<string, DroidCustomModel>> {
|
||||
* Removes ccs-* entries whose profile no longer exists in active profiles.
|
||||
*/
|
||||
export async function pruneOrphanedModels(activeProfiles: string[]): Promise<number> {
|
||||
// Validate all profile names before pruning
|
||||
activeProfiles.forEach((profile) => validateProfileName(profile));
|
||||
// Snapshot at call time so caller-side mutation cannot affect filtering while lock is pending.
|
||||
const activeProfilesSnapshot = [...activeProfiles];
|
||||
|
||||
ensureFactoryDir();
|
||||
const settingsPath = getSettingsPath();
|
||||
@@ -397,6 +397,12 @@ export async function pruneOrphanedModels(activeProfiles: string[]): Promise<num
|
||||
|
||||
try {
|
||||
release = await acquireFactoryLock(3);
|
||||
const activeProfileSet = new Set<string>();
|
||||
for (const profile of activeProfilesSnapshot) {
|
||||
validateProfileName(profile);
|
||||
activeProfileSet.add(profile);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(settingsPath)) return 0;
|
||||
|
||||
const settings = readDroidSettings();
|
||||
@@ -406,7 +412,7 @@ export async function pruneOrphanedModels(activeProfiles: string[]): Promise<num
|
||||
settings.customModels = settings.customModels.filter((m) => {
|
||||
const profile = parseManagedProfile(m.displayName);
|
||||
if (profile) {
|
||||
return activeProfiles.includes(profile);
|
||||
return activeProfileSet.has(profile);
|
||||
}
|
||||
|
||||
// Drop malformed managed entries; keep user-managed entries.
|
||||
|
||||
@@ -570,6 +570,48 @@ describe('droid-config-manager', () => {
|
||||
expect(settings.customModels).toHaveLength(1);
|
||||
expect(settings.customModels[0].displayName).toBe('My GPT');
|
||||
});
|
||||
|
||||
it('should reject invalid active profile names', async () => {
|
||||
await expect(pruneOrphanedModels(['bad profile'])).rejects.toThrow(/Invalid profile name/);
|
||||
});
|
||||
|
||||
it('should use active profile snapshot taken at call time', async () => {
|
||||
const factoryDir = path.join(tmpDir, '.factory');
|
||||
fs.mkdirSync(factoryDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(factoryDir, 'settings.json'),
|
||||
JSON.stringify({
|
||||
customModels: [
|
||||
{
|
||||
model: 'opus',
|
||||
displayName: 'CCS gemini',
|
||||
baseUrl: 'x',
|
||||
apiKey: 'y',
|
||||
provider: 'anthropic',
|
||||
},
|
||||
{
|
||||
model: 'sonnet',
|
||||
displayName: 'CCS codex',
|
||||
baseUrl: 'x',
|
||||
apiKey: 'y',
|
||||
provider: 'anthropic',
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
const activeProfiles = ['gemini'];
|
||||
const prunePromise = pruneOrphanedModels(activeProfiles);
|
||||
activeProfiles.push('codex'); // Mutation after call should not affect in-flight prune decision.
|
||||
|
||||
const removed = await prunePromise;
|
||||
expect(removed).toBe(1);
|
||||
|
||||
const models = await listCcsModels();
|
||||
expect(models.size).toBe(1);
|
||||
expect(models.has('gemini')).toBe(true);
|
||||
expect(models.has('codex')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('concurrent writes', () => {
|
||||
|
||||
Reference in New Issue
Block a user