Skip to content
Open
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ jobs:
cache: 'npm'

- run: npm ci
- run: npm run build
- run: npm test
env:
CI: true
Expand Down
48 changes: 24 additions & 24 deletions src/commands/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ describe('runConfigure', () => {
const { capture, deps } = makeCapture();
// Pre-existing dev profile — re-running configure interactively must keep it
// (the internal dogfooding flow) without ever prompting for the endpoint.
writeProfile(
await writeProfile(
'dev',
{ apiKey: 'sk-old', apiUrl: 'https://api.example.com:8443' },
{ path: credentialsPath },
Expand Down Expand Up @@ -350,7 +350,7 @@ describe('runConfigure', () => {
it('dogfood-2026-05-25 — --from-env without TESTSPRITE_API_URL inherits existing profile api_url AND validates against it', async () => {
const { capture, deps } = makeCapture();
// Pre-write an existing profile with a custom (non-default) endpoint.
writeProfile(
await writeProfile(
'default',
{ apiKey: 'sk-old', apiUrl: 'https://api.example.com' },
{ path: credentialsPath },
Expand Down Expand Up @@ -398,7 +398,7 @@ describe('runConfigure', () => {

it('dogfood-2026-05-25 — --endpoint-url flag overrides existing profile api_url', async () => {
const { capture, deps } = makeCapture();
writeProfile(
await writeProfile(
'default',
{ apiKey: 'sk-old', apiUrl: 'https://api.example.com' },
{ path: credentialsPath },
Expand Down Expand Up @@ -428,7 +428,7 @@ describe('runConfigure', () => {
it('dogfood-2026-05-25 — no advisory when inherited url equals DEFAULT_API_URL', async () => {
const { capture, deps } = makeCapture();
// Existing profile has the default prod endpoint — no advisory needed.
writeProfile(
await writeProfile(
'default',
{ apiKey: 'sk-old', apiUrl: 'https://api.testsprite.com' },
{ path: credentialsPath },
Expand Down Expand Up @@ -504,7 +504,7 @@ describe('runConfigure', () => {
// An exported-but-empty env var (`export TESTSPRITE_API_URL=`) must not
// short-circuit the `??` chain to an empty endpoint; it should fall through
// to the existing profile's api_url.
writeProfile(
await writeProfile(
'default',
{ apiKey: 'sk-old', apiUrl: 'https://api.example.com:8443' },
{ path: credentialsPath },
Expand Down Expand Up @@ -610,7 +610,7 @@ describe('runWhoami', () => {
}

it('calls GET /me using the configured profile and prints text output', async () => {
writeProfile(
await writeProfile(
'default',
{ apiKey: 'sk-stored', apiUrl: 'https://api.example.com' },
{ path: credentialsPath },
Expand All @@ -633,7 +633,7 @@ describe('runWhoami', () => {
});

it('emits JSON when --output json', async () => {
writeProfile('default', { apiKey: 'sk' }, { path: credentialsPath });
await writeProfile('default', { apiKey: 'sk' }, { path: credentialsPath });
const { capture, deps } = makeCapture();
await runWhoami(
{ profile: 'default', output: 'json', debug: false },
Expand All @@ -644,7 +644,7 @@ describe('runWhoami', () => {
});

it('L1788: text output includes the resolved endpoint URL', async () => {
writeProfile(
await writeProfile(
'default',
{ apiKey: 'sk-stored', apiUrl: 'https://api.example.com' },
{ path: credentialsPath },
Expand Down Expand Up @@ -672,7 +672,7 @@ describe('runWhoami', () => {
});

it('L1788: JSON output does NOT add endpoint (raw /me envelope is passed through)', async () => {
writeProfile(
await writeProfile(
'default',
{ apiKey: 'sk-stored', apiUrl: 'https://api.example.com' },
{ path: credentialsPath },
Expand Down Expand Up @@ -701,7 +701,7 @@ describe('runWhoami', () => {
});

it('L1866: renders email + name in text mode when the backend supplies them', async () => {
writeProfile('default', { apiKey: 'sk' }, { path: credentialsPath });
await writeProfile('default', { apiKey: 'sk' }, { path: credentialsPath });
const { capture, deps } = makeCapture();
const meWithEmail = new Response(
JSON.stringify({ ...sampleMe, email: 'alice@example.com', displayName: 'Alice' }),
Expand All @@ -718,7 +718,7 @@ describe('runWhoami', () => {
});

it('L1866: omits email/name lines when the backend does not return them', async () => {
writeProfile('default', { apiKey: 'sk' }, { path: credentialsPath });
await writeProfile('default', { apiKey: 'sk' }, { path: credentialsPath });
const { capture, deps } = makeCapture();
await runWhoami(
{ profile: 'default', output: 'text', debug: false },
Expand All @@ -731,7 +731,7 @@ describe('runWhoami', () => {
});

it('L1866: passes email through verbatim in JSON mode when present', async () => {
writeProfile('default', { apiKey: 'sk' }, { path: credentialsPath });
await writeProfile('default', { apiKey: 'sk' }, { path: credentialsPath });
const { capture, deps } = makeCapture();
const meWithEmail = new Response(JSON.stringify({ ...sampleMe, email: 'alice@example.com' }), {
status: 200,
Expand Down Expand Up @@ -769,7 +769,7 @@ describe('runWhoami', () => {
});

it('emits debug events to stderr when debug is enabled', async () => {
writeProfile('default', { apiKey: 'sk' }, { path: credentialsPath });
await writeProfile('default', { apiKey: 'sk' }, { path: credentialsPath });
const { capture, deps } = makeCapture();
await runWhoami(
{ profile: 'default', output: 'json', debug: true },
Expand All @@ -782,7 +782,7 @@ describe('runWhoami', () => {
});

it('forwards server AUTH_INVALID with exit code 3', async () => {
writeProfile('default', { apiKey: 'sk-bad' }, { path: credentialsPath });
await writeProfile('default', { apiKey: 'sk-bad' }, { path: credentialsPath });
const { deps } = makeCapture();
const errorBody = {
error: {
Expand Down Expand Up @@ -812,7 +812,7 @@ describe('runWhoami', () => {
...sampleMe,
scopes: ['read:projects', 'read:tests'],
};
writeProfile('default', { apiKey: 'sk' }, { path: credentialsPath });
await writeProfile('default', { apiKey: 'sk' }, { path: credentialsPath });
const { capture, deps } = makeCapture();
const fetchImpl = makeFetch(
new Response(JSON.stringify(readOnlyMe), {
Expand All @@ -835,7 +835,7 @@ describe('runWhoami', () => {
...sampleMe,
scopes: ['read:projects', 'read:tests', 'write:tests', 'run:tests'],
};
writeProfile('default', { apiKey: 'sk' }, { path: credentialsPath });
await writeProfile('default', { apiKey: 'sk' }, { path: credentialsPath });
const { capture, deps } = makeCapture();
const fetchImpl = makeFetch(
new Response(JSON.stringify(fullMe), {
Expand All @@ -856,7 +856,7 @@ describe('runWhoami', () => {
...sampleMe,
scopes: ['read:projects', 'read:tests'],
};
writeProfile('default', { apiKey: 'sk' }, { path: credentialsPath });
await writeProfile('default', { apiKey: 'sk' }, { path: credentialsPath });
const { capture, deps } = makeCapture();
const fetchImpl = makeFetch(
new Response(JSON.stringify(readOnlyMe), {
Expand All @@ -878,8 +878,8 @@ describe('runWhoami', () => {

describe('runLogout', () => {
it('removes the profile and reports success', async () => {
writeProfile('default', { apiKey: 'sk' }, { path: credentialsPath });
writeProfile('dev', { apiKey: 'sk-dev' }, { path: credentialsPath });
await writeProfile('default', { apiKey: 'sk' }, { path: credentialsPath });
await writeProfile('dev', { apiKey: 'sk-dev' }, { path: credentialsPath });
const { capture, deps } = makeCapture();
await runLogout(
{ profile: 'default', output: 'text', debug: false },
Expand Down Expand Up @@ -927,7 +927,7 @@ describe('createAuthCommand wiring', () => {
});

it('remove deletes the active profile and exits 0', async () => {
writeProfile('default', { apiKey: 'sk-remove' }, { path: credentialsPath });
await writeProfile('default', { apiKey: 'sk-remove' }, { path: credentialsPath });
const { deps } = makeCapture();
const auth = createAuthCommand({ ...deps, credentialsPath });
auth.exitOverride();
Expand All @@ -937,7 +937,7 @@ describe('createAuthCommand wiring', () => {
});

it('deprecated `whoami` alias emits a deprecation notice pointing at `auth status`', async () => {
writeProfile('default', { apiKey: 'sk' }, { path: credentialsPath });
await writeProfile('default', { apiKey: 'sk' }, { path: credentialsPath });
const { capture, deps } = makeCapture();
const auth = createAuthCommand({
...deps,
Expand All @@ -953,7 +953,7 @@ describe('createAuthCommand wiring', () => {
});

it('whoami uses injected fetch and exits 0', async () => {
writeProfile('default', { apiKey: 'sk' }, { path: credentialsPath });
await writeProfile('default', { apiKey: 'sk' }, { path: credentialsPath });
const { deps } = makeCapture();
const fetchImpl = vi.fn(
async () =>
Expand All @@ -970,7 +970,7 @@ describe('createAuthCommand wiring', () => {
});

it('L1802: `status` alias resolves to the whoami action', async () => {
writeProfile('default', { apiKey: 'sk' }, { path: credentialsPath });
await writeProfile('default', { apiKey: 'sk' }, { path: credentialsPath });
const { deps } = makeCapture();
const fetchImpl = vi.fn(
async () =>
Expand All @@ -987,7 +987,7 @@ describe('createAuthCommand wiring', () => {
});

it('logout removes the profile', async () => {
writeProfile('default', { apiKey: 'sk' }, { path: credentialsPath });
await writeProfile('default', { apiKey: 'sk' }, { path: credentialsPath });
const { deps } = makeCapture();
const auth = createAuthCommand({ ...deps, credentialsPath });
auth.exitOverride();
Expand Down
4 changes: 2 additions & 2 deletions src/commands/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export async function runConfigure(opts: ConfigureOptions, deps: AuthDeps = {}):
);
}

writeProfile(opts.profile, { apiKey, apiUrl }, { path: credentialsPath });
await writeProfile(opts.profile, { apiKey, apiUrl }, { path: credentialsPath });

out.print({ profile: opts.profile, apiUrl, status: 'configured' }, data => {
const d = data as { profile: string; apiUrl: string };
Expand Down Expand Up @@ -258,7 +258,7 @@ export async function runLogout(opts: CommonOptions, deps: AuthDeps = {}): Promi
return;
}

const removed = deleteProfile(opts.profile, { path: credentialsPath });
const removed = await deleteProfile(opts.profile, { path: credentialsPath });
out.print({ profile: opts.profile, status: removed ? 'logged_out' : 'no_credentials' }, data => {
const d = data as { profile: string; status: string };
return d.status === 'logged_out'
Expand Down
20 changes: 10 additions & 10 deletions src/commands/usage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ describe('runUsage — dry-run', () => {

describe('runUsage — real path without credits (current backend)', () => {
it('returns the /me response and emits a note about missing balance', async () => {
writeProfile('default', { apiKey: 'sk-abc' }, { path: credentialsPath });
await writeProfile('default', { apiKey: 'sk-abc' }, { path: credentialsPath });
const { capture, deps } = makeCapture();
const result = await runUsage(
{ profile: 'default', output: 'text', debug: false },
Expand All @@ -116,7 +116,7 @@ describe('runUsage — real path without credits (current backend)', () => {
});

it('text output includes identity fields even without credits', async () => {
writeProfile('default', { apiKey: 'sk-abc' }, { path: credentialsPath });
await writeProfile('default', { apiKey: 'sk-abc' }, { path: credentialsPath });
const { capture, deps } = makeCapture();
await runUsage(
{ profile: 'default', output: 'text', debug: false },
Expand All @@ -132,7 +132,7 @@ describe('runUsage — real path without credits (current backend)', () => {
});

it('JSON output passes the raw /me response through (no credits key present)', async () => {
writeProfile('default', { apiKey: 'sk-abc' }, { path: credentialsPath });
await writeProfile('default', { apiKey: 'sk-abc' }, { path: credentialsPath });
const { capture, deps } = makeCapture();
await runUsage(
{ profile: 'default', output: 'json', debug: false },
Expand All @@ -150,7 +150,7 @@ describe('runUsage — real path without credits (current backend)', () => {

describe('runUsage — real path with credits (future backend)', () => {
it('renders balance block when credits + subPlan are present', async () => {
writeProfile('default', { apiKey: 'sk-abc' }, { path: credentialsPath });
await writeProfile('default', { apiKey: 'sk-abc' }, { path: credentialsPath });
const { capture, deps } = makeCapture();
await runUsage(
{ profile: 'default', output: 'text', debug: false },
Expand All @@ -173,7 +173,7 @@ describe('runUsage — real path with credits (future backend)', () => {
});

it('does NOT emit the missing-balance note when credits are present', async () => {
writeProfile('default', { apiKey: 'sk-abc' }, { path: credentialsPath });
await writeProfile('default', { apiKey: 'sk-abc' }, { path: credentialsPath });
const { capture, deps } = makeCapture();
await runUsage(
{ profile: 'default', output: 'text', debug: false },
Expand All @@ -189,7 +189,7 @@ describe('runUsage — real path with credits (future backend)', () => {
});

it('emits low-balance warning when credits < creditsPerRun', async () => {
writeProfile('default', { apiKey: 'sk-abc' }, { path: credentialsPath });
await writeProfile('default', { apiKey: 'sk-abc' }, { path: credentialsPath });
const { capture, deps } = makeCapture();
const lowBalance: UsageResponse = { ...meWithCredits, credits: 1, creditsPerRun: 2 };
await runUsage(
Expand All @@ -206,7 +206,7 @@ describe('runUsage — real path with credits (future backend)', () => {
});

it('emits free-plan upgrade hint when subPlan is Free', async () => {
writeProfile('default', { apiKey: 'sk-abc' }, { path: credentialsPath });
await writeProfile('default', { apiKey: 'sk-abc' }, { path: credentialsPath });
const { capture, deps } = makeCapture();
const freePlan: UsageResponse = { ...meWithCredits, subPlan: 'Free', credits: 10 };
await runUsage(
Expand All @@ -223,7 +223,7 @@ describe('runUsage — real path with credits (future backend)', () => {
});

it('JSON output passes credits and subPlan through verbatim', async () => {
writeProfile('default', { apiKey: 'sk-abc' }, { path: credentialsPath });
await writeProfile('default', { apiKey: 'sk-abc' }, { path: credentialsPath });
const { capture, deps } = makeCapture();
await runUsage(
{ profile: 'default', output: 'json', debug: false },
Expand All @@ -249,7 +249,7 @@ describe('runUsage — error handling', () => {
});

it('forwards server AUTH_INVALID with exit code 3', async () => {
writeProfile('default', { apiKey: 'sk-bad' }, { path: credentialsPath });
await writeProfile('default', { apiKey: 'sk-bad' }, { path: credentialsPath });
const { deps } = makeCapture();
const errorBody = {
error: {
Expand All @@ -273,7 +273,7 @@ describe('runUsage — error handling', () => {
});

it('re-maps INSUFFICIENT_CREDITS (rate_limited with credits sub-case) to exit 12', async () => {
writeProfile('default', { apiKey: 'sk-abc' }, { path: credentialsPath });
await writeProfile('default', { apiKey: 'sk-abc' }, { path: credentialsPath });
const { deps } = makeCapture();
const creditError = {
error: {
Expand Down
24 changes: 14 additions & 10 deletions src/lib/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,21 @@ describe('loadConfig', () => {
expect(config.profile).toBe('default');
});

it('honors TESTSPRITE_API_URL over the file', () => {
writeProfile('default', { apiUrl: 'https://from-file.example.com' }, { path: credentialsPath });
it('honors TESTSPRITE_API_URL over the file', async () => {
await writeProfile(
'default',
{ apiUrl: 'https://from-file.example.com' },
{ path: credentialsPath },
);
const config = loadConfig({
env: { TESTSPRITE_API_URL: 'https://from-env.example.com' },
credentialsPath,
});
expect(config.apiUrl).toBe('https://from-env.example.com');
});

it('honors TESTSPRITE_API_KEY over the file', () => {
writeProfile('default', { apiKey: 'sk-from-file' }, { path: credentialsPath });
it('honors TESTSPRITE_API_KEY over the file', async () => {
await writeProfile('default', { apiKey: 'sk-from-file' }, { path: credentialsPath });
const config = loadConfig({
env: { TESTSPRITE_API_KEY: 'sk-from-env' },
credentialsPath,
Expand All @@ -55,8 +59,8 @@ describe('loadConfig', () => {
).toBe('option-profile');
});

it('option.endpointUrl overrides everything', () => {
writeProfile('default', { apiUrl: 'https://file' }, { path: credentialsPath });
it('option.endpointUrl overrides everything', async () => {
await writeProfile('default', { apiUrl: 'https://file' }, { path: credentialsPath });
const config = loadConfig({
endpointUrl: 'https://flag',
env: { TESTSPRITE_API_URL: 'https://env' },
Expand All @@ -65,8 +69,8 @@ describe('loadConfig', () => {
expect(config.apiUrl).toBe('https://flag');
});

it('falls back to credentials file when env is unset', () => {
writeProfile(
it('falls back to credentials file when env is unset', async () => {
await writeProfile(
'default',
{ apiKey: 'sk-file', apiUrl: 'https://from-file.example.com' },
{ path: credentialsPath },
Expand All @@ -76,8 +80,8 @@ describe('loadConfig', () => {
expect(config.apiUrl).toBe('https://from-file.example.com');
});

it('reads the requested profile, not just default', () => {
writeProfile('dev', { apiKey: 'sk-dev' }, { path: credentialsPath });
it('reads the requested profile, not just default', async () => {
await writeProfile('dev', { apiKey: 'sk-dev' }, { path: credentialsPath });
const config = loadConfig({ profile: 'dev', env: {}, credentialsPath });
expect(config.apiKey).toBe('sk-dev');
});
Expand Down
Loading
Loading