Skip to content

[Hackathon] fix(config): bypass unused credential files when key and endpoint are explicit #536

Open

Description

@walter-w-white

Verified against

988c0355bb05631e9c6c1785e443937ff2a88c0e (current main), @testsprite/testsprite-cli0.14.0, Linux, Node22.23.3.

Contact

Not on Discord. Please contact @walter-w-white on this GitHub issue or PR.

What is broken

Ordinary commands still require ~/.testsprite/credentials to be readable when both the API key and endpoint are supplied explicitly. An unreadable leftover file makes project list, auth status, and test get exit1 before sending any request. A directory at the unused credentials pathname has the same effect. Neither complete environment variables nor an environment key plus --endpoint-url can bypass it.

DOCUMENTATION.md says the key/URL environment variables override the file, “so CI can run without ever touching ~/.testsprite/credentials.” The current doctor recovery also tells env-key users “commands still work.” On the same fixture, doctor reaches the API and exits0 while an immediate ordinary command fails locally.

Reproduction

This minimal, offline reproduction from a built repository uses a disposable directory instead of any real credential file. It also reproduces as root because it needs no permission denial:

node --input-type=module <<'JS'
import { mkdtempSync, mkdirSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { loadConfig } from './dist/lib/config.js';
const temporary = mkdtempSync(join(tmpdir(), 'testsprite-env-override-'));
const credentialsPath = join(temporary, 'credentials');
const env = {
  TESTSPRITE_API_KEY: 'sk-user-fixture-not-real',
  TESTSPRITE_API_URL: 'http://127.0.0.1:19191',
};
try {
  // Normal control: the complete pair works with no credentials file.
  console.log(loadConfig({ env, credentialsPath }));
  mkdirSync(credentialsPath);
  // Expected: the same explicit configuration. Actual: EISDIR.
  console.log(loadConfig({ env, credentialsPath }));
} finally {
  rmSync(temporary, { recursive: true, force: true });
}
JS

I also ran the unchanged CLI entry point against strict loopback HTTP fixtures, with a synthetic homedir and no production services. Permission cases ran with CapEff=CapBnd=0; an independent native read proved EACCES before the CLI check. The final matrix has25scenarios/30CLI processes; a separate six-call native reproducer confirms the minimal workflow:

Same explicit key and API URL Command Exit HTTP requests
Missing credentials file project list --output json 0 1
Existing file, mode000 project list --output json 1 (EACCES) 0
Same unreadable file doctor --output json 0 (credentials warning) 2
Same file after restoring read permission project list --output json 0 1
Directory at credentials pathname project list --output json 1 (EISDIR) 0
Directory removed project list --output json 0 1

The broader matrix includes text/JSON, auth/test reads, flag endpoint precedence, partial overrides requiring real file fallback, blank-variable normalization, malformed-profile rejection, dry-run, and a503retry after the fixture makes the file unreadable. That last request succeeds with the resolved key: the unnecessary dependency is at initial config resolution, not transport replay. An uninstrumented filesystem reproduction gives the same failure.

Expected and actual

With a valid key and endpoint completely supplied by supported overrides, the ordinary read should use those values without opening an unused credential file. Actual JSON stderr is an EACCES/EISDIR envelope with exit1 and no request; no server authentication error is involved.

This blocks environment-configured scripts and the advertised env-key workaround for an unreadable local store. I have not measured hosted incidence or tested Windows ACLs. No credential disclosure, file corruption, or server defect is claimed.

Root cause and proposed scope

loadConfig calls readProfile before normalizing/selecting TESTSPRITE_API_KEY and TESTSPRITE_API_URL. readProfile synchronously reads the existing file even when none of its values are needed. The ordinary client factory therefore fails before using its higher-priority inputs.

A focused fix can validate the profile name separately, resolve explicit key/endpoint values first, and read the credential file only when a needed field still requires fallback. Preserve errors when fallback really is needed, blank-variable behavior, malformed-profile rejection, and the existing precedence. Do not broadly catch filesystem errors or change ACLs. Add regressions for both complete and partial overrides; the local six-test regression currently has four controls passing and these two cases failing.

Related work and contribution intent

Credit to @aythin and @jangjos-128 for the ACL diagnosis and doctor recovery in #304/#301/#356. This is a remaining ordinary-command config dependency; the existing doctor fix works in the reproduction and should not be re-reported. #8/#97 handle empty variables. #399/#401 handle prototype-sensitive profile names. #225 adds optional configuration defaults but its current patch still eagerly reads credentials. I reviewed open and closed issues/PRs and the matching discussions.

I intend to implement this after maintainer acceptance and actual assignment to @walter-w-white, following the contribution guide. No production fix has been started.

Current duplicate review

Compared current open and closed reports again on 2026-10-19. #438 adds validation and diagnostics for a directory/FIFO credentials path; #465 makes telemetry authentication best-effort. Neither removes the ordinary loadConfig dependency on an unused credential file when key and endpoint are fully supplied. This reproduction disables telemetry; doctor works while the ordinary command still fails. Partial overrides must continue reading and validating required fallback fields.

Contribution contact

Please contact @walter-w-white on this issue. No Discord identity is provided. AI assistance was used for source investigation, offline reproduction and drafting. This issue reports one existing CLI behavior; no production fix or assignment is claimed.

Pre-submission revalidation

Revalidated on 2026-10-19 with six actual CLI invocations and DAC permission bypass removed from the entire reproduction process. Only unreadable-file and directory-path ordinary reads violated the override contract; missing-file, doctor, repaired-permission and removed-directory controls behaved as expected. Tested upstream remains 988c0355bb05631e9c6c1785e443937ff2a88c0e (CLI 0.14.0, Linux, Node 22.23.3). All reproduction traffic is local; no hosted testing credits were used.

I intend to implement this fix after maintainer acceptance and assignment.

Please contact @walter-w-white on this GitHub issue or PR; no Discord identity is provided.

Activity

  1. zeshi-du commented on Oct 20, 2026

    @zeshi-du
    Contributor

    Thanks @walter-w-white — confirmed on v0.15.0 and accepted. loadConfig calls readProfile before looking at TESTSPRITE_API_KEY/TESTSPRITE_API_URL/--endpoint-url, so an unreadable or directory credentials path breaks commands that don't need it.

    Scope:

    • Validate the profile name first, resolve explicit key and endpoint, and read the credentials file only when a needed field is still missing.
    • Keep: blank-variable normalization, malformed-profile rejection, existing precedence, and errors when fallback is genuinely needed.
    • Do not broadly catch filesystem errors.
    • Tests for complete and partial overrides in config tests. The complete case should fail before the fix.

    Comment /assign to claim it.

  2. added
    acceptedTriaged and accepted — a PR for this will be reviewed
    on Oct 20, 2026
  3. walter-w-white commented on Oct 20, 2026

    @walter-w-white
    Author

    /assign

  4. testsprite-hob commented on Oct 20, 2026

    @testsprite-hob

    Assigned to @walter-w-white. Thanks for taking this on. (2 more slots available.)

    Your open assigned issues (1/3):
    (none)

  5. ruili-testsprite commented on Oct 20, 2026

    @ruili-testsprite
    Contributor

    One addition to the scope above, since it's the one caller that depends on the eager read:

    • runDoctor learns that the credentials file is unreadable by catching the error loadConfig throws (credentialsReadError). With a lazy read that path goes quiet whenever the key and endpoint come from flags or env, so have doctor check the credentials file directly and keep reporting an unreadable file.
    • Print (TESTSPRITE_API_KEY is set, so commands still work) only when both the key and the endpoint come from outside the file.
    • Add a doctor test: unreadable credentials file plus complete env overrides, where commands succeed and doctor still reports the file.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

acceptedTriaged and accepted — a PR for this will be reviewedin-progressAssigned and actively being worked on

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions