Skip to content

Commit 61bc167

Browse files
Recovered: feat: support TESTSPRITE_PROJECT_ID default (#144 by @naufalfx805-source) (#249)
* feat: support project env default * fix: tighten project env fallback --------- Co-authored-by: nopp <naufalfx805@gmail.com>
1 parent d821dac commit 61bc167

5 files changed

Lines changed: 225 additions & 62 deletions

File tree

DOCUMENTATION.md

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -600,24 +600,25 @@ These apply to every command:
600600

601601
### Environment variables
602602

603-
| Variable | Purpose |
604-
| ----------------------------------------- | ---------------------------------------------------------------------------------------- |
605-
| `TESTSPRITE_API_KEY` | API key — overrides the credentials file |
606-
| `TESTSPRITE_API_URL` | API endpoint — overrides the credentials file |
607-
| `TESTSPRITE_PROFILE` | Active profile (below `--profile`, above `default`) |
608-
| `TESTSPRITE_REQUEST_TIMEOUT_MS` | Per-request timeout in **milliseconds** (default `120000`, range `1000``600000`) |
609-
| `TESTSPRITE_NO_UPDATE_NOTIFIER` | Any non-empty value disables the once-per-24h "new version available" notice |
610-
| `NO_COLOR` | Suppress ANSI escape sequences in ticker output ([no-color.org](https://no-color.org/)) |
611-
| `HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY` | Standard proxy support — API traffic is routed through the configured proxy |
612-
| `TESTSPRITE_NO_SKILL_WARNING` | Any non-empty value silences the "verify skill not installed" reminder (CI / manual use) |
613-
| `TESTSPRITE_PORTAL_URL` | Override the Portal origin used for `dashboardUrl` links (non-prod environments) |
603+
| Variable | Purpose |
604+
| ----------------------------------------- | ------------------------------------------------------------------------------------------------ |
605+
| `TESTSPRITE_API_KEY` | API key - overrides the credentials file |
606+
| `TESTSPRITE_API_URL` | API endpoint - overrides the credentials file |
607+
| `TESTSPRITE_PROFILE` | Active profile (below `--profile`, above `default`) |
608+
| `TESTSPRITE_PROJECT_ID` | Default project for `test list`, `test create`, and `test run --all` when `--project` is omitted |
609+
| `TESTSPRITE_REQUEST_TIMEOUT_MS` | Per-request timeout in **milliseconds** (default `120000`, range `1000`-`600000`) |
610+
| `TESTSPRITE_NO_UPDATE_NOTIFIER` | Any non-empty value disables the once-per-24h "new version available" notice |
611+
| `NO_COLOR` | Suppress ANSI escape sequences in ticker output ([no-color.org](https://no-color.org/)) |
612+
| `HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY` | Standard proxy support - API traffic is routed through the configured proxy |
613+
| `TESTSPRITE_NO_SKILL_WARNING` | Any non-empty value silences the "verify skill not installed" reminder (CI / manual use) |
614+
| `TESTSPRITE_PORTAL_URL` | Override the Portal origin used for `dashboardUrl` links (non-prod environments) |
614615

615616
### Update notice
616617

617618
Interactive runs print a one-line "new version available" notice on stderr when
618619
a newer release exists. To learn this, the CLI contacts the public npm registry
619620
(`registry.npmjs.org`) at most once per 24 hours; the request carries the
620-
package name only never your API key, project data, or command line. The
621+
package name only - never your API key, project data, or command line. The
621622
check is skipped in CI, when stderr is not a TTY, under `--output json` /
622623
`--dry-run`, and entirely when `TESTSPRITE_NO_UPDATE_NOTIFIER` is set. Any
623624
failure is silent: the notice can never break or delay a command. This is the
@@ -627,7 +628,7 @@ Separately, the backend advertises its **minimum supported CLI version** on
627628
every `/api/cli/v1` response. When the running CLI is below that floor, a
628629
one-line upgrade advisory is printed to stderr (same opt-outs as the update
629630
notice; it never changes the exit status). A backend may also reject a
630-
too-old client outright with HTTP 426 surfaced as `CLIENT_TOO_OLD`,
631+
too-old client outright with HTTP 426 - surfaced as `CLIENT_TOO_OLD`,
631632
exit `14`, non-retriable, with upgrade guidance.
632633

633634
### Scopes

src/commands/test.run.spec.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2582,9 +2582,58 @@ describe('runTestRunAll — batch fresh run', () => {
25822582
await expect(test.parseAsync(['run', '--all'], { from: 'user' })).rejects.toMatchObject({
25832583
code: 'VALIDATION_ERROR',
25842584
exitCode: 5,
2585+
details: expect.objectContaining({
2586+
reason: expect.stringContaining('TESTSPRITE_PROJECT_ID'),
2587+
}),
25852588
});
25862589
});
25872590

2591+
it('run --all uses TESTSPRITE_PROJECT_ID when --project is omitted', async () => {
2592+
const { createTestCommand } = await import('./test.js');
2593+
const { credentialsPath } = makeCreds();
2594+
type Captured = { url: string; method: string; body: unknown };
2595+
const captured: Captured[] = [];
2596+
const fetchImpl = makeFetch((url, init) => {
2597+
const method = init.method ?? 'GET';
2598+
captured.push({ url, method, body: init.body ? JSON.parse(init.body as string) : undefined });
2599+
return { body: BATCH_FRESH_RESP };
2600+
});
2601+
const test = createTestCommand({
2602+
credentialsPath,
2603+
env: { TESTSPRITE_PROJECT_ID: 'project_env' } as NodeJS.ProcessEnv,
2604+
fetchImpl,
2605+
stdout: () => undefined,
2606+
stderr: () => undefined,
2607+
sleep: instantSleep,
2608+
});
2609+
await test.parseAsync(['run', '--all'], { from: 'user' });
2610+
const post = captured.find(c => c.method === 'POST' && c.url.includes('/tests/batch/run'))!;
2611+
expect(post.body).toMatchObject({ projectId: 'project_env', source: 'cli' });
2612+
});
2613+
2614+
it('run --all uses TESTSPRITE_PROJECT_ID when --project is blank', async () => {
2615+
const { createTestCommand } = await import('./test.js');
2616+
const { credentialsPath } = makeCreds();
2617+
type Captured = { url: string; method: string; body: unknown };
2618+
const captured: Captured[] = [];
2619+
const fetchImpl = makeFetch((url, init) => {
2620+
const method = init.method ?? 'GET';
2621+
captured.push({ url, method, body: init.body ? JSON.parse(init.body as string) : undefined });
2622+
return { body: BATCH_FRESH_RESP };
2623+
});
2624+
const test = createTestCommand({
2625+
credentialsPath,
2626+
env: { TESTSPRITE_PROJECT_ID: 'project_env' } as NodeJS.ProcessEnv,
2627+
fetchImpl,
2628+
stdout: () => undefined,
2629+
stderr: () => undefined,
2630+
sleep: instantSleep,
2631+
});
2632+
await test.parseAsync(['run', '--all', '--project', ' '], { from: 'user' });
2633+
const post = captured.find(c => c.method === 'POST' && c.url.includes('/tests/batch/run'))!;
2634+
expect(post.body).toMatchObject({ projectId: 'project_env', source: 'cli' });
2635+
});
2636+
25882637
it('--all --target-url → exit 5 (target-url has no effect on BE-only batch)', async () => {
25892638
const { createTestCommand } = await import('./test.js');
25902639
const test = createTestCommand();

src/commands/test.test.ts

Lines changed: 99 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,64 @@ describe('runList', () => {
365365
expect(seen[0]).toContain('createdFrom=portal');
366366
});
367367

368+
it('uses TESTSPRITE_PROJECT_ID when --project is omitted', async () => {
369+
const { credentialsPath } = makeCreds();
370+
const seen: string[] = [];
371+
const fetchImpl = makeFetch(url => {
372+
seen.push(url);
373+
return { body: { items: [FE_TEST], nextToken: null } };
374+
});
375+
await runList(
376+
{ profile: 'default', output: 'json', debug: false },
377+
{
378+
credentialsPath,
379+
env: { TESTSPRITE_PROJECT_ID: 'project_env' } as NodeJS.ProcessEnv,
380+
fetchImpl,
381+
stdout: () => undefined,
382+
},
383+
);
384+
expect(seen[0]).toContain('projectId=project_env');
385+
});
386+
387+
it('uses TESTSPRITE_PROJECT_ID when --project is blank', async () => {
388+
const { credentialsPath } = makeCreds();
389+
const seen: string[] = [];
390+
const fetchImpl = makeFetch(url => {
391+
seen.push(url);
392+
return { body: { items: [FE_TEST], nextToken: null } };
393+
});
394+
await runList(
395+
{ profile: 'default', output: 'json', debug: false, projectId: ' ' },
396+
{
397+
credentialsPath,
398+
env: { TESTSPRITE_PROJECT_ID: 'project_env' } as NodeJS.ProcessEnv,
399+
fetchImpl,
400+
stdout: () => undefined,
401+
},
402+
);
403+
expect(seen[0]).toContain('projectId=project_env');
404+
});
405+
406+
it('prefers explicit --project over TESTSPRITE_PROJECT_ID', async () => {
407+
const { credentialsPath } = makeCreds();
408+
const seen: string[] = [];
409+
const fetchImpl = makeFetch(url => {
410+
seen.push(url);
411+
return { body: { items: [FE_TEST], nextToken: null } };
412+
});
413+
await runList(
414+
{ profile: 'default', output: 'json', debug: false, projectId: 'project_flag' },
415+
{
416+
credentialsPath,
417+
env: { TESTSPRITE_PROJECT_ID: 'project_env' } as NodeJS.ProcessEnv,
418+
fetchImpl,
419+
stdout: () => undefined,
420+
},
421+
);
422+
expect(seen[0]).toContain('projectId=project_flag');
423+
expect(seen[0]).not.toContain('projectId=project_env');
424+
});
425+
368426
it('accepts --created-from cli and passes createdFrom=cli to the wire (dogfood 2026-06-04)', async () => {
369427
// End-to-end through parseEnumFlag: backend now stamps createFrom='cli'
370428
// on `testsprite test create` rows, so the filter must accept 'cli'.
@@ -783,10 +841,15 @@ describe('createTestCommand list — required flag', () => {
783841
await test.parseAsync(['list'], { from: 'user' });
784842
expect.unreachable('expected ApiError');
785843
} catch (err) {
786-
const apiErr = err as { code?: string; exitCode?: number; details?: { field?: string } };
844+
const apiErr = err as {
845+
code?: string;
846+
exitCode?: number;
847+
details?: { field?: string; reason?: string };
848+
};
787849
expect(apiErr.code).toBe('VALIDATION_ERROR');
788850
expect(apiErr.exitCode).toBe(5);
789851
expect(apiErr.details?.field).toBe('project');
852+
expect(apiErr.details?.reason).toContain('TESTSPRITE_PROJECT_ID');
790853
}
791854
});
792855

@@ -4847,7 +4910,7 @@ describe('runCreate', () => {
48474910
...SAMPLE_RESPONSE,
48484911
type: 'backend',
48494912
warnings: [
4850-
'This test appears to hardcode an auth credential read auth from __AUTH_HEADERS__.',
4913+
'This test appears to hardcode an auth credential - read auth from __AUTH_HEADERS__.',
48514914
],
48524915
},
48534916
};
@@ -4873,10 +4936,43 @@ describe('runCreate', () => {
48734936
);
48744937
// Warning lands on stderr, prefixed `[warn]`.
48754938
expect(err.some(l => l.includes('[warn]') && l.includes('__AUTH_HEADERS__'))).toBe(true);
4876-
// stdout stays the parseable wire object no warning noise.
4939+
// stdout stays the parseable wire object - no warning noise.
48774940
expect(out.join('\n')).not.toContain('[warn]');
48784941
});
48794942

4943+
it('uses TESTSPRITE_PROJECT_ID for create when --project is omitted', async () => {
4944+
const { credentialsPath } = makeCreds();
4945+
const codeFile = writeCodeFile('code body');
4946+
type Captured = { method: string; body: unknown; url: string };
4947+
const captured: Captured[] = [];
4948+
const fetchImpl = makeFetch((url, init) => {
4949+
const method = init.method ?? 'GET';
4950+
captured.push({ url, method, body: init.body ? JSON.parse(init.body as string) : undefined });
4951+
if (method === 'GET') return { status: 200, body: { items: [] } };
4952+
return { status: 200, body: SAMPLE_RESPONSE };
4953+
});
4954+
await runCreate(
4955+
{
4956+
profile: 'default',
4957+
output: 'json',
4958+
debug: false,
4959+
type: 'frontend',
4960+
name: 'n',
4961+
codeFile,
4962+
},
4963+
{
4964+
credentialsPath,
4965+
env: { TESTSPRITE_PROJECT_ID: 'project_env' } as NodeJS.ProcessEnv,
4966+
fetchImpl,
4967+
stdout: () => undefined,
4968+
},
4969+
);
4970+
expect(captured.some(c => c.method === 'GET' && c.url.includes('projectId=project_env'))).toBe(
4971+
true,
4972+
);
4973+
const post = captured.find(c => c.method === 'POST')!;
4974+
expect(post.body).toMatchObject({ projectId: 'project_env' });
4975+
});
48804976
it('respects a caller-supplied --idempotency-key (for safe retries)', async () => {
48814977
const { credentialsPath } = makeCreds();
48824978
const codeFile = writeCodeFile('code body');
@@ -5030,7 +5126,6 @@ describe('runCreate', () => {
50305126
profile: 'default',
50315127
output: 'json',
50325128
debug: false,
5033-
// @ts-expect-error — exercising the runtime gate
50345129
projectId: undefined,
50355130
type: 'frontend',
50365131
name: 'n',

0 commit comments

Comments
 (0)