1096 lines
61 KiB
JavaScript
1096 lines
61 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { spawn } from 'node:child_process';
|
|
import { createDecipheriv, createHash } from 'node:crypto';
|
|
import { createServer } from 'node:http';
|
|
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
import { fileURLToPath } from 'node:url';
|
|
import test from 'node:test';
|
|
|
|
const serverScript = fileURLToPath(new URL('./index.mjs', import.meta.url));
|
|
|
|
function token(accountId, identity = accountId, expiresIn = 3600, { email, userId, plan = 'team' } = {}) {
|
|
const header = Buffer.from(JSON.stringify({ alg: 'none' })).toString('base64url');
|
|
const payload = Buffer.from(JSON.stringify({
|
|
...(expiresIn == null ? {} : { exp: Math.floor(Date.now() / 1000) + expiresIn }),
|
|
sub: identity,
|
|
'https://api.openai.com/auth': { chatgpt_account_id: accountId, chatgpt_plan_type: plan, ...(userId ? { chatgpt_user_id: userId } : {}) },
|
|
...(email ? { 'https://api.openai.com/profile': { email } } : {}),
|
|
})).toString('base64url');
|
|
return `${header}.${payload}.test-signature`;
|
|
}
|
|
|
|
function requestIdentity(req) {
|
|
const bearer = String(req.headers.authorization || '').replace(/^Bearer\s+/i, '');
|
|
try {
|
|
return JSON.parse(Buffer.from(bearer.split('.')[1], 'base64url').toString()).sub || '';
|
|
} catch {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function usageKey(workspace, identity) {
|
|
return `${workspace}:${identity}`;
|
|
}
|
|
|
|
async function listen(server) {
|
|
await new Promise((resolve, reject) => {
|
|
server.once('error', reject);
|
|
server.listen(0, '127.0.0.1', resolve);
|
|
});
|
|
return server.address().port;
|
|
}
|
|
|
|
async function unusedPort() {
|
|
const server = createServer();
|
|
const port = await listen(server);
|
|
await new Promise((resolve) => server.close(resolve));
|
|
return port;
|
|
}
|
|
|
|
async function waitForApp(baseUrl, child, output) {
|
|
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
if (child.exitCode !== null) throw new Error(`test server exited early: ${output()}`);
|
|
try {
|
|
const response = await fetch(`${baseUrl}/api/auth/session`, { signal: AbortSignal.timeout(500) });
|
|
if (response.ok) return;
|
|
} catch {
|
|
// Wait for the isolated server to bind its local port.
|
|
}
|
|
await delay(50);
|
|
}
|
|
throw new Error(`test server did not start: ${output()}`);
|
|
}
|
|
|
|
async function startQuotaApp(t, {
|
|
teams,
|
|
children,
|
|
usage,
|
|
initialState = {},
|
|
refreshResponses = null,
|
|
sessionResponses = {},
|
|
}) {
|
|
const requests = [];
|
|
const upstream = createServer(async (req, res) => {
|
|
const url = new URL(req.url || '/', 'http://127.0.0.1');
|
|
const accountId = req.headers['chatgpt-account-id'] || '';
|
|
const identity = requestIdentity(req);
|
|
const request = { method: req.method, pathname: url.pathname, accountId, identity };
|
|
requests.push(request);
|
|
let status = 200;
|
|
let payload = {};
|
|
const sourceTeam = Array.isArray(teams)
|
|
? teams.find((item) => item.accountId === accountId || item.team === accountId)
|
|
: teams[accountId];
|
|
const members = sourceTeam?.members || [];
|
|
if (req.method === 'POST' && url.pathname === '/mock-oauth/token') {
|
|
const chunks = [];
|
|
for await (const chunk of req) chunks.push(chunk);
|
|
const form = new URLSearchParams(Buffer.concat(chunks).toString());
|
|
request.grantType = form.get('grant_type');
|
|
request.refreshToken = form.get('refresh_token');
|
|
request.authorizationCode = form.get('code');
|
|
const mock = refreshResponses?.[request.refreshToken || request.authorizationCode];
|
|
status = mock?.status || 400;
|
|
payload = mock?.payload || { error: 'test_oauth_response_not_configured' };
|
|
} else if (req.method === 'GET' && url.pathname === '/backend-api/subscriptions') {
|
|
payload = { seats_in_use: members.length, seats_entitled: 2, seat_capacity: [{ type: 'default', paid: 2, held: 0 }], assigned: { default: members.length } };
|
|
} else if (req.method === 'GET' && url.pathname === `/backend-api/accounts/${accountId}/users`) {
|
|
const offset = Number(url.searchParams.get('offset')) || 0;
|
|
const limit = Number(url.searchParams.get('limit')) || 100;
|
|
payload = { items: members.slice(offset, offset + limit), total: members.length, offset, limit };
|
|
} else if (req.method === 'GET' && url.pathname === '/backend-api/wham/usage') {
|
|
const current = usage[usageKey(accountId, identity)] || usage[accountId] || usage.default;
|
|
status = current?.status || 200;
|
|
payload = current?.payload || {};
|
|
} else if (req.method === 'GET' && url.pathname === '/api/auth/session') {
|
|
const current = sessionResponses[identity];
|
|
status = current?.status || 503;
|
|
payload = current?.payload || { error: 'test_workspace_session_not_configured' };
|
|
} else if (req.method === 'POST' && url.pathname === '/backend-api/codex/responses') {
|
|
payload = { status: 'active' };
|
|
} else {
|
|
status = 503;
|
|
payload = { message: `unexpected_mock_request:${req.method}:${url.pathname}` };
|
|
}
|
|
res.writeHead(status, { 'content-type': 'application/json' });
|
|
res.end(JSON.stringify(payload));
|
|
});
|
|
const upstreamPort = await listen(upstream);
|
|
const dataRoot = path.resolve(tmpdir());
|
|
const dataDir = await mkdtemp(path.join(dataRoot, 'quota-logic-integration-'));
|
|
const state = {
|
|
version: 1,
|
|
settings: {
|
|
autoRefill: false,
|
|
promoteJoinedAccounts: false,
|
|
delayedKickEnabled: false,
|
|
...initialState.settings,
|
|
},
|
|
mothers: teams,
|
|
children,
|
|
history: [],
|
|
...initialState,
|
|
};
|
|
await writeFile(path.join(dataDir, 'state.json'), JSON.stringify(state));
|
|
const port = await unusedPort();
|
|
const baseUrl = `http://127.0.0.1:${port}`;
|
|
let output = '';
|
|
const spawnArgs = [];
|
|
if (refreshResponses) {
|
|
const preload = `
|
|
const nativeFetch = globalThis.fetch;
|
|
globalThis.fetch = async (input, options) => {
|
|
const url = new URL(typeof input === 'string' || input instanceof URL ? String(input) : input.url);
|
|
if (url.hostname === 'auth.openai.com' && ['/api/oauth/oauth2/token', '/oauth/token'].includes(url.pathname)) {
|
|
return nativeFetch(${JSON.stringify(`http://127.0.0.1:${upstreamPort}/mock-oauth/token`)}, options);
|
|
}
|
|
if (url.hostname !== '127.0.0.1') throw new Error('isolated_test_external_network_disabled');
|
|
return nativeFetch(input, options);
|
|
};
|
|
`;
|
|
spawnArgs.push('--import', `data:text/javascript,${encodeURIComponent(preload)}`);
|
|
}
|
|
spawnArgs.push(serverScript);
|
|
const app = spawn(process.execPath, spawnArgs, {
|
|
env: {
|
|
...process.env,
|
|
HOST: '127.0.0.1',
|
|
PORT: String(port),
|
|
CHATGPT_BASE_URL: `http://127.0.0.1:${upstreamPort}`,
|
|
TEAM_ROTATION_DATA_DIR: dataDir,
|
|
TEAM_ROTATION_DATA_KEY: 'quota-logic-test-only-key',
|
|
TEAM_ROTATION_API_TOKEN: '',
|
|
TEAM_ROTATION_LOGIN_PASSWORD: 'daixuteam',
|
|
DISABLE_MAINTENANCE: 'true',
|
|
OPENAI_CALLBACK_ENABLED: 'false',
|
|
OPENAI_REQUEST_TIMEOUT_MS: '2000',
|
|
},
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
windowsHide: true,
|
|
});
|
|
app.stdout.on('data', (chunk) => { output += chunk.toString(); });
|
|
app.stderr.on('data', (chunk) => { output += chunk.toString(); });
|
|
t.after(async () => {
|
|
if (app.exitCode === null && app.signalCode === null) {
|
|
const exited = new Promise((resolve) => app.once('exit', resolve));
|
|
app.kill();
|
|
await exited;
|
|
}
|
|
upstream.closeAllConnections?.();
|
|
await new Promise((resolve) => upstream.close(resolve));
|
|
await rm(dataDir, { recursive: true, force: true });
|
|
});
|
|
await waitForApp(baseUrl, app, () => output);
|
|
const login = await fetch(`${baseUrl}/api/auth/login`, {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({ password: 'daixuteam' }),
|
|
signal: AbortSignal.timeout(5000),
|
|
});
|
|
assert.equal(login.status, 200, await login.text());
|
|
const cookie = login.headers.get('set-cookie')?.split(';', 1)[0];
|
|
assert.match(cookie || '', /^team_rotation_session=/);
|
|
async function rawPost(endpoint, body) {
|
|
const response = await fetch(`${baseUrl}${endpoint}`, {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json', cookie },
|
|
body: JSON.stringify(body),
|
|
signal: AbortSignal.timeout(10_000),
|
|
});
|
|
const data = await response.json();
|
|
return { status: response.status, ok: response.ok, data };
|
|
}
|
|
async function post(endpoint, body) {
|
|
const result = await rawPost(endpoint, body);
|
|
assert.ok(result.ok, JSON.stringify(result.data));
|
|
return result.data;
|
|
}
|
|
async function check(motherId, childIds) {
|
|
return post('/api/maintenance/check', { motherId, ...(childIds === undefined ? {} : { childIds }) });
|
|
}
|
|
async function publicState() {
|
|
const response = await fetch(`${baseUrl}/api/state`, { headers: { cookie }, signal: AbortSignal.timeout(5000) });
|
|
assert.equal(response.status, 200);
|
|
return response.json();
|
|
}
|
|
async function persistedState() {
|
|
const saved = JSON.parse(await readFile(path.join(dataDir, 'state.json'), 'utf8'));
|
|
if (!saved.encrypted) return saved;
|
|
const key = createHash('sha256').update('quota-logic-test-only-key').digest();
|
|
const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(saved.iv, 'base64'));
|
|
decipher.setAuthTag(Buffer.from(saved.tag, 'base64'));
|
|
return JSON.parse(Buffer.concat([decipher.update(Buffer.from(saved.ciphertext, 'base64')), decipher.final()]).toString());
|
|
}
|
|
return { check, post, rawPost, publicState, persistedState, requests, usage, refreshResponses };
|
|
}
|
|
|
|
function team(id, ownerEmail, members) {
|
|
return {
|
|
id: `mother-${id}`,
|
|
team: id,
|
|
accountId: id,
|
|
teamName: id,
|
|
email: ownerEmail,
|
|
primaryOwnerEmail: ownerEmail,
|
|
accessToken: token(id, `owner:${ownerEmail}`),
|
|
ownerAccounts: [],
|
|
members,
|
|
};
|
|
}
|
|
|
|
function member(id, email, role = 'standard-user') {
|
|
return { id, accountUserId: `${id}__member`, email, role, seat_type: 'default', createdTime: '2026-10-01T00:00:00.000Z' };
|
|
}
|
|
|
|
function child(id, email, memberships) {
|
|
const workspaceTokens = Object.fromEntries(memberships.map((entry) => [entry.team, { accessToken: token(entry.team, `child:${id}`) }]));
|
|
return {
|
|
id,
|
|
email,
|
|
status: 'active',
|
|
team: memberships[0]?.team || null,
|
|
accessToken: token(`free-${id}`, `child:${id}`),
|
|
workspaceTokens,
|
|
workspaceHistory: memberships.map((entry) => ({ status: 'active', ...entry, quota5h: entry.quota5h ?? null, quota7d: entry.quota7d ?? null, quotaSnapshot: entry.quotaSnapshot || null })),
|
|
};
|
|
}
|
|
|
|
function currentAccount(state, workspace, email) {
|
|
const account = state.teams.find((item) => item.id === `mother-${workspace}`)?.currentAccounts
|
|
.find((item) => item.email === email);
|
|
assert.ok(account, `current Team account not found: ${workspace}/${email}`);
|
|
return account;
|
|
}
|
|
|
|
function snapshot(used5h, used7d, extra = {}) {
|
|
return {
|
|
primary: { usedPercent: used5h, windowMinutes: 300 },
|
|
secondary: { usedPercent: used7d, windowMinutes: 10080 },
|
|
quotaWindowMapping: 'duration-v1',
|
|
source: 'wham/usage',
|
|
updatedAt: '2026-10-01T00:00:00.000Z',
|
|
...extra,
|
|
};
|
|
}
|
|
|
|
test('quota windows are mapped by duration and preserve legal 0/100 values', { timeout: 25_000 }, async (t) => {
|
|
const workspace = 'quota-window-workspace';
|
|
const ownerEmail = 'owner@quota-window.test';
|
|
const childEmail = 'member@quota-window.test';
|
|
const app = await startQuotaApp(t, {
|
|
usage: {
|
|
[workspace]: {
|
|
payload: { rate_limit: {
|
|
// Deliberately swap the upstream order: primary is the 7d window.
|
|
primary_window: { used_percent: 100, limit_window_seconds: 604800 },
|
|
secondary_window: { used_percent: 0, limit_window_seconds: 18000 },
|
|
} },
|
|
},
|
|
},
|
|
teams: [team(workspace, ownerEmail, [member('owner-member', ownerEmail, 'account-owner'), member('child-member', childEmail)])],
|
|
children: [child('child-window', childEmail, [{ team: workspace, memberId: 'child-member' }])],
|
|
});
|
|
await app.check('mother-' + workspace);
|
|
const state = await app.publicState();
|
|
const current = state.teams[0].currentAccounts.find((account) => account.email === childEmail);
|
|
assert.ok(current);
|
|
assert.equal(current.quota5h, 100, 'the 18,000-second window is the 5h column');
|
|
assert.equal(current.quota7d, 0, 'the 604,800-second window is the 7d column');
|
|
});
|
|
|
|
test('quota failures retain Team history, while successful missing windows clear stale values and remain isolated', { timeout: 30_000 }, async (t) => {
|
|
const teamA = 'quota-isolation-a';
|
|
const teamB = 'quota-isolation-b';
|
|
const sharedEmail = 'shared@quota-isolation.test';
|
|
const ownerA = 'owner-a@quota-isolation.test';
|
|
const ownerB = 'owner-b@quota-isolation.test';
|
|
const teams = [
|
|
team(teamA, ownerA, [member('owner-a', ownerA, 'account-owner'), member('shared-a', sharedEmail)]),
|
|
team(teamB, ownerB, [member('owner-b', ownerB, 'account-owner'), member('shared-b', sharedEmail)]),
|
|
];
|
|
const shared = child('shared-quota-child', sharedEmail, [
|
|
{ team: teamA, memberId: 'shared-a', quota5h: 55, quota7d: 66, quotaSnapshot: { primary: { usedPercent: 45 }, secondary: { usedPercent: 34 } } },
|
|
{ team: teamB, memberId: 'shared-b', quota5h: 11, quota7d: 22, quotaSnapshot: { primary: { usedPercent: 89 }, secondary: { usedPercent: 78 } } },
|
|
]);
|
|
const app = await startQuotaApp(t, {
|
|
usage: {
|
|
[teamA]: { payload: { rate_limit: { primary_window: { used_percent: 20, limit_window_seconds: 18000 } } } },
|
|
[teamB]: { payload: { rate_limit: { primary_window: { used_percent: 60, limit_window_seconds: 18000 }, secondary_window: { used_percent: 10, limit_window_seconds: 604800 } } } },
|
|
},
|
|
teams,
|
|
children: [shared],
|
|
});
|
|
await app.check('mother-' + teamA);
|
|
await app.check('mother-' + teamB);
|
|
let state = await app.publicState();
|
|
let rowA = state.teams.find((item) => item.id === 'mother-' + teamA).currentAccounts.find((account) => account.email === sharedEmail);
|
|
let rowB = state.teams.find((item) => item.id === 'mother-' + teamB).currentAccounts.find((account) => account.email === sharedEmail);
|
|
const ownerRowA = state.teams.find((item) => item.id === 'mother-' + teamA).currentAccounts.find((account) => account.email === ownerA);
|
|
const ownerRowB = state.teams.find((item) => item.id === 'mother-' + teamB).currentAccounts.find((account) => account.email === ownerB);
|
|
assert.equal(rowA.quota5h, 80);
|
|
assert.equal(rowA.quota7d, null, 'successful response missing 7d clears only Team A 7d');
|
|
assert.equal(rowB.quota5h, 40);
|
|
assert.equal(rowB.quota7d, 90);
|
|
assert.equal(ownerRowA?.quota5h, 80, 'Team A owner quota comes from Team A probe');
|
|
assert.equal(ownerRowA?.quota7d, null);
|
|
assert.equal(ownerRowB?.quota5h, 40, 'Team B owner quota stays isolated');
|
|
assert.equal(ownerRowB?.quota7d, 90);
|
|
|
|
// A failed response must keep both previously measured Team B windows.
|
|
app.usage[teamB].status = 503;
|
|
app.usage[teamB].payload = { error: 'temporary_failure' };
|
|
await app.check('mother-' + teamB);
|
|
state = await app.publicState();
|
|
rowB = state.teams.find((item) => item.id === 'mother-' + teamB).currentAccounts.find((account) => account.email === sharedEmail);
|
|
assert.equal(rowB.quota5h, 40, 'failed Team B probe keeps its 5h history');
|
|
assert.equal(rowB.quota7d, 90, 'failed Team B probe keeps its 7d history');
|
|
});
|
|
|
|
test('a primary weekly window clears stale 5h values, reset data, and exported metadata', { timeout: 25_000 }, async (t) => {
|
|
const workspace = 'weekly-only-workspace';
|
|
const ownerEmail = 'owner@weekly-only.test';
|
|
const childEmail = 'member@weekly-only.test';
|
|
const stale = {
|
|
quota5h: 9,
|
|
quota7d: 33,
|
|
quota5hResetAfterSeconds: 500,
|
|
quota5hResetAt: '2026-10-01T01:00:00.000Z',
|
|
quotaSnapshot: snapshot(91, 67),
|
|
extra: { codex_5h_used_percent: 91, codex_5h_reset_after_seconds: 500, codex_5h_reset_at: '2026-10-01T01:00:00.000Z', codex_5h_window_minutes: 300, codex_primary_used_percent: 91, unrelated: 'preserved' },
|
|
};
|
|
const mother = { ...team(workspace, ownerEmail, [member('weekly-owner', ownerEmail, 'account-owner'), member('weekly-child', childEmail)]), ...stale };
|
|
const account = { ...child('weekly-child-local', childEmail, [{ team: workspace, memberId: 'weekly-child', ...stale }]), ...stale };
|
|
const app = await startQuotaApp(t, {
|
|
teams: [mother],
|
|
children: [account],
|
|
usage: { [workspace]: { payload: { rate_limit: { primary_window: { used_percent: 22, limit_window_seconds: 604800, reset_after_seconds: 4000 } } } } },
|
|
});
|
|
await app.check(mother.id);
|
|
let state = await app.publicState();
|
|
for (const email of [ownerEmail, childEmail]) {
|
|
const row = currentAccount(state, workspace, email);
|
|
assert.equal(row.quota5h, null, `${email} has no measured 5h window`);
|
|
assert.equal(row.quota7d, 78, `${email} weekly window is displayed in 7d`);
|
|
assert.equal(row.quota5hResetAfterSeconds, null, `${email} stale 5h reset seconds are cleared`);
|
|
assert.equal(row.quota5hResetAt, null, `${email} stale 5h reset timestamp is cleared`);
|
|
assert.equal(row.quotaSnapshot.primary.usedPercent, null);
|
|
assert.equal(row.quotaSnapshot.secondary.windowMinutes, 10080);
|
|
}
|
|
const exported = await app.post('/api/sub2api/team-export', { motherIds: [mother.id] });
|
|
assert.equal(exported.accounts.length, 2);
|
|
for (const entry of exported.accounts) {
|
|
assert.equal(entry.extra.codex_7d_used_percent, 22);
|
|
assert.equal(entry.extra.unrelated, 'preserved');
|
|
assert.deepEqual(Object.keys(entry.extra).filter((key) => /^codex_(5h|primary)_/.test(key)), [], 'absent 5h window cannot leak from old export metadata');
|
|
}
|
|
|
|
// An unknown-duration primary must not be guessed as 5h when the other
|
|
// window is explicitly weekly. Positional compatibility is for legacy
|
|
// responses that have no duration information for either window.
|
|
app.usage[workspace].payload = { rate_limit: {
|
|
primary_window: { used_percent: 17 },
|
|
secondary_window: { used_percent: 22, limit_window_seconds: 604800 },
|
|
} };
|
|
await app.check(mother.id);
|
|
state = await app.publicState();
|
|
assert.equal(currentAccount(state, workspace, childEmail).quota5h, null);
|
|
assert.equal(currentAccount(state, workspace, childEmail).quota7d, 78);
|
|
|
|
app.usage[workspace].payload = { rate_limit: { primary_window: { used_percent: 17 }, secondary_window: { used_percent: 22 } } };
|
|
await app.check(mother.id);
|
|
state = await app.publicState();
|
|
assert.equal(currentAccount(state, workspace, childEmail).quota5h, 83, 'legacy windows without duration retain positional compatibility');
|
|
assert.equal(currentAccount(state, workspace, childEmail).quota7d, 78);
|
|
});
|
|
|
|
test('null, empty, whitespace, and invalid usage values remain unknown rather than becoming zero usage', { timeout: 25_000 }, async (t) => {
|
|
const workspace = 'unknown-usage-workspace';
|
|
const ownerEmail = 'owner@unknown-usage.test';
|
|
const cases = [
|
|
['null', null],
|
|
['empty', ''],
|
|
['whitespace', ' '],
|
|
['invalid-string', 'not-a-number'],
|
|
['boolean', false],
|
|
['array', []],
|
|
['object', {}],
|
|
];
|
|
const children = cases.map(([name]) => child(`invalid-${name}`, `${name}@unknown-usage.test`, [{ team: workspace, memberId: `member-${name}`, quota5h: 42, quota7d: 24, quotaSnapshot: snapshot(58, 76) }]));
|
|
const usage = {
|
|
[workspace]: { payload: { rate_limit: { primary_window: { used_percent: 50, limit_window_seconds: 18000 }, secondary_window: { used_percent: 50, limit_window_seconds: 604800 } } } },
|
|
};
|
|
cases.forEach(([name, value]) => {
|
|
usage[usageKey(workspace, `child:invalid-${name}`)] = { payload: { rate_limit: {
|
|
primary_window: { used_percent: value, limit_window_seconds: 18000 },
|
|
secondary_window: { used_percent: value, limit_window_seconds: 604800 },
|
|
} } };
|
|
});
|
|
const app = await startQuotaApp(t, {
|
|
teams: [team(workspace, ownerEmail, [member('unknown-owner', ownerEmail, 'account-owner'), ...children.map((item, index) => member(`member-${cases[index][0]}`, item.email))])],
|
|
children,
|
|
usage,
|
|
});
|
|
await app.check(`mother-${workspace}`);
|
|
const state = await app.publicState();
|
|
for (const [name] of cases) {
|
|
const row = currentAccount(state, workspace, `${name}@unknown-usage.test`);
|
|
assert.equal(row.quota5h, null, `${name} must not become a 100% remaining 5h quota`);
|
|
assert.equal(row.quota7d, null, `${name} must not become a 100% remaining 7d quota`);
|
|
assert.equal(row.quotaSnapshot.primary.usedPercent, null);
|
|
assert.equal(row.quotaSnapshot.secondary.usedPercent, null);
|
|
}
|
|
});
|
|
|
|
test('identity-only local owner membership may display that same owner current-Team measurement', { timeout: 25_000 }, async (t) => {
|
|
const workspace = 'owner-identity-workspace';
|
|
const ownerEmail = 'owner@owner-identity.test';
|
|
const mother = {
|
|
...team(workspace, ownerEmail, [member('identity-owner', ownerEmail, 'account-owner')]),
|
|
quota5h: 83,
|
|
quota7d: 65,
|
|
quotaSnapshot: snapshot(17, 35),
|
|
quota5hResetAfterSeconds: 1234,
|
|
quota5hResetAt: '2026-10-08T02:00:00.000Z',
|
|
lastProbe: { ok: true, status: 200, ...snapshot(17, 35) },
|
|
};
|
|
mother.ownerAccounts = [{ email: ownerEmail, accessToken: token(workspace, `owner:${ownerEmail}`) }];
|
|
const app = await startQuotaApp(t, {
|
|
teams: [mother],
|
|
children: [child('identity-owner-local', ownerEmail, [{ team: workspace, memberId: 'identity-owner' }])],
|
|
usage: {},
|
|
});
|
|
const row = currentAccount(await app.publicState(), workspace, ownerEmail);
|
|
assert.equal(row.id, 'identity-owner-local', 'local identity remains available for member actions');
|
|
assert.equal(row.quota5h, 83);
|
|
assert.equal(row.quota7d, 65);
|
|
assert.equal(row.quotaSnapshot.primary.usedPercent, 17);
|
|
assert.equal(row.quota5hResetAfterSeconds, 1234);
|
|
assert.equal(row.lastProbe.ok, true);
|
|
});
|
|
|
|
test('an empty legacy owner snapshot falls back to the same owner, while a complete empty snapshot stays authoritative', { timeout: 25_000 }, async (t) => {
|
|
const legacyTeam = 'owner-empty-legacy-workspace';
|
|
const completeTeam = 'owner-empty-complete-workspace';
|
|
const ownerEmail = 'owner@empty-snapshot.test';
|
|
const measured = {
|
|
quota5h: 83,
|
|
quota7d: 65,
|
|
quotaSnapshot: snapshot(17, 35),
|
|
quota5hResetAfterSeconds: 1234,
|
|
quota5hResetAt: '2026-10-08T02:00:00.000Z',
|
|
quotaUpdatedAt: '2026-10-08T01:00:00.000Z',
|
|
lastProbe: { ok: true, status: 200, ...snapshot(17, 35) },
|
|
};
|
|
const app = await startQuotaApp(t, {
|
|
teams: [
|
|
{ ...team(legacyTeam, ownerEmail, [member('empty-legacy-owner', ownerEmail, 'account-owner')]), ...measured },
|
|
{ ...team(completeTeam, ownerEmail, [member('empty-complete-owner', ownerEmail, 'account-owner')]), ...measured },
|
|
],
|
|
children: [child('empty-snapshot-owner-local', ownerEmail, [
|
|
{ team: legacyTeam, memberId: 'empty-legacy-owner', quotaSnapshot: { primary: { usedPercent: null }, secondary: { usedPercent: null } } },
|
|
{ team: completeTeam, memberId: 'empty-complete-owner', quotaSnapshot: snapshot(null, null) },
|
|
])],
|
|
usage: {},
|
|
});
|
|
const state = await app.publicState();
|
|
const legacyRow = currentAccount(state, legacyTeam, ownerEmail);
|
|
const completeRow = currentAccount(state, completeTeam, ownerEmail);
|
|
assert.equal(legacyRow.quota5h, 83, 'an empty legacy snapshot is not a measurement that can hide the same owner quota');
|
|
assert.equal(legacyRow.quota7d, 65);
|
|
assert.equal(legacyRow.quotaSnapshot.primary.usedPercent, 17);
|
|
assert.equal(legacyRow.quota5hResetAfterSeconds, 1234);
|
|
assert.equal(completeRow.quota5h, null, 'a successful duration-v1 snapshot explicitly missing every window remains authoritative');
|
|
assert.equal(completeRow.quota7d, null);
|
|
assert.equal(completeRow.quotaSnapshot.primary.usedPercent, null);
|
|
assert.equal(completeRow.quota5hResetAfterSeconds, null);
|
|
|
|
const legacyExport = await app.post('/api/sub2api/team-export', { motherIds: [`mother-${legacyTeam}`] });
|
|
const completeExport = await app.post('/api/sub2api/team-export', { motherIds: [`mother-${completeTeam}`] });
|
|
assert.equal(legacyExport.accounts.length, 1);
|
|
assert.equal(completeExport.accounts.length, 1);
|
|
assert.equal(legacyExport.accounts[0].extra.codex_5h_used_percent, 17, 'Team JSON uses the same measured fallback as currentAccounts');
|
|
assert.equal(legacyExport.accounts[0].extra.codex_7d_used_percent, 35);
|
|
assert.equal(legacyExport.accounts[0].extra.codex_5h_reset_after_seconds, 1234);
|
|
assert.deepEqual(Object.keys(completeExport.accounts[0].extra).filter((key) => /^codex_(5h|7d|primary|secondary)_/.test(key)), [], 'authoritative empty windows cannot be restored by exporting owner metadata');
|
|
});
|
|
|
|
test('a real child quota of zero and its reset/probe fields take precedence over owner cache', { timeout: 25_000 }, async (t) => {
|
|
const workspace = 'owner-zero-workspace';
|
|
const ownerEmail = 'owner@owner-zero.test';
|
|
const mother = {
|
|
...team(workspace, ownerEmail, [member('zero-owner', ownerEmail, 'account-owner')]),
|
|
quota5h: 88,
|
|
quota7d: 77,
|
|
quotaSnapshot: snapshot(12, 23),
|
|
quota5hResetAfterSeconds: 1000,
|
|
lastProbe: { ok: true, status: 200, ...snapshot(12, 23) },
|
|
};
|
|
const localOwner = child('zero-owner-local', ownerEmail, [{
|
|
team: workspace,
|
|
memberId: 'zero-owner',
|
|
quota5h: 0,
|
|
quota7d: 0,
|
|
quotaSnapshot: snapshot(100, 100),
|
|
quota5hResetAfterSeconds: 99,
|
|
lastProbe: { ok: false, status: 503, message: 'child-temporary-failure' },
|
|
}]);
|
|
const app = await startQuotaApp(t, { teams: [mother], children: [localOwner], usage: {} });
|
|
const row = currentAccount(await app.publicState(), workspace, ownerEmail);
|
|
assert.equal(row.quota5h, 0);
|
|
assert.equal(row.quota7d, 0);
|
|
assert.equal(row.quotaSnapshot.primary.usedPercent, 100);
|
|
assert.equal(row.quota5hResetAfterSeconds, 99, 'reset fields belong to the displayed quota source');
|
|
assert.equal(row.lastProbe.status, 503, 'probe failure belongs to the displayed cached child quota');
|
|
});
|
|
|
|
test('a successful child missing-window snapshot cannot resurrect an older owner cache', { timeout: 25_000 }, async (t) => {
|
|
const workspace = 'owner-missing-window-workspace';
|
|
const ownerEmail = 'owner@owner-missing-window.test';
|
|
const localId = 'missing-window-owner-local';
|
|
const mother = {
|
|
...team(workspace, ownerEmail, [member('missing-window-owner', ownerEmail, 'account-owner')]),
|
|
quota5h: 80,
|
|
quota7d: 90,
|
|
quotaSnapshot: snapshot(20, 10),
|
|
quota5hResetAfterSeconds: 1000,
|
|
};
|
|
const app = await startQuotaApp(t, {
|
|
teams: [mother],
|
|
children: [child(localId, ownerEmail, [{ team: workspace, memberId: 'missing-window-owner', quota5h: 80, quota7d: 90, quotaSnapshot: snapshot(20, 10) }])],
|
|
usage: {
|
|
[usageKey(workspace, `owner:${ownerEmail}`)]: { payload: { rate_limit: { primary_window: { used_percent: 20, limit_window_seconds: 18000 }, secondary_window: { used_percent: 10, limit_window_seconds: 604800 } } } },
|
|
[usageKey(workspace, `child:${localId}`)]: { payload: { rate_limit: { primary_window: { used_percent: 22, limit_window_seconds: 604800 } } } },
|
|
},
|
|
});
|
|
await app.check(mother.id);
|
|
const state = await app.publicState();
|
|
const row = currentAccount(state, workspace, ownerEmail);
|
|
assert.equal(state.mothers[0].quota5h, 80, 'owner and local-child probes intentionally have distinct values');
|
|
assert.equal(row.quota5h, null, 'known missing 5h window must not fall back to the owner old/full snapshot');
|
|
assert.equal(row.quota7d, 78);
|
|
assert.equal(row.quota5hResetAfterSeconds, null);
|
|
assert.equal(row.quotaSnapshot.primary.usedPercent, null);
|
|
assert.equal(row.lastProbe.secondary.usedPercent, 22, 'the displayed successful probe must match the child snapshot');
|
|
});
|
|
|
|
test('a Team with identity-only membership cannot inherit quota, resets, or probes from another Team', { timeout: 25_000 }, async (t) => {
|
|
const teamA = 'identity-isolation-a';
|
|
const teamB = 'identity-isolation-b';
|
|
const email = 'shared@identity-isolation.test';
|
|
const known = {
|
|
quota5h: 12,
|
|
quota7d: 34,
|
|
quotaSnapshot: snapshot(88, 66),
|
|
quota5hResetAfterSeconds: 111,
|
|
quota5hResetAt: '2026-10-08T03:00:00.000Z',
|
|
lastProbe: { ok: true, status: 200, ...snapshot(88, 66) },
|
|
};
|
|
const account = {
|
|
...child('identity-shared', email, [{ team: teamA, memberId: 'isolation-a', ...known }, { team: teamB, memberId: 'isolation-b' }]),
|
|
...known,
|
|
};
|
|
const app = await startQuotaApp(t, {
|
|
teams: [team(teamA, 'owner-a@identity-isolation.test', [member('isolation-a', email)]), team(teamB, 'owner-b@identity-isolation.test', [member('isolation-b', email)])],
|
|
children: [account],
|
|
usage: {},
|
|
});
|
|
const state = await app.publicState();
|
|
const rowA = currentAccount(state, teamA, email);
|
|
const rowB = currentAccount(state, teamB, email);
|
|
assert.equal(rowA.quota5h, 12);
|
|
assert.equal(rowA.quota7d, 34);
|
|
assert.equal(rowB.quota5h, null);
|
|
assert.equal(rowB.quota7d, null);
|
|
assert.equal(rowB.quotaSnapshot, null);
|
|
assert.equal(rowB.quota5hResetAfterSeconds, null);
|
|
assert.equal(rowB.quota5hResetAt, null);
|
|
assert.equal(rowB.lastProbe, null);
|
|
});
|
|
|
|
test('main-owner quota uses the main-owner identity even when another owner token expires later', { timeout: 25_000 }, async (t) => {
|
|
const workspace = 'owner-probe-identity-workspace';
|
|
const mainEmail = 'main-owner@owner-probe-identity.test';
|
|
const otherEmail = 'other-owner@owner-probe-identity.test';
|
|
const mother = {
|
|
...team(workspace, mainEmail, [member('probe-main', mainEmail, 'account-owner'), member('probe-other', otherEmail, 'account-owner')]),
|
|
accessToken: token(workspace, `owner:${mainEmail}`, 1800),
|
|
quota5h: 80,
|
|
quota7d: 90,
|
|
quotaSnapshot: snapshot(20, 10),
|
|
extra: { codex_5h_used_percent: 20, codex_7d_used_percent: 10 },
|
|
ownerAccounts: [{ email: otherEmail, accessToken: token(workspace, `owner:${otherEmail}`, 3600) }],
|
|
};
|
|
const app = await startQuotaApp(t, {
|
|
teams: [mother],
|
|
children: [],
|
|
usage: {
|
|
[usageKey(workspace, `owner:${mainEmail}`)]: { payload: { rate_limit: { primary_window: { used_percent: 18, limit_window_seconds: 18000 }, secondary_window: { used_percent: 39, limit_window_seconds: 604800 } } } },
|
|
[usageKey(workspace, `owner:${otherEmail}`)]: { payload: { rate_limit: { primary_window: { used_percent: 90, limit_window_seconds: 18000 }, secondary_window: { used_percent: 95, limit_window_seconds: 604800 } } } },
|
|
},
|
|
});
|
|
await app.check(mother.id);
|
|
const state = await app.publicState();
|
|
const row = currentAccount(state, workspace, mainEmail);
|
|
const other = currentAccount(state, workspace, otherEmail);
|
|
assert.equal(row.quota5h, 82, 'main-owner quota cannot be measured through another owner token');
|
|
assert.equal(row.quota7d, 61);
|
|
assert.equal(other.quota5h, null, 'unmeasured second owner cannot copy the main-owner quota');
|
|
assert.equal(other.quota7d, null);
|
|
assert.equal(other.quotaSnapshot, null);
|
|
assert.deepEqual(app.requests.filter((entry) => entry.pathname === '/backend-api/wham/usage').map((entry) => entry.identity), [`owner:${mainEmail}`]);
|
|
const exported = await app.post('/api/sub2api/team-export', { motherIds: [mother.id] });
|
|
const otherExport = exported.accounts.find((entry) => entry.email === otherEmail);
|
|
assert.ok(otherExport);
|
|
assert.deepEqual(Object.keys(otherExport.extra).filter((key) => /^codex_(5h|7d|primary|secondary|usage_)/.test(key)), [], 'unmeasured owner export cannot inherit main-owner usage metadata');
|
|
});
|
|
|
|
test('failed owner and member probes preserve each identity successful current-Team cache', { timeout: 25_000 }, async (t) => {
|
|
const workspace = 'failed-cache-workspace';
|
|
const ownerEmail = 'owner@failed-cache.test';
|
|
const childEmail = 'member@failed-cache.test';
|
|
const localId = 'failed-cache-child';
|
|
const ownerKey = usageKey(workspace, `owner:${ownerEmail}`);
|
|
const childKey = usageKey(workspace, `child:${localId}`);
|
|
const app = await startQuotaApp(t, {
|
|
teams: [team(workspace, ownerEmail, [member('cache-owner', ownerEmail, 'account-owner'), member('cache-child', childEmail)])],
|
|
children: [child(localId, childEmail, [{ team: workspace, memberId: 'cache-child' }])],
|
|
usage: {
|
|
[ownerKey]: { payload: { rate_limit: { primary_window: { used_percent: 12, limit_window_seconds: 18000 }, secondary_window: { used_percent: 32, limit_window_seconds: 604800 } } } },
|
|
[childKey]: { payload: { rate_limit: { primary_window: { used_percent: 44, limit_window_seconds: 18000 }, secondary_window: { used_percent: 22, limit_window_seconds: 604800 } } } },
|
|
},
|
|
});
|
|
await app.check(`mother-${workspace}`);
|
|
app.usage[ownerKey] = { status: 503, payload: { error: 'owner-temporary-failure' } };
|
|
app.usage[childKey] = { status: 503, payload: { error: 'child-temporary-failure' } };
|
|
await app.check(`mother-${workspace}`);
|
|
const state = await app.publicState();
|
|
const ownerRow = currentAccount(state, workspace, ownerEmail);
|
|
const childRow = currentAccount(state, workspace, childEmail);
|
|
assert.equal(ownerRow.quota5h, 88);
|
|
assert.equal(ownerRow.quota7d, 68);
|
|
assert.equal(childRow.quota5h, 56);
|
|
assert.equal(childRow.quota7d, 78);
|
|
assert.equal(ownerRow.quotaSnapshot.primary.usedPercent, 12);
|
|
assert.equal(childRow.quotaSnapshot.primary.usedPercent, 44);
|
|
assert.equal(ownerRow.lastProbe.status, 503);
|
|
assert.equal(childRow.lastProbe.status, 503);
|
|
});
|
|
|
|
for (const plan of ['team', 'self_serve_business_prolite']) {
|
|
test(`a valid top-level ${plan} member token is used for that member quota and Team JSON export`, { timeout: 25_000 }, async (t) => {
|
|
const workspace = 'top-level-team-member-workspace';
|
|
const ownerEmail = 'owner@top-level-member.test';
|
|
const email = 'member@top-level-member.test';
|
|
const localId = 'top-level-member-child';
|
|
const identity = `child:${localId}`;
|
|
const accessToken = token(workspace, identity, 3600, { email, plan });
|
|
const account = {
|
|
...child(localId, email, [{ team: workspace, memberId: 'top-level-member', workspaceTokenStatus: 'pending', joinStatus: 'member_confirmed_token_pending', teamAuthRetryAfter: '2099-01-01T00:00:00.000Z', teamAuthLastError: 'previous-login-failure' }]),
|
|
accountId: workspace,
|
|
plan: 'team',
|
|
accessToken,
|
|
refreshToken: 'fake-top-level-team-refresh-token',
|
|
workspaceTokens: {},
|
|
};
|
|
const app = await startQuotaApp(t, {
|
|
teams: [team(workspace, ownerEmail, [member('top-level-owner', ownerEmail, 'account-owner'), member('top-level-member', email)])],
|
|
children: [account],
|
|
usage: {
|
|
[usageKey(workspace, `owner:${ownerEmail}`)]: { payload: { rate_limit: { primary_window: { used_percent: 5, limit_window_seconds: 18000 }, secondary_window: { used_percent: 10, limit_window_seconds: 604800 } } } },
|
|
[usageKey(workspace, identity)]: { payload: { rate_limit: { primary_window: { used_percent: 41, limit_window_seconds: 18000 }, secondary_window: { used_percent: 66, limit_window_seconds: 604800 } } } },
|
|
},
|
|
});
|
|
const result = await app.check(`mother-${workspace}`);
|
|
assert.equal(result.results.find((item) => item.id === localId)?.ok, true, 'valid imported Team member token must not be reported as missing_token');
|
|
const state = await app.publicState();
|
|
const row = currentAccount(state, workspace, email);
|
|
assert.equal(row.quota5h, 59, 'quota is measured using this member own identity');
|
|
assert.equal(row.quota7d, 34);
|
|
const membership = state.children.find((item) => item.id === localId)?.workspaceHistory.find((entry) => entry.team === workspace);
|
|
assert.equal(membership?.workspaceTokenStatus, 'ready');
|
|
assert.equal(membership?.joinStatus, 'member_confirmed');
|
|
assert.equal(membership?.teamAuthRetryAfter, null);
|
|
assert.equal(membership?.teamAuthLastError, null);
|
|
const exported = await app.post('/api/sub2api/team-export', { motherIds: [`mother-${workspace}`] });
|
|
const entry = exported.accounts.find((item) => item.email === email);
|
|
assert.ok(entry, 'the ordinary member top-level Team token is exportable in its workspace');
|
|
assert.equal(entry.credentials.access_token, accessToken);
|
|
assert.equal(entry.credentials.refresh_token, account.refreshToken);
|
|
assert.equal(entry.credentials.chatgpt_account_id, workspace);
|
|
assert.equal(entry.extra.codex_5h_used_percent, 41);
|
|
assert.equal(entry.extra.codex_7d_used_percent, 66);
|
|
const persisted = await app.persistedState();
|
|
const saved = persisted.children.find((item) => item.id === localId);
|
|
assert.equal(saved.workspaceTokens[workspace].accessToken, accessToken, 'adopted Team credentials are saved to the isolated state file');
|
|
assert.equal(saved.workspaceTokens[workspace].refreshToken, account.refreshToken);
|
|
assert.equal(saved.workspaceHistory.find((item) => item.team === workspace).workspaceTokenStatus, 'ready');
|
|
assert.deepEqual(app.requests.filter((request) => request.pathname === '/backend-api/wham/usage').map((request) => request.identity).sort(), [identity, `owner:${ownerEmail}`].sort());
|
|
});
|
|
}
|
|
|
|
test('a scoped Team quota check probes only selected members and still probes the owner, while the default checks every member', { timeout: 25_000 }, async (t) => {
|
|
const workspace = 'scoped-quota-workspace';
|
|
const ownerEmail = 'owner@scoped-quota.test';
|
|
const selected = child('scoped-selected', 'selected@scoped-quota.test', [{ team: workspace, memberId: 'scoped-selected-member' }]);
|
|
const unselected = child('scoped-unselected', 'unselected@scoped-quota.test', [{ team: workspace, memberId: 'scoped-unselected-member' }]);
|
|
const ownerIdentity = `owner:${ownerEmail}`;
|
|
const selectedIdentity = `child:${selected.id}`;
|
|
const unselectedIdentity = `child:${unselected.id}`;
|
|
const app = await startQuotaApp(t, {
|
|
teams: [team(workspace, ownerEmail, [member('scoped-owner-member', ownerEmail, 'account-owner'), member('scoped-selected-member', selected.email), member('scoped-unselected-member', unselected.email)])],
|
|
children: [selected, unselected],
|
|
usage: {
|
|
[usageKey(workspace, ownerIdentity)]: { payload: { rate_limit: { primary_window: { used_percent: 5, limit_window_seconds: 18000 } } } },
|
|
[usageKey(workspace, selectedIdentity)]: { payload: { rate_limit: { primary_window: { used_percent: 41, limit_window_seconds: 18000 } } } },
|
|
[usageKey(workspace, unselectedIdentity)]: { payload: { rate_limit: { primary_window: { used_percent: 66, limit_window_seconds: 18000 } } } },
|
|
},
|
|
});
|
|
const scopedResult = await app.check(`mother-${workspace}`, [selected.id]);
|
|
assert.equal(scopedResult.checked, 1);
|
|
assert.deepEqual(scopedResult.results.map((entry) => entry.id), [selected.id]);
|
|
assert.equal(scopedResult.ownerProbe.ok, true, 'a selected-member check must retain the owner probe');
|
|
const scopedState = await app.publicState();
|
|
assert.equal(currentAccount(scopedState, workspace, ownerEmail).quota5h, 95);
|
|
assert.equal(currentAccount(scopedState, workspace, selected.email).quota5h, 59);
|
|
assert.equal(currentAccount(scopedState, workspace, unselected.email).quota5h, null, 'an unselected member cannot inherit another member measurement');
|
|
assert.deepEqual(app.requests.filter((request) => request.pathname === '/backend-api/wham/usage').map((request) => request.identity).sort(), [ownerIdentity, selectedIdentity].sort(), 'the unselected token must not be probed');
|
|
const beforeDefaultCheck = app.requests.length;
|
|
const defaultResult = await app.check(`mother-${workspace}`);
|
|
assert.equal(defaultResult.checked, 2, 'omitting childIds preserves the existing full-Team behavior');
|
|
assert.deepEqual(defaultResult.results.map((entry) => entry.id).sort(), [selected.id, unselected.id].sort());
|
|
assert.deepEqual(app.requests.slice(beforeDefaultCheck).filter((request) => request.pathname === '/backend-api/wham/usage').map((request) => request.identity).sort(), [ownerIdentity, selectedIdentity, unselectedIdentity].sort());
|
|
assert.equal(currentAccount(await app.publicState(), workspace, unselected.email).quota5h, 34);
|
|
});
|
|
|
|
test('top-level Team fallback requires matching identity, Team scope, and an unexpired token', { timeout: 25_000 }, async (t) => {
|
|
const workspace = 'top-level-token-safety-workspace';
|
|
const ownerEmail = 'owner@top-level-safety.test';
|
|
const cases = [
|
|
{ name: 'user-id-match', accepted: true, userId: 'same-user-id', storedUserId: 'same-user-id' },
|
|
{ name: 'business-plan', accepted: true, plan: 'business', profileEmail: true },
|
|
{ name: 'prolite-plan', accepted: true, plan: 'self_serve_business_prolite', profileEmail: true },
|
|
{ name: 'free-plan', accepted: false, plan: 'free', profileEmail: true },
|
|
{ name: 'prolite-like-free-plan', accepted: false, plan: 'self_serve_business_free', profileEmail: true },
|
|
{ name: 'unknown-business-prefix-plan', accepted: false, plan: 'self_serve_business_unknown', profileEmail: true },
|
|
{ name: 'unknown-prolite-suffix-plan', accepted: false, plan: 'self_serve_business_prolite_unknown', profileEmail: true },
|
|
{ name: 'other-team', accepted: false, tokenTeam: 'unrelated-workspace', profileEmail: true },
|
|
{ name: 'prolite-other-team', accepted: false, plan: 'self_serve_business_prolite', tokenTeam: 'unrelated-workspace', profileEmail: true },
|
|
{ name: 'wrong-email', accepted: false, email: 'someone-else@top-level-safety.test' },
|
|
{ name: 'prolite-wrong-email', accepted: false, plan: 'self_serve_business_prolite', email: 'someone-else@top-level-safety.test' },
|
|
{ name: 'expired', accepted: false, expiresIn: -3600, profileEmail: true },
|
|
{ name: 'expiring', accepted: false, expiresIn: 10, profileEmail: true },
|
|
{ name: 'missing-expiration', accepted: false, expiresIn: null, profileEmail: true },
|
|
{ name: 'unknown-identity', accepted: false },
|
|
{ name: 'wrong-user-id', accepted: false, userId: 'different-user-id', storedUserId: 'wrong-user-id-stored' },
|
|
{ name: 'prolite-wrong-user-id', accepted: false, plan: 'self_serve_business_prolite', userId: 'different-prolite-user-id', storedUserId: 'wrong-prolite-user-id-stored' },
|
|
{ name: 'mismatched-email-matching-user-id', accepted: false, email: 'someone-else@top-level-safety.test', userId: 'matching-user-id-with-wrong-email', storedUserId: 'matching-user-id-with-wrong-email' },
|
|
{ name: 'matching-email-mismatched-user-id', accepted: false, profileEmail: true, userId: 'wrong-user-with-matching-email', storedUserId: 'expected-user-with-matching-email' },
|
|
];
|
|
const children = cases.map((entry) => {
|
|
const id = `safety-${entry.name}`;
|
|
const email = `${entry.name}@top-level-safety.test`;
|
|
return {
|
|
...child(id, email, [{ team: workspace, memberId: `member-${entry.name}` }]),
|
|
accountId: workspace,
|
|
plan: 'team',
|
|
chatgptUserId: entry.storedUserId || '',
|
|
accessToken: token(entry.tokenTeam || workspace, `child:${id}`, entry.expiresIn === undefined ? 3600 : entry.expiresIn, { email: entry.profileEmail ? email : entry.email, userId: entry.userId, plan: entry.plan || 'team' }),
|
|
workspaceTokens: {},
|
|
};
|
|
});
|
|
const usage = {
|
|
[usageKey(workspace, `owner:${ownerEmail}`)]: { payload: { rate_limit: { primary_window: { used_percent: 9, limit_window_seconds: 18000 } } } },
|
|
};
|
|
children.forEach((account) => {
|
|
usage[usageKey(workspace, `child:${account.id}`)] = { payload: { rate_limit: { primary_window: { used_percent: 33, limit_window_seconds: 18000 }, secondary_window: { used_percent: 44, limit_window_seconds: 604800 } } } };
|
|
});
|
|
const app = await startQuotaApp(t, {
|
|
teams: [team(workspace, ownerEmail, [member('safety-owner', ownerEmail, 'account-owner'), ...children.map((account, index) => member(`member-${cases[index].name}`, account.email))])],
|
|
children,
|
|
usage,
|
|
});
|
|
const result = await app.check(`mother-${workspace}`);
|
|
const state = await app.publicState();
|
|
const exported = await app.post('/api/sub2api/team-export', { motherIds: [`mother-${workspace}`] });
|
|
for (const entry of cases) {
|
|
const id = `safety-${entry.name}`;
|
|
const email = `${entry.name}@top-level-safety.test`;
|
|
const row = currentAccount(state, workspace, email);
|
|
const probe = result.results.find((item) => item.id === id);
|
|
assert.equal(probe?.ok, entry.accepted, `${entry.name} acceptance must match the confirmed token identity and scope`);
|
|
assert.equal(row.quota5h, entry.accepted ? 67 : null, `${entry.name} cannot use unrelated or expired credentials to read quota`);
|
|
assert.equal(row.quota7d, entry.accepted ? 56 : null);
|
|
assert.equal(exported.accounts.some((account) => account.email === email), entry.accepted, `${entry.name} Team export follows the same scope rules`);
|
|
assert.equal(app.requests.some((request) => request.pathname === '/backend-api/wham/usage' && request.identity === `child:${id}`), entry.accepted);
|
|
}
|
|
});
|
|
|
|
test('a same-Team workspace map token belonging to another member cannot supply this member quota or Team export', { timeout: 25_000 }, async (t) => {
|
|
const workspace = 'workspace-map-identity-isolation';
|
|
const ownerEmail = 'owner@workspace-map-isolation.test';
|
|
const email = 'member@workspace-map-isolation.test';
|
|
const localId = 'workspace-map-isolation-child';
|
|
const userId = 'workspace-map-isolation-user';
|
|
const mother = team(workspace, ownerEmail, [member('map-isolation-owner', ownerEmail, 'account-owner'), member('map-isolation-member', email)]);
|
|
const personalToken = token(`personal-${localId}`, `child:${localId}`, 3600, { email, userId, plan: 'free' });
|
|
const account = {
|
|
...child(localId, email, [{ team: workspace, memberId: 'map-isolation-member' }]),
|
|
chatgptUserId: userId,
|
|
accountId: `personal-${localId}`,
|
|
accessToken: personalToken,
|
|
workspaceTokens: { [workspace]: { accessToken: mother.accessToken } },
|
|
};
|
|
const app = await startQuotaApp(t, {
|
|
teams: [mother],
|
|
children: [account],
|
|
usage: { [usageKey(workspace, `owner:${ownerEmail}`)]: { payload: { rate_limit: { primary_window: { used_percent: 7, limit_window_seconds: 18000 }, secondary_window: { used_percent: 11, limit_window_seconds: 604800 } } } } },
|
|
refreshResponses: {},
|
|
});
|
|
const result = await app.check(mother.id);
|
|
const probe = result.results.find((entry) => entry.id === localId);
|
|
assert.equal(probe?.ok, false);
|
|
assert.equal(probe?.message, 'missing_token');
|
|
const row = currentAccount(await app.publicState(), workspace, email);
|
|
assert.equal(row.quota5h, null);
|
|
assert.equal(row.quota7d, null);
|
|
assert.deepEqual(app.requests.filter((request) => request.pathname === '/backend-api/wham/usage').map((request) => request.identity), [`owner:${ownerEmail}`], 'the owner token can be measured only as the owner, never again as this child');
|
|
const exported = await app.post('/api/sub2api/team-export', { motherIds: [mother.id] });
|
|
assert.equal(exported.accounts.some((entry) => entry.email === email), false);
|
|
assert.equal((await app.persistedState()).children[0].accessToken, personalToken);
|
|
});
|
|
|
|
test('workspace switching refuses another member same-Team token and preserves the original personal identity and credentials', { timeout: 25_000 }, async (t) => {
|
|
const workspace = 'workspace-switch-identity-isolation';
|
|
const ownerEmail = 'owner@workspace-switch-isolation.test';
|
|
const email = 'member@workspace-switch-isolation.test';
|
|
const localId = 'workspace-switch-isolation-child';
|
|
const userId = 'workspace-switch-isolation-user';
|
|
const identity = `child:${localId}`;
|
|
const personalId = `personal-${localId}`;
|
|
const personalToken = token(personalId, identity, 3600, { email, userId, plan: 'free' });
|
|
const mother = team(workspace, ownerEmail, [member('switch-isolation-owner', ownerEmail, 'account-owner'), member('switch-isolation-member', email)]);
|
|
const account = { ...child(localId, email, [{ team: workspace, memberId: 'switch-isolation-member' }]), chatgptUserId: userId, accountId: personalId, accessToken: personalToken, refreshToken: 'fixture-original-personal-rt', workspaceTokens: {} };
|
|
const app = await startQuotaApp(t, {
|
|
teams: [mother],
|
|
children: [account],
|
|
usage: { [usageKey(workspace, `owner:${ownerEmail}`)]: { payload: { rate_limit: { primary_window: { used_percent: 7, limit_window_seconds: 18000 } } } } },
|
|
refreshResponses: {},
|
|
sessionResponses: { [identity]: { status: 200, payload: { accessToken: mother.accessToken } } },
|
|
});
|
|
const switched = await app.rawPost(`/api/children/${localId}/switch`, { workspaceId: workspace });
|
|
assert.equal(switched.ok, false, 'a matching workspace alone cannot authorize using another person token');
|
|
assert.equal(switched.data.ok, false);
|
|
assert.equal(switched.data.code, 'token_identity_mismatch');
|
|
const saved = (await app.persistedState()).children[0];
|
|
assert.equal(saved.accessToken, personalToken);
|
|
assert.equal(saved.refreshToken, account.refreshToken);
|
|
assert.equal(saved.accountId, personalId);
|
|
assert.equal(saved.chatgptUserId, userId);
|
|
assert.equal(saved.workspaceTokens[workspace], undefined);
|
|
assert.equal(saved.workspaceHistory.find((entry) => entry.team === workspace).quota5h ?? null, null);
|
|
assert.deepEqual(app.requests.filter((request) => request.pathname === '/api/auth/session').map((request) => request.identity), [identity]);
|
|
const exported = await app.post('/api/sub2api/team-export', { motherIds: [mother.id] });
|
|
assert.equal(exported.accounts.some((entry) => entry.email === email), false);
|
|
});
|
|
|
|
test('a valid workspace map token takes priority, while an expired map may fall back to a valid matching top-level token', { timeout: 25_000 }, async (t) => {
|
|
const workspace = 'top-level-map-priority-workspace';
|
|
const ownerEmail = 'owner@map-priority.test';
|
|
const cases = [
|
|
{ name: 'valid-map', mapExpiresIn: 3600, expectedIdentity: 'workspace:valid-map', expectedQuota: 76 },
|
|
{ name: 'expired-map', mapExpiresIn: -3600, expectedIdentity: 'top-level:expired-map', expectedQuota: 49 },
|
|
];
|
|
const children = cases.map((entry) => {
|
|
const email = `${entry.name}@map-priority.test`;
|
|
return {
|
|
...child(`priority-${entry.name}`, email, [{ team: workspace, memberId: `priority-${entry.name}` }]),
|
|
plan: 'team',
|
|
accountId: workspace,
|
|
accessToken: token(workspace, `top-level:${entry.name}`, 3600, { email }),
|
|
workspaceTokens: { [workspace]: { accessToken: token(workspace, `workspace:${entry.name}`, entry.mapExpiresIn, { email }) } },
|
|
};
|
|
});
|
|
const usage = {
|
|
[usageKey(workspace, `owner:${ownerEmail}`)]: { payload: { rate_limit: { primary_window: { used_percent: 9, limit_window_seconds: 18000 } } } },
|
|
};
|
|
for (const entry of cases) {
|
|
usage[usageKey(workspace, `workspace:${entry.name}`)] = { payload: { rate_limit: { primary_window: { used_percent: 24, limit_window_seconds: 18000 } } } };
|
|
usage[usageKey(workspace, `top-level:${entry.name}`)] = { payload: { rate_limit: { primary_window: { used_percent: 51, limit_window_seconds: 18000 } } } };
|
|
}
|
|
const app = await startQuotaApp(t, {
|
|
teams: [team(workspace, ownerEmail, [member('priority-owner', ownerEmail, 'account-owner'), ...children.map((account) => member(account.id, account.email))])],
|
|
children,
|
|
usage,
|
|
});
|
|
const result = await app.check(`mother-${workspace}`);
|
|
const state = await app.publicState();
|
|
const exported = await app.post('/api/sub2api/team-export', { motherIds: [`mother-${workspace}`] });
|
|
for (const entry of cases) {
|
|
const account = children.find((item) => item.email === `${entry.name}@map-priority.test`);
|
|
assert.equal(result.results.find((item) => item.id === account.id)?.ok, true, `${entry.name} must select a valid own Team token`);
|
|
assert.equal(currentAccount(state, workspace, account.email).quota5h, entry.expectedQuota);
|
|
const requests = app.requests.filter((request) => request.pathname === '/backend-api/wham/usage' && request.identity.includes(`:${entry.name}`));
|
|
assert.deepEqual(requests.map((request) => request.identity), [entry.expectedIdentity]);
|
|
const exportEntry = exported.accounts.find((item) => item.email === account.email);
|
|
assert.ok(exportEntry);
|
|
const exportIdentity = requestIdentity({ headers: { authorization: `Bearer ${exportEntry.credentials.access_token}` } });
|
|
assert.equal(exportIdentity, entry.expectedIdentity);
|
|
}
|
|
});
|
|
|
|
test('an expired matching top-level Team token can use its RT to restore its own Team quota', { timeout: 25_000 }, async (t) => {
|
|
const workspace = 'expired-top-level-refresh-workspace';
|
|
const ownerEmail = 'owner@expired-top-refresh.test';
|
|
const email = 'member@expired-top-refresh.test';
|
|
const localId = 'expired-top-refresh-child';
|
|
const userId = 'expired-top-refresh-user';
|
|
const refreshToken = 'fake-expired-top-level-refresh-token';
|
|
const renewedToken = token(workspace, 'renewed:expired-top-refresh', 3600, { email, userId });
|
|
const account = {
|
|
...child(localId, email, [{ team: workspace, memberId: 'expired-top-refresh-member', joinStatus: 'member_confirmed', workspaceTokenStatus: 'pending' }]),
|
|
accountId: workspace,
|
|
chatgptUserId: userId,
|
|
plan: 'team',
|
|
accessToken: token(workspace, 'expired:top-level-refresh', -3600, { email, userId }),
|
|
refreshToken,
|
|
workspaceTokens: {},
|
|
};
|
|
const app = await startQuotaApp(t, {
|
|
teams: [team(workspace, ownerEmail, [member('expired-top-refresh-owner', ownerEmail, 'account-owner'), member('expired-top-refresh-member', email)])],
|
|
children: [account],
|
|
usage: {
|
|
[usageKey(workspace, `owner:${ownerEmail}`)]: { payload: { rate_limit: { primary_window: { used_percent: 5, limit_window_seconds: 18000 } } } },
|
|
[usageKey(workspace, 'renewed:expired-top-refresh')]: { payload: { rate_limit: { primary_window: { used_percent: 39, limit_window_seconds: 18000 }, secondary_window: { used_percent: 72, limit_window_seconds: 604800 } } } },
|
|
},
|
|
refreshResponses: { [refreshToken]: { status: 200, payload: { access_token: renewedToken, refresh_token: 'fake-rotated-team-refresh-token' } } },
|
|
});
|
|
const result = await app.check(`mother-${workspace}`);
|
|
const probe = result.results.find((item) => item.id === localId);
|
|
assert.equal(probe?.ok, true);
|
|
assert.equal(probe?.tokenRecovery?.ok, true);
|
|
assert.equal(probe?.tokenRecovery?.credentialLogin, false, 'matching RT recovery does not need a password login');
|
|
const state = await app.publicState();
|
|
assert.equal(currentAccount(state, workspace, email).quota5h, 61);
|
|
assert.equal(currentAccount(state, workspace, email).quota7d, 28);
|
|
const saved = (await app.persistedState()).children.find((item) => item.id === localId);
|
|
assert.equal(saved.chatgptUserId, userId);
|
|
assert.equal(saved.workspaceTokens[workspace].accessToken, renewedToken);
|
|
assert.equal(saved.workspaceTokens[workspace].refreshToken, 'fake-rotated-team-refresh-token');
|
|
assert.equal(saved.workspaceHistory.find((entry) => entry.team === workspace).workspaceTokenStatus, 'ready');
|
|
const exported = await app.post('/api/sub2api/team-export', { motherIds: [`mother-${workspace}`] });
|
|
const entry = exported.accounts.find((item) => item.email === email);
|
|
assert.equal(entry?.credentials.access_token, renewedToken);
|
|
assert.equal(entry?.credentials.refresh_token, 'fake-rotated-team-refresh-token');
|
|
assert.deepEqual(app.requests.filter((request) => request.pathname === '/mock-oauth/token').map((request) => [request.grantType, request.refreshToken]), [['refresh_token', refreshToken]]);
|
|
});
|
|
|
|
test('an explicit same-member Team refresh updates the account and selected workspace credentials together', { timeout: 25_000 }, async (t) => {
|
|
const workspace = 'explicit-refresh-workspace';
|
|
const otherWorkspace = 'explicit-refresh-other-workspace';
|
|
const email = 'member@explicit-refresh.test';
|
|
const localId = 'explicit-refresh-child';
|
|
const userId = 'explicit-refresh-user';
|
|
const refreshToken = 'fake-explicit-refresh-token';
|
|
const oldToken = token(workspace, 'old:explicit-refresh', -3600, { email, userId });
|
|
const oldMappedToken = token(workspace, 'old-map:explicit-refresh', 3600, { email, userId });
|
|
const otherToken = token(otherWorkspace, 'other:explicit-refresh', 3600, { email, userId });
|
|
const renewedToken = token(workspace, 'new:explicit-refresh', 3600, { email, userId });
|
|
const account = {
|
|
...child(localId, email, [{ team: workspace, memberId: 'explicit-refresh-member', workspaceTokenStatus: 'pending', joinStatus: 'member_confirmed_token_pending', teamAuthRetryAfter: '2099-01-01T00:00:00.000Z', teamAuthLastError: 'old-auth-failure' }]),
|
|
accountId: workspace,
|
|
chatgptUserId: userId,
|
|
plan: 'team',
|
|
accessToken: oldToken,
|
|
refreshToken,
|
|
workspaceTokens: {
|
|
[workspace]: { accessToken: oldMappedToken, refreshToken: 'fake-old-map-refresh-token' },
|
|
[otherWorkspace]: { accessToken: otherToken, refreshToken: 'fake-other-workspace-refresh-token' },
|
|
},
|
|
};
|
|
const app = await startQuotaApp(t, {
|
|
teams: [team(workspace, 'owner@explicit-refresh.test', [member('explicit-refresh-member', email)])],
|
|
children: [account],
|
|
usage: {},
|
|
refreshResponses: { [refreshToken]: { status: 200, payload: { access_token: renewedToken, refresh_token: 'fake-explicit-rotated-refresh-token' } } },
|
|
});
|
|
const response = await app.rawPost(`/api/children/${localId}/acquire`, { mode: 'refresh-at' });
|
|
assert.equal(response.status, 200);
|
|
assert.equal(response.data.source, 'refresh_token');
|
|
const saved = (await app.persistedState()).children.find((item) => item.id === localId);
|
|
assert.equal(saved.accessToken, renewedToken);
|
|
assert.equal(saved.refreshToken, 'fake-explicit-rotated-refresh-token');
|
|
assert.equal(saved.chatgptUserId, userId);
|
|
assert.equal(saved.workspaceTokens[workspace].accessToken, renewedToken, 'regular account refresh replaces the selected workspace old AT');
|
|
assert.equal(saved.workspaceTokens[workspace].refreshToken, 'fake-explicit-rotated-refresh-token');
|
|
assert.equal(saved.workspaceTokens[otherWorkspace].accessToken, otherToken, 'another Team retains its own credentials');
|
|
const membership = saved.workspaceHistory.find((entry) => entry.team === workspace);
|
|
assert.equal(membership.workspaceTokenStatus, 'ready');
|
|
assert.equal(membership.joinStatus, 'member_confirmed');
|
|
assert.equal(membership.teamAuthRetryAfter, null);
|
|
assert.equal(membership.teamAuthLastError, null);
|
|
const exported = await app.post('/api/sub2api/team-export', { motherIds: [`mother-${workspace}`] });
|
|
assert.equal(exported.accounts.find((item) => item.email === email)?.credentials.access_token, renewedToken);
|
|
});
|
|
|
|
test('refresh and callback login reject another member token without replacing the original identity or credentials', { timeout: 25_000 }, async (t) => {
|
|
const workspace = 'refresh-identity-mismatch-workspace';
|
|
const cases = [
|
|
{ name: 'refresh-wrong-user', responseEmail: 'same', responseUser: 'wrong', login: false },
|
|
{ name: 'refresh-wrong-email', responseEmail: 'wrong', responseUser: 'same', login: false },
|
|
{ name: 'login-wrong-user', responseEmail: 'same', responseUser: 'wrong', login: true },
|
|
{ name: 'login-wrong-email', responseEmail: 'wrong', responseUser: 'same', login: true },
|
|
];
|
|
const children = cases.map((entry) => {
|
|
const email = `${entry.name}@identity-mismatch.test`;
|
|
const userId = `stored-user:${entry.name}`;
|
|
const accessToken = token(workspace, `original:${entry.name}`, -3600, { email, userId });
|
|
return {
|
|
...child(`identity-${entry.name}`, email, [{ team: workspace, memberId: `member-${entry.name}` }]),
|
|
accountId: workspace,
|
|
chatgptUserId: userId,
|
|
accessToken,
|
|
refreshToken: entry.login ? '' : `fake-refresh:${entry.name}`,
|
|
workspaceTokens: { [workspace]: { accessToken, refreshToken: `fake-map-refresh:${entry.name}` } },
|
|
...(entry.login ? { password: 'test-only-password', totp: 'JBSWY3DPEHPK3PXP', authSession: { state: `oauth-state:${entry.name}`, codeVerifier: 'test-only-code-verifier' } } : {}),
|
|
};
|
|
});
|
|
const refreshResponses = {};
|
|
cases.forEach((entry, index) => {
|
|
const account = children[index];
|
|
const wrongToken = token(workspace, `unexpected:${entry.name}`, 3600, {
|
|
email: entry.responseEmail === 'same' ? account.email : 'another-member@identity-mismatch.test',
|
|
userId: entry.responseUser === 'same' ? account.chatgptUserId : `different-user:${entry.name}`,
|
|
});
|
|
refreshResponses[entry.login ? `authorization-code:${entry.name}` : account.refreshToken] = {
|
|
status: 200,
|
|
payload: { access_token: wrongToken, refresh_token: `fake-unexpected-refresh:${entry.name}` },
|
|
};
|
|
});
|
|
const app = await startQuotaApp(t, {
|
|
teams: [team(workspace, 'owner@identity-mismatch.test', children.map((account, index) => member(`member-${cases[index].name}`, account.email)))],
|
|
children,
|
|
usage: {},
|
|
refreshResponses,
|
|
});
|
|
for (const [index, entry] of cases.entries()) {
|
|
const account = children[index];
|
|
const body = entry.login
|
|
? { mode: 'free-json', callbackUrl: `http://localhost:1455/auth/callback?code=${encodeURIComponent(`authorization-code:${entry.name}`)}&state=${encodeURIComponent(`oauth-state:${entry.name}`)}` }
|
|
: { mode: 'refresh-at' };
|
|
const response = await app.rawPost(`/api/children/${account.id}/acquire`, body);
|
|
assert.equal(response.status, 502, `${entry.name} rejects a different-member credential response`);
|
|
assert.equal(response.data.code, 'token_identity_mismatch');
|
|
const saved = (await app.persistedState()).children.find((item) => item.id === account.id);
|
|
assert.equal(saved.email, account.email);
|
|
assert.equal(saved.chatgptUserId, account.chatgptUserId);
|
|
assert.equal(saved.accessToken, account.accessToken);
|
|
assert.equal(saved.refreshToken, account.refreshToken);
|
|
assert.equal(saved.workspaceTokens[workspace].accessToken, account.accessToken);
|
|
assert.equal(saved.workspaceTokens[workspace].refreshToken, `fake-map-refresh:${entry.name}`);
|
|
}
|
|
assert.deepEqual(app.requests.filter((request) => request.pathname === '/mock-oauth/token').map((request) => request.grantType), ['refresh_token', 'refresh_token', 'authorization_code', 'authorization_code']);
|
|
});
|