Skip to content

[Hackathon] fix(local-target): accept mixed-case HTTP loopback origins and preserve automatic tunnel discovery #648

Open

Description

@Xuefeng-Zhu

Verified against (commit SHA or released version)

Commit a18d37c8183c1cc39190cfdf2a7a82897e4e113c, package @testsprite/testsprite-cli 0.15.0, Node 24.19.0, Linux cloud container. Runtime verification used unmodified production source and lockfile. No Mac/Windows runtime verification or hosted TestSprite calls are claimed.

Discord Username / User ID

frankzhu94

What does this improvement do?

Accept mixed-case HTTP/localhost origins consistently, so valid local targets are not rejected and a mixed-case stored environment is not misclassified as remote.

Actual compiled CLI dry-run witnesses:

node dist/index.js --dry-run test run test_xyz \
  --target-url 'http://LOCALHOST:3000' --output json
# exit 5: loopback URLs must use http://localhost:<port> ...

node dist/index.js --dry-run project env create project_1 --name local \
  --url 'http://Localhost:3000' --output json
# same local VALIDATION_ERROR / exit 5

Lowercase http://localhost:3000 succeeds in both dry-run controls. HTTP://localhost:3000 is also refused, despite having the same parsed origin.

Expected: hostname and HTTP scheme letter case do not change acceptance or automatic local-target discovery. Preserve all existing restrictions on explicit port, path, query, credentials and HTTPS.

Observed additional impact: with an injected default environment {name:'default',url:'http://Localhost:3000',isDefault:true}, resolveEnvironmentRunTarget returns undefined, while its lowercase control resolves a tunnel target. In a full CLI fixture this allows the ordinary trigger request to be sent without a tunnel.

This proves client interpretation only. Real backend storage/acceptance of a mixed-case environment URL was not tested; no hosted run, cloud execution or charge is asserted. The direct refusal is independently reproducible either way.

Details / implementation notes

Root cause

WHATWG URL normalization recognizes these as the same host/scheme; the raw regex loses that equivalence. The existing loopbackHost test covers mixed case, but the following parseLoopbackTargetUrl tests use lowercase URL spellings.

Executed controls

Input Parser Stored environment resolver CLI dry-run target/write
http://localhost:3000 localhost, 3000 local target exit 0 / 0
http://LOCALHOST:3000 exit 5 undefined exit 5 / 5
http://Localhost:3000 exit 5 undefined exit 5 / 5
HTTP://localhost:3000 exit 5 undefined exit 5 / 5
http://127.0.0.1:3000 accepted local target exit 0 / 0
http://[::1]:3000 accepted local target exit 0 / 0
http://localhost:80 explicit 80 preserved local target exit 0 / 0

HTTPS and non-root-path parser controls still refuse as intended.

In the full stored-environment fixture, GET /tests/test_xyz and GET /projects/project_1/env succeed. The listing contains the mixed-case default URL. The actual CLI then emits:

POST /api/cli/v1/tests/test_xyz/runs

{"source":"cli"}

No tunnel-mint request or tunnel identifier is emitted. The fixture replies with a queued run and the CLI exits 0. The stored URL is not explicitly sent in that trigger body; the response echo is fixture data. This is not evidence that production accepts or bills the request.

Small offline reproduction

After building the pinned checkout, save the following outside it and run node repro-loopback.mjs /absolute/path/to/testsprite-cli. It uses injected environment reads only: no DNS, local-port connection, HTTP request, or credentials. This exact script was executed successfully in the cloud. Assertions describe currently observed behavior, not corrected behavior.

Reproduction script
import assert from 'node:assert/strict';
import {resolve} from 'node:path';
import {pathToFileURL} from 'node:url';
const root=resolve(process.argv[2]);
const {parseLoopbackTargetUrl}=await import(pathToFileURL(root+'/dist/lib/local-target.js'));
const {resolveEnvironmentRunTarget}=await import(pathToFileURL(root+'/dist/lib/environment-tunnel.js'));
for(const url of ['http://localhost:3000','http://LOCALHOST:3000','http://Localhost:3000','HTTP://localhost:3000','http://localhost:80']) {
  let parsed;
  try {parsed=parseLoopbackTargetUrl(url);} catch(error) {parsed={error:error.message,exit:error.exitCode};}
  const result=await resolveEnvironmentRunTarget({
    knownTest:{projectId:'project_fixture',type:'frontend'},
    client:{get:async()=>({environments:[{name:'default',url,isDefault:true}]})},
    stderr:()=>{},
  });
  const mixed=url.includes('LOCALHOST')||url.includes('Localhost')||url.startsWith('HTTP:');
  if(mixed){assert.equal(parsed.exit,5);assert.equal(result,undefined);}
  else {assert.equal(parsed.host,'localhost');assert.equal(result.port,url.endsWith(':80')?80:3000);}
  console.log(JSON.stringify({url,parsed,resolved:result??null}));
}

Proposed scope and validation

Make the existing HTTP-origin regex case-insensitive, or validate parsed components while preserving the explicit default-port and strict raw-input contracts. Do not trim otherwise-invalid input or silently broaden the accepted path/query/credential/scheme set. Cover independent hostname/scheme case and the stored-environment resolver, plus explicit :80 and existing rejection controls. No runtime dependency or backend change is proposed.

Pinned-source baseline passed build, 4,798 unit tests (2 skipped), 110 e2e tests (1 skipped), typecheck, lint (0 errors, 215 warnings), and format checking. A focused rerun of local-target, environment-tunnel, test.quickwins, test.run and gh-output suites passed 356 tests / 5 files, alongside this regression witness. Production source remains clean.

Related reports / scope coordination

Reviewed the 642-record open/closed issue/PR snapshot through #646 and relevant discussion. Refreshed GitHub's issue search for parseLoopbackTargetUrl before filing; its related hit was #549, not this case-handling defect.

#549 is accepted/in progress for ci run refusing local targets with a suitable error and zero outputs. This proposal fixes shared raw-origin parsing and automatic discovery on existing test run / environment-write surfaces; it does not add local-target support to ci run. A patch may touch a shared helper, so coordinate with that work.

No matching report was found in checked public records; private/internal work is unknown. Implementation is pending maintainer triage, with no patch, PR or assignment included. AI disclosure: Codex assisted with source review, cloud-only verification, duplicate checking and this write-up.

Confirmations

  • I have searched existing issues and this is not a duplicate.
  • I have provided my Discord identity above for reward coordination.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    hackathonCLI hackathon submissionsneeds-triageNew issue/PR awaiting first triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions