Skip to content

feat(upload): address failing tests by test-case GUID in collection links - #1196

Open
max-trunk wants to merge 1 commit into
mainfrom
max/print-test-case-guid-links
Open

feat(upload): address failing tests by test-case GUID in collection links#1196
max-trunk wants to merge 1 commit into
mainfrom
max/print-test-case-guid-links

Conversation

@max-trunk

@max-trunk max-trunk commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

not to be merged until the guid url structure support is on prod

What this changes

The report table's per-test links address the test case by its GUID — a deterministic UUIDv8 hash of (test_collection_id, repo_id, test_case_id) — instead of the /t/{id} short link the web app has to resolve a repo for and redirect.

before  …/collections/tc_a1b2c3d4/t/88e5353c-190c-5dce-9d06-0e66c3e062b1?repo=trunk-io%2Fanalytics-cli
after   …/collections/tc_a1b2c3d4/tests/bfeebcf4-72d1-887d-8bcd-788d0dec7f97

No ?repo= — the GUID resolves the whole identity tuple, so there is nothing for the web app to look up. Same shape of win as #1169 took for upload links.

Where the two server-owned ids come from

createBundleUpload now returns repoId and testCollectionId. Both land on CreateBundleUploadResponse and are threaded to the report table — deliberately not through BundleMetaBaseProps, since the server does not need them there and that would churn the bundle schema.

The hash itself is gen_test_case_guid from the context crate (#1166), which the server consumes too — so there is no second implementation of it to keep in sync.

Depends on the matching web app route

This needs the collections/{shortId}/tests/{guid} route to land before it ships. The path shape is pinned in one format! plus one golden test, so correcting it is a two-line change if it differs.

One request for whoever builds it: an unknown GUID should render an empty state, not a hard 404, matching how the existing test-detail page handles an id it cannot find. See the next section for why that is not cosmetic.

Why this might not be as clean as it looks

  • A well-formed GUID can still address nothing, silently. FailedTestsExtractor derives test_case.id by re-parsing the raw JUnit tree, while the server derives its test_case_id from the normalized report the CLI writes into the bundle. When a report puts file on <testsuite> rather than <testcase>, the two disagree and we hash the wrong tuple. That is pre-existing — today's short link is equally wrong in that case — and the fix belongs in its own PR, but it is why the empty-state request above matters.
  • repoId is Option even though the server guarantees it. Deliberate: serde would hard-fail the whole createBundleUpload deserialization — killing the upload — over a field that only decorates a link. A rollback, a canary, or a partial deploy all produce that. testCollectionId has no choice; it is genuinely optional, and comes back null whenever the short id did not resolve to a live collection.
  • The pre-upload Learn more lines still print the short linkcreateBundleUpload runs after them, so one --verbose run shows two URL shapes for the same test. Reordering the calls to fix it was tried in refactor(upload): resolve the upload id before the quarantine step #1197 and closed: it shifts the client-reported upload window, which is a bad trade for something only visible under -v (a default run shows the report table, which is fully on the GUID here). Reasoning in refactor(upload): resolve the upload id before the quarantine step #1197.

Fallback behaviour

Every way the GUID can be unavailable funnels through one decision point in url_for_test_case and degrades to today's link: no collection short id, links hidden, --dry-run, a server that sent no ids, or a test_case.id that is not a UUID (the RSpec integration emits trunk:<id>-<location>, and hashing a non-UUID would mint a valid-looking id that addresses nothing).

Implementation notes

  • TestCaseGuidScope bundles the two server ids rather than taking two Option<&str> params, so "both ids or neither" is unrepresentable — the server cannot produce one without the other.
  • uuid becomes a direct dep of api; it was already in the lock, so the lock diff is one line.
  • The unit test pins a golden vector shared with the server-side implementation rather than recomputing the hash, so a drift in either the hash or the URL shape fails the build.
  • render_test_table now uses self.api_address instead of the global get_api_host(), matching collection_upload_url right above it.

Testing

  • api: 25 passed — golden GUID vector, plus fallbacks for an unparseable server id and the repo-scoped path
  • trunk-analytics-cli --test upload: 58 passed, including a new mock-server case that returns no ids and asserts the full short-link form (?repo= included) comes back
  • --test test: 13, --test validate: 9, cli lib: 23, test_utils: 1
  • cargo clippy adds no warnings (304 before and after); cargo fmt clean for every file touched — the one pre-existing diff in context/src/junit/parser.rs is on main too

🤖 Generated with Claude Code

…inks

The report table's per-test links now use the ADR-0019 GUID — a UUIDv8 hash of
`(test_collection_id, repo_id, test_case_id)` — addressing the canonical
`collections/<short_id>/tests/<guid>` page directly instead of the `/t/<id>`
short link the webapp has to resolve a repo for and redirect.

The two server-owned legs of the tuple come from `createBundleUpload`, which
now returns `repoId` and `testCollectionId` (trunk-io/trunk#33761). Both are
`Option` with `serde(default)`: a server predating that PR sends neither,
`testCollectionId` is absent under maintenance mode and for orgs with the
receipt-inserts flag off, and the CLI must keep working against all of them.

Every way the GUID can be unavailable funnels through one decision point in
`url_for_test_case` and falls back to today's link:

- no collection short id, or links hidden
- the server returned no ids
- `test_case.id` is not a UUID — a report-supplied id passes through verbatim
  (RSpec emits `trunk:<id>-<location>`), and hashing one would mint a
  well-formed GUID that addresses nothing

`gen_test_case_guid` is the same compiled `context` crate ingest runs, so there
is no second implementation of the frozen hash. The URL test pins ADR-0019's
first golden vector rather than recomputing the hash, so a drift in either the
contract or the URL shape fails the build.

The pre-upload `log_failure` lane still prints the short link: it runs before
`createBundleUpload`, so the ids do not exist yet. Hoisting that call is the
follow-up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@trunk-io

trunk-io Bot commented Sep 10, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@TylerJang27 TylerJang27 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Forcing a CI rerun to see it in practice

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants