mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 08:15:17 +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:
1 parent
812bb5c0a5
commit
15d6c06dbc
2 files changed
+51
-3
No files matched your search
@@ -386,8 +386,8 @@ export async function listCcsModels(): Promise<Map<string, DroidCustomModel>> {
|
|||||||
* Removes ccs-* entries whose profile no longer exists in active profiles.
|
* Removes ccs-* entries whose profile no longer exists in active profiles.
|
||||||
*/
|
*/
|
||||||
export async function pruneOrphanedModels(activeProfiles: string[]): Promise<number> {
|
export async function pruneOrphanedModels(activeProfiles: string[]): Promise<number> {
|
||||||
// Validate all profile names before pruning
|
// Snapshot at call time so caller-side mutation cannot affect filtering while lock is pending.
|
||||||
activeProfiles.forEach((profile) => validateProfileName(profile));
|
const activeProfilesSnapshot = [...activeProfiles];
|
||||||
|
|
||||||
ensureFactoryDir();
|
ensureFactoryDir();
|
||||||
const settingsPath = getSettingsPath();
|
const settingsPath = getSettingsPath();
|
||||||
@@ -397,6 +397,12 @@ export async function pruneOrphanedModels(activeProfiles: string[]): Promise<num
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
release = await acquireFactoryLock(3);
|
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;
|
if (!fs.existsSync(settingsPath)) return 0;
|
||||||
|
|
||||||
const settings = readDroidSettings();
|
const settings = readDroidSettings();
|
||||||
@@ -406,7 +412,7 @@ export async function pruneOrphanedModels(activeProfiles: string[]): Promise<num
|
|||||||
settings.customModels = settings.customModels.filter((m) => {
|
settings.customModels = settings.customModels.filter((m) => {
|
||||||
const profile = parseManagedProfile(m.displayName);
|
const profile = parseManagedProfile(m.displayName);
|
||||||
if (profile) {
|
if (profile) {
|
||||||
return activeProfiles.includes(profile);
|
return activeProfileSet.has(profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drop malformed managed entries; keep user-managed entries.
|
// Drop malformed managed entries; keep user-managed entries.
|
||||||
|
|||||||
@@ -570,6 +570,48 @@ describe('droid-config-manager', () => {
|
|||||||
expect(settings.customModels).toHaveLength(1);
|
expect(settings.customModels).toHaveLength(1);
|
||||||
expect(settings.customModels[0].displayName).toBe('My GPT');
|
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', () => {
|
describe('concurrent writes', () => {
|
||||||
|
|||||||
Reference in new issue
Block a user