test/cloud-catalog-filter.test.js
Rama master · réplica de lectura del repositorio original.
import { afterEach, beforeEach, describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { config } from '../src/config.js';
import {
__resetModelCatalogState,
__setModelCatalogDeps,
__waitForModelCatalogSync,
addAccountByKey,
configureBindHost,
getAvailableModelsForAccount,
getDroughtSummary,
isDroughtMode,
isModelBlockedByDrought,
isModelAllowedForAccount,
removeAccount,
setAccountStatus,
trySyncModelCatalog,
} from '../src/auth.js';
import { handleDashboardApi } from '../src/dashboard/api.js';
import { handleModels } from '../src/handlers/models.js';
import {
MODELS,
MODEL_TIER_ACCESS,
filterModelKeysByCloudCatalog,
listModels,
mergeCloudCatalogSnapshot,
mergeCloudModels,
setActiveCloudCatalogAccounts,
} from '../src/models.js';
const ALLOWED_KEY = 'gemini-2.5-flash';
const SECOND_ALLOWED_KEY = 'claude-4-sonnet';
const ACCOUNT_A = 'catalog-account-a';
const ACCOUNT_B = 'catalog-account-b';
const ORIGINAL_ALLOW_NO_AUTH = process.env.DASHBOARD_ALLOW_NO_AUTH;
const ORIGINAL_IGNORE_FILTER = process.env.WINDSURFAPI_IGNORE_CLOUD_FILTER;
const ORIGINAL_SPECIAL_BACKEND = process.env.WINDSURFAPI_SPECIAL_AGENT_BACKEND;
const ORIGINAL_CLI_ENABLED = process.env.DEVIN_CLI_ENABLED;
const ORIGINAL_DASHBOARD_PASSWORD = config.dashboardPassword;
const ORIGINAL_API_KEY = config.apiKey;
const createdAccountIds = [];
function fakeRes() {
return {
statusCode: 0,
body: '',
writeHead(status) { this.statusCode = status; },
end(chunk) { this.body += chunk ? String(chunk) : ''; },
json() { return this.body ? JSON.parse(this.body) : null; },
};
}
function localReq(path) {
return {
url: `/dashboard/api${path}`,
headers: {},
socket: { remoteAddress: '127.0.0.1' },
};
}
function syncAllowed(keys = [ALLOWED_KEY], accountId = ACCOUNT_A) {
setActiveCloudCatalogAccounts([accountId]);
const configs = keys.map((key) => ({ modelUid: MODELS[key].modelUid }));
confirmCloudCatalog(configs, accountId);
}
function cascadeKeys(keys) {
return keys.filter((key) => MODELS[key]?.backend !== 'special_agent');
}
function fullStaticCloudConfigs() {
const seen = new Set();
const configs = [];
for (const model of Object.values(MODELS)) {
if (model?.deprecated || model?.backend === 'special_agent' || typeof model?.modelUid !== 'string') continue;
const normalized = model.modelUid.trim().toLowerCase();
if (!normalized || seen.has(normalized)) continue;
seen.add(normalized);
configs.push({ modelUid: model.modelUid });
}
return configs;
}
function confirmCloudCatalog(configs, accountId) {
// Direct model-layer calls represent two separate sync rounds. Production
// schedules the second round after the confirmation delay.
mergeCloudModels(configs, { accountId });
mergeCloudModels(configs, { accountId });
}
beforeEach(() => {
__resetModelCatalogState();
__setModelCatalogDeps(null);
delete process.env.WINDSURFAPI_IGNORE_CLOUD_FILTER;
delete process.env.WINDSURFAPI_SPECIAL_AGENT_BACKEND;
delete process.env.DEVIN_CLI_ENABLED;
process.env.DASHBOARD_ALLOW_NO_AUTH = '1';
config.dashboardPassword = '';
config.apiKey = '';
configureBindHost('127.0.0.1');
});
afterEach(async () => {
for (const accountId of createdAccountIds.splice(0)) removeAccount(accountId);
await __waitForModelCatalogSync();
__resetModelCatalogState();
__setModelCatalogDeps(null);
if (ORIGINAL_ALLOW_NO_AUTH === undefined) delete process.env.DASHBOARD_ALLOW_NO_AUTH;
else process.env.DASHBOARD_ALLOW_NO_AUTH = ORIGINAL_ALLOW_NO_AUTH;
if (ORIGINAL_IGNORE_FILTER === undefined) delete process.env.WINDSURFAPI_IGNORE_CLOUD_FILTER;
else process.env.WINDSURFAPI_IGNORE_CLOUD_FILTER = ORIGINAL_IGNORE_FILTER;
if (ORIGINAL_SPECIAL_BACKEND === undefined) delete process.env.WINDSURFAPI_SPECIAL_AGENT_BACKEND;
else process.env.WINDSURFAPI_SPECIAL_AGENT_BACKEND = ORIGINAL_SPECIAL_BACKEND;
if (ORIGINAL_CLI_ENABLED === undefined) delete process.env.DEVIN_CLI_ENABLED;
else process.env.DEVIN_CLI_ENABLED = ORIGINAL_CLI_ENABLED;
config.dashboardPassword = ORIGINAL_DASHBOARD_PASSWORD;
config.apiKey = ORIGINAL_API_KEY;
configureBindHost('0.0.0.0');
});
describe('upstream account cloud catalog filtering', () => {
it('limits GET /v1/models and tier routing to catalog-approved Cascade models', () => {
syncAllowed();
const apiKeys = handleModels({}).data.map((model) => model._windsurf_id);
assert.deepEqual(cascadeKeys(apiKeys), [ALLOWED_KEY]);
assert.deepEqual(cascadeKeys(MODEL_TIER_ACCESS.pro), [ALLOWED_KEY]);
assert.deepEqual(cascadeKeys(MODEL_TIER_ACCESS.unknown), [ALLOWED_KEY]);
assert.deepEqual(cascadeKeys(MODEL_TIER_ACCESS.free), [ALLOWED_KEY]);
});
it('makes the upstream account catalog win over entitlement and manual tier overrides', () => {
syncAllowed();
const disallowedKey = 'claude-4-sonnet';
const statusAccount = {
id: ACCOUNT_A,
tier: 'pro',
userStatusLastFetched: Date.now(),
capabilities: {
[ALLOWED_KEY]: { ok: true, reason: 'user_status' },
[disallowedKey]: { ok: true, reason: 'user_status' },
},
};
const manualAccount = { ...statusAccount, tierManual: true };
assert.equal(isModelAllowedForAccount(statusAccount, ALLOWED_KEY), true);
assert.equal(
isModelAllowedForAccount(statusAccount, disallowedKey),
false,
'per-account entitlement must not bypass the upstream account catalog',
);
assert.equal(
isModelAllowedForAccount(manualAccount, disallowedKey),
false,
'manual tier override must not bypass the upstream account catalog',
);
assert.ok(!getAvailableModelsForAccount(statusAccount).includes(disallowedKey));
});
it('uses the same approved catalog for both Dashboard model endpoints', async () => {
syncAllowed();
const modelsRes = fakeRes();
await handleDashboardApi('GET', '/models', {}, localReq('/models'), modelsRes);
assert.equal(modelsRes.statusCode, 200);
assert.deepEqual(
cascadeKeys(modelsRes.json().models.map((model) => model.id)),
[ALLOWED_KEY],
'/dashboard/api/models must not expose models outside the active account catalogs',
);
const tierRes = fakeRes();
await handleDashboardApi('GET', '/tier-access', {}, localReq('/tier-access'), tierRes);
assert.equal(tierRes.statusCode, 200);
assert.deepEqual(cascadeKeys(tierRes.json().allModels), [ALLOWED_KEY]);
});
it('does not expose mutable cloud-catalog state', () => {
syncAllowed();
const snapshot = filterModelKeysByCloudCatalog();
snapshot.length = 0;
assert.deepEqual(
cascadeKeys(listModels().map((model) => model._windsurf_id)),
[ALLOWED_KEY],
'mutating a returned snapshot must not disable the active filter',
);
});
it('fails open before a usable catalog arrives and supports an explicit opt-out', () => {
const baseline = handleModels({}).data.map((model) => model.id);
assert.ok(baseline.length > 100);
setActiveCloudCatalogAccounts([ACCOUNT_A]);
mergeCloudModels([null, {}, { modelUid: 123 }], { accountId: ACCOUNT_A });
assert.deepEqual(handleModels({}).data.map((model) => model.id), baseline);
syncAllowed();
process.env.WINDSURFAPI_IGNORE_CLOUD_FILTER = '1';
assert.deepEqual(handleModels({}).data.map((model) => model.id), baseline);
});
it('keeps the accepted catalog when mergeCloudModels receives a non-array', () => {
// This test used to assert the opposite — that a non-array response CLEARS the
// catalog back to the unfiltered baseline — and its name said "clears a stale
// catalog". That was the defect, not the contract: a malformed body is NO DATA,
// and applying it deleted the account's snapshot so every model the filter
// existed to hide was re-advertised (#231's own symptom) with no confirmation
// round. A truncated / throttled / auth-blipped upstream response is the most
// likely degenerate shape there is, so it was the one unguarded path that got
// taken most. See test/cloud-catalog-degenerate-response.test.js.
const unfiltered = handleModels({}).data.map((model) => model.id);
syncAllowed();
const filtered = handleModels({}).data.map((model) => model.id);
assert.notDeepEqual(filtered, unfiltered, 'precondition: the sync must narrow the view');
mergeCloudModels({ malformed: true }, { accountId: ACCOUNT_A });
assert.deepEqual(
handleModels({}).data.map((model) => model.id), filtered,
'a malformed response must preserve the last-known-good catalog, not widen it back out',
);
});
it('matches cloud model UIDs case-insensitively', () => {
setActiveCloudCatalogAccounts([ACCOUNT_A]);
const configs = [{ modelUid: MODELS[ALLOWED_KEY].modelUid.toLowerCase() }];
confirmCloudCatalog(configs, ACCOUNT_A);
assert.deepEqual(
cascadeKeys(handleModels({}).data.map((model) => model._windsurf_id)),
[ALLOWED_KEY],
);
});
it('keeps enabled special-agent models because Cascade catalog policy does not govern them', () => {
process.env.WINDSURFAPI_SPECIAL_AGENT_BACKEND = 'devin-cli';
syncAllowed();
const models = listModels();
assert.deepEqual(cascadeKeys(models.map((model) => model._windsurf_id)), [ALLOWED_KEY]);
assert.ok(models.some((model) => model._backend === 'special_agent' && model._available));
});
it('keeps a newly discovered cloud model when that same catalog allows it', () => {
const uid = 'MODEL_CLOUD_CATALOG_TEST';
const key = 'model-cloud-catalog-test';
setActiveCloudCatalogAccounts([ACCOUNT_A]);
const configs = [{
modelUid: uid,
provider: 'MODEL_PROVIDER_ANTHROPIC',
creditMultiplier: 2,
}];
confirmCloudCatalog(configs, ACCOUNT_A);
const models = handleModels({}).data;
assert.deepEqual(cascadeKeys(models.map((model) => model._windsurf_id)), [key]);
assert.equal(models[0].owned_by, 'anthropic');
});
it('unions model listings while enforcing each account catalog during routing', () => {
setActiveCloudCatalogAccounts([ACCOUNT_A, ACCOUNT_B]);
const configsA = [{ modelUid: MODELS[ALLOWED_KEY].modelUid }];
const configsB = [{ modelUid: MODELS[SECOND_ALLOWED_KEY].modelUid }];
confirmCloudCatalog(configsA, ACCOUNT_A);
confirmCloudCatalog(configsB, ACCOUNT_B);
assert.deepEqual(
cascadeKeys(handleModels({}).data.map((model) => model._windsurf_id)).sort(),
[ALLOWED_KEY, SECOND_ALLOWED_KEY].sort(),
'the pool catalog should be the union of active account catalogs',
);
const baseAccount = {
tier: 'pro',
tierManual: true,
capabilities: {},
};
const accountA = { ...baseAccount, id: ACCOUNT_A };
const accountB = { ...baseAccount, id: ACCOUNT_B };
assert.equal(isModelAllowedForAccount(accountA, ALLOWED_KEY), true);
assert.equal(isModelAllowedForAccount(accountA, SECOND_ALLOWED_KEY), false);
assert.equal(isModelAllowedForAccount(accountB, ALLOWED_KEY), false);
assert.equal(isModelAllowedForAccount(accountB, SECOND_ALLOWED_KEY), true);
});
it('fails global listings open until every active account has a usable catalog', () => {
const baseline = handleModels({}).data.map((model) => model.id);
setActiveCloudCatalogAccounts([ACCOUNT_A, ACCOUNT_B]);
const configs = [{ modelUid: MODELS[ALLOWED_KEY].modelUid }];
mergeCloudModels(configs, { accountId: ACCOUNT_A });
assert.deepEqual(handleModels({}).data.map((model) => model.id), baseline);
});
it('synchronizes every active account instead of reusing the first catalog', async () => {
const runId = Date.now().toString(36);
const apiKeyA = `catalog-sync-${runId}-a`;
const apiKeyB = `catalog-sync-${runId}-b`;
const requestedKeys = [];
const scheduledRetries = [];
__setModelCatalogDeps({
disableConnectSync: true,
scheduleCatalogRetry: (retry) => {
scheduledRetries.push(retry);
return () => {};
},
getCascadeModelConfigs: async (apiKey) => {
requestedKeys.push(apiKey);
const modelKey = apiKey === apiKeyA ? ALLOWED_KEY : SECOND_ALLOWED_KEY;
return { configs: [{ modelUid: MODELS[modelKey].modelUid }] };
},
});
const accountA = addAccountByKey(apiKeyA, 'catalog-a');
const accountB = addAccountByKey(apiKeyB, 'catalog-b');
createdAccountIds.push(accountA.id, accountB.id);
accountA.tier = 'pro';
accountA.tierManual = true;
accountB.tier = 'pro';
accountB.tierManual = true;
await __waitForModelCatalogSync();
assert.equal(scheduledRetries.length, 2);
for (const retry of scheduledRetries.splice(0)) retry();
await __waitForModelCatalogSync();
assert.deepEqual(new Set(requestedKeys), new Set([apiKeyA, apiKeyB]));
assert.equal(isModelAllowedForAccount(accountA, ALLOWED_KEY), true);
assert.equal(isModelAllowedForAccount(accountA, SECOND_ALLOWED_KEY), false);
assert.equal(isModelAllowedForAccount(accountB, ALLOWED_KEY), false);
assert.equal(isModelAllowedForAccount(accountB, SECOND_ALLOWED_KEY), true);
setAccountStatus(accountB.id, 'disabled');
assert.deepEqual(
cascadeKeys(handleModels({}).data.map((model) => model._windsurf_id)),
[ALLOWED_KEY],
'inactive account catalogs must not remain in the pool listing',
);
});
it('confirms an anomalously small first snapshot only after a delayed sync round', async () => {
const runId = Date.now().toString(36);
const apiKey = `catalog-partial-${runId}`;
let requests = 0;
let scheduledRetry;
let retryDelay;
const partial = [{ modelUid: MODELS[SECOND_ALLOWED_KEY].modelUid }];
__setModelCatalogDeps({
disableConnectSync: true,
scheduleCatalogRetry: (retry, delay) => {
scheduledRetry = retry;
retryDelay = delay;
return () => {};
},
getCascadeModelConfigs: async () => {
requests += 1;
return { configs: partial };
},
});
const account = addAccountByKey(apiKey, 'catalog-partial');
createdAccountIds.push(account.id);
account.tier = 'pro';
account.tierManual = true;
await __waitForModelCatalogSync();
assert.equal(requests, 1, 'one sync round must issue only one catalog request');
assert.equal(retryDelay, 30_000);
assert.equal(typeof scheduledRetry, 'function');
assert.equal(isModelAllowedForAccount(account, ALLOWED_KEY), true);
trySyncModelCatalog();
await __waitForModelCatalogSync();
assert.equal(
requests,
1,
'ordinary sync triggers must not bypass the delayed confirmation round',
);
scheduledRetry();
await __waitForModelCatalogSync();
assert.equal(requests, 2);
assert.equal(isModelAllowedForAccount(account, ALLOWED_KEY), false);
assert.equal(isModelAllowedForAccount(account, SECOND_ALLOWED_KEY), true);
});
it('keeps re-checking a differing candidate, bounded, instead of polling forever', async () => {
// This test used to assert that a differing confirmation schedules NOTHING
// further. That is what wedged the catalog: an upstream returning a small set
// that varies each round never satisfies the identical-repeat check, so the
// candidate was re-quarantined indefinitely while a stale LARGER snapshot
// stayed authoritative — and there is no periodic catalog refresh to recover
// it, only account-lifecycle events. Measured: after a downgrade plus four
// sync cycles the proxy still allowed a model the account had lost.
//
// The re-check is now armed every quarantined round, and models.js bounds the
// quarantine (CLOUD_CATALOG_CONFIRM_MAX_ROUNDS) so it converges on the newest
// snapshot instead of polling forever. Both halves matter: "keeps re-checking"
// AND "is bounded". See test/cloud-catalog-degenerate-response.test.js.
const runId = Date.now().toString(36);
const apiKey = `catalog-changing-${runId}`;
const scheduledRetries = [];
let requests = 0;
__setModelCatalogDeps({
disableConnectSync: true,
scheduleCatalogRetry: (retry) => {
scheduledRetries.push(retry);
return () => {};
},
getCascadeModelConfigs: async () => {
requests += 1;
const modelKey = requests === 1 ? ALLOWED_KEY : SECOND_ALLOWED_KEY;
return { configs: [{ modelUid: MODELS[modelKey].modelUid }] };
},
});
const account = addAccountByKey(apiKey, 'catalog-changing');
createdAccountIds.push(account.id);
account.tier = 'pro';
account.tierManual = true;
await __waitForModelCatalogSync();
assert.equal(scheduledRetries.length, 1);
scheduledRetries.shift()();
await __waitForModelCatalogSync();
assert.equal(requests, 2);
assert.equal(
scheduledRetries.length, 1,
'a differing confirmation must arm the NEXT re-check — dropping it here is what let a '
+ 'small-and-varying upstream wedge the stale snapshot in place permanently',
);
assert.equal(isModelAllowedForAccount(account, ALLOWED_KEY), true);
assert.equal(isModelAllowedForAccount(account, SECOND_ALLOWED_KEY), true);
// Bounded: the re-checks converge rather than continuing indefinitely. Drain
// them and assert the loop terminates well inside a small budget.
let rounds = 2;
while (scheduledRetries.length && rounds < 8) {
scheduledRetries.shift()();
await __waitForModelCatalogSync();
rounds = requests;
}
assert.ok(
scheduledRetries.length === 0,
`the re-check loop did not converge within ${rounds} rounds — it must be bounded, not perpetual`,
);
assert.ok(rounds <= 4, `expected convergence in a few rounds, took ${rounds}`);
setAccountStatus(account.id, 'disabled');
setAccountStatus(account.id, 'active');
await __waitForModelCatalogSync();
assert.equal(requests, rounds + 1);
assert.equal(
scheduledRetries.length,
1,
'reactivation must start a new delayed confirmation after stale pending state is cleared',
);
assert.equal(isModelAllowedForAccount(account, ALLOWED_KEY), true);
assert.equal(isModelAllowedForAccount(account, SECOND_ALLOWED_KEY), true);
});
it('keeps the last accepted snapshot until a large shrink is confirmed', () => {
setActiveCloudCatalogAccounts([ACCOUNT_A]);
mergeCloudModels(fullStaticCloudConfigs(), { accountId: ACCOUNT_A });
const partial = [{ modelUid: MODELS[SECOND_ALLOWED_KEY].modelUid }];
// The first shrink is quarantined and preserves the last-known-good catalog.
const firstAdded = mergeCloudModels(partial, { accountId: ACCOUNT_A });
assert.equal(firstAdded, 0);
assert.ok(
handleModels({}).data.some((model) => model._windsurf_id === ALLOWED_KEY),
'an unconfirmed shrink must preserve the last accepted snapshot',
);
// A matching snapshot from a later sync round confirms the smaller catalog.
mergeCloudModels(partial, { accountId: ACCOUNT_A });
assert.deepEqual(
cascadeKeys(handleModels({}).data.map((model) => model._windsurf_id)),
[SECOND_ALLOWED_KEY],
);
});
it('accepts a confirmed small allowlist and fails drought restriction open when no free model remains', async () => {
const runId = Date.now().toString(36);
const apiKey = `catalog-stable-small-${runId}`;
let requests = 0;
let scheduledRetry;
__setModelCatalogDeps({
disableConnectSync: true,
scheduleCatalogRetry: (retry) => {
scheduledRetry = retry;
return () => {};
},
getCascadeModelConfigs: async () => {
requests += 1;
return { configs: [{ modelUid: MODELS[SECOND_ALLOWED_KEY].modelUid }] };
},
});
const account = addAccountByKey(apiKey, 'catalog-stable-small');
createdAccountIds.push(account.id);
account.tier = 'pro';
account.tierManual = true;
account.credits = { weeklyPercent: 0, dailyPercent: 0 };
await __waitForModelCatalogSync();
scheduledRetry();
await __waitForModelCatalogSync();
assert.equal(requests, 2);
assert.equal(isModelAllowedForAccount(account, SECOND_ALLOWED_KEY), true);
assert.equal(isModelAllowedForAccount(account, ALLOWED_KEY), false);
assert.equal(isDroughtMode(), true);
assert.equal(isModelBlockedByDrought(SECOND_ALLOWED_KEY), false);
const summary = getDroughtSummary();
assert.deepEqual(summary.freeTierModels, []);
assert.equal(summary.restrictionFailOpen, true);
});
it('leaves a malformed catalog response retryable and accepts the next valid response', async () => {
const runId = Date.now().toString(36);
const apiKey = `catalog-retry-${runId}`;
let requests = 0;
__setModelCatalogDeps({
disableConnectSync: true,
getCascadeModelConfigs: async () => {
requests += 1;
if (requests === 1) return {};
return { configs: fullStaticCloudConfigs() };
},
});
const account = addAccountByKey(apiKey, 'catalog-retry');
createdAccountIds.push(account.id);
await __waitForModelCatalogSync();
trySyncModelCatalog();
await __waitForModelCatalogSync();
assert.equal(requests, 2);
assert.equal(isModelAllowedForAccount(account, ALLOWED_KEY), true);
});
it('treats a valid empty catalog as synchronized instead of retrying it', async () => {
const runId = Date.now().toString(36);
const apiKey = `catalog-empty-${runId}`;
let requests = 0;
__setModelCatalogDeps({
disableConnectSync: true,
getCascadeModelConfigs: async () => {
requests += 1;
return { configs: [] };
},
});
const account = addAccountByKey(apiKey, 'catalog-empty');
createdAccountIds.push(account.id);
await __waitForModelCatalogSync();
trySyncModelCatalog();
await __waitForModelCatalogSync();
assert.equal(requests, 1);
});
it('labels a valid empty catalog as no_filter', () => {
const snapshot = mergeCloudCatalogSnapshot([], { accountId: ACCOUNT_A });
assert.equal(snapshot.accepted, true);
assert.equal(snapshot.reason, 'no_filter');
});
});