Skip to content

[OPIK-8125] [QA] Proposed e2e spec from the #8031 exploration: untruncated experiment-comparison export - #8036

Draft
CometActions wants to merge 1 commit into
mainfrom
comet-qa-bot/OPIK-8125/e2e-compare-export-truncation
Draft

[OPIK-8125] [QA] Proposed e2e spec from the #8031 exploration: untruncated experiment-comparison export#8036
CometActions wants to merge 1 commit into
mainfrom
comet-qa-bot/OPIK-8125/e2e-compare-export-truncation

Conversation

@CometActions

@CometActions CometActions commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Generated by the release QA side flow (test-proposal step). It is a draft and needs human review before merge — nothing here has been reviewed by a person.

Where this came from

Exploratory testing of opik#8031 ([OPIK-8125] [FE] fix: export experiment items without truncation). A human-driven exploration verified the flow end to end on that PR's own deployed environment (pr-8031.dev.comet.com, 2.2.42-8031-merge-3115, OSS install, workspace default) before any spec was written. This PR turns the one strong candidate from that exploration into a permanent spec.

Base note. The task I was given said #8031 was still open with its head branch alive, and told me to target jacques/OPIK-8125-export-truncation in that case. That snapshot was stale: #8031 merged at 2026-08-27T09:38:47Z as bfa4ced0ef7217c5f98af301c369700b8efd22d1, and the head branch has since been deleted (git ls-remote --heads origin no longer lists it). So this targets main, where the fix already lives. The spec was written against the PR's head commit 96dec07ec2335de21a0516aa32f9553cdd6e776d and then rebased onto current main (472c30818) before being run again — see "Verification" for exactly what ran where.

What the spec covers

tests_end_to_end/e2e/tests/experiments/compare-export-truncation.spec.ts@t2-cuj, @area:experiments, @cap:experiments.export-comparison (previously covered: false).

One test: exporting the selected rows writes the whole value the grid shows truncated.

The change under test is a single React Query param — the export refetch now passes truncate: false instead of inheriting the table's flag — so the only way to see it is to press Export and read the file. What makes it worth a permanent test is that it is silent: the grid renders identically in the broken and the fixed build (it is supposed to show truncated text), nothing errors, nothing is missing, and a 1 000-character answer is long enough to look complete. The defect only exists inside a downloaded file, on the path people take precisely when they want the data whole.

What it asserts, in order:

  1. The fixture discriminates, through the API, before a browser is opened: the same rows read with truncate=false come back byte-identical to the seed, and read with truncate=true come back cut. Without this a green export assertion would prove nothing — a seed that fell under both limits round-trips whole through a broken build too.
  2. The grid itself still renders the truncated copy, on both axes.
  3. Selecting two of the three rows and exporting JSON yields exactly two rows, whose dataset.input and output.output are string-equal to the seeded values, with no ... suffix anywhere and the unselected row absent.
  4. The same for CSV: a header plus exactly two rows, each seeded string present verbatim, the unselected row's input absent.
  5. Each export issued its own compare-items read carrying truncate=false, rather than reusing the grid's truncated one — the regression asserted at its cause as well as its symptom, following the pattern experiment-logs-date-window.spec.ts already uses in this area.

Two truncation limits, not one. They are separate mechanisms and the fix has to hold for both, so the fixture seeds both:

axis limit suffix seeded
experiment output (output.output) TruncationUtils.DEFAULT_SLIM_STRING_MAX_LENGTH = 1 000 ... (so a cut value is 1 003 chars) ~2 400 and ~11 500 chars
dataset item data map (dataset.input) responseFormatting.truncationSize = 10 001 none — a cut value looks perfectly ordinary ~2 000 (under, control) and ~11 500 (over)

A 2 000-character dataset input is not truncated, so a spec that only seeded that would have passed against the pre-fix build on the dataset axis. The long row is deliberately kept under the dataset limit to act as that control.

experiments-compare.spec.ts drives the same grid but seeds single-character outputs, so it would pass unchanged against a truncating export.

Supporting changes

  • fixtures/long-value-experiment.fixture.ts (new) — one experiment over three dataset items straddling both limits. Seeded through the SDK bridge (dataset) and the REST client (traces + experiment linkage), the same shape jsonOutputExperiment uses and for the same reason. It asserts its own seed lengths against both limits before writing, so a filler that came up short fails loudly instead of leaving a spec that cannot fail. Teardown lives in the fixture (experiment → dataset → traces) and honours shouldLeaveArtifacts. Chained between jsonOutputExperiment and groupedDataset.
  • core/backendlistCompareItems(), the compare-items read with truncate under the caller's control and the payloads kept. The existing listCompareItemIds() hard-codes truncate: true because it only asserts order; here the payload is the answer.
  • pom/compare-experiments.page.tsselectItemRow(), readCellText() (by row id + column id, never by position — column order is user-configurable and persisted), and exportAs('CSV' | 'JSON'), which drives the toolbar menu and returns the captured download's contents.
  • coverage/taxonomy.yaml — spec added to the experiments specs: list; export-comparison flipped to covered: true, tier: t2-cuj.

Verification

Run from tests_end_to_end/e2e/ against the exploration's environment, OPIK_DEPLOYMENT=oss, OPIK_BASE_URL=https://pr-8031.dev.comet.com, workspace default.

check command result
the new spec npx playwright test tests/experiments/compare-export-truncation.spec.ts 1 passed (7.0s)
the whole area (shared POM touched) npx playwright test tests/experiments/ 10 passed (58.7s)
tags python3 tests_end_to_end/coverage/tag_lint.py --taxonomy … --estate tests_end_to_end 52 specs checked, 1 exempt, 0 problem(s)
types npx tsc --noEmit 0 errors — but see the caveat below

Both Playwright runs above were executed after rebasing onto current main, so the estate code under test is main's.

Two honest caveats about "against main":

  1. The application those runs hit is the PR's own deployment, which carries this fix but was built before 472c30818 ([OPIK-8109] [BE] fix: bound dataset items page read by page size) — the one commit to land on main since. That change rewrites SELECT_DATASET_ITEM_VERSIONS, a different query path from the compare-items endpoint this spec reads, so it is not expected to interact; but I could not deploy main itself, so that is reasoning, not a run. The first CI run on this PR is the real check.
  2. npx tsc --noEmit as documented in .agents/skills/writing-e2e-tests/SKILL.md currently fails on main before reaching any source file: tsconfig.json(13,5): error TS5102: Option 'baseUrl' has been removed, from the pinned typescript@7.0.2. I got the 0-error result above by extending the repo tsconfig with paths rewritten and types: ["node"], which typechecks the whole estate cleanly, including these files. This is pre-existing and not caused by this PR — it reproduces on unmodified main — but it means the skill's own typecheck step is currently a no-op for everyone, and it is worth fixing separately.

What I deliberately did not write

The exploration produced two candidates. One was written; one was dropped:

  • "Export is identical whether or not workspace table truncation is enabled" (marked weak by the exploration). It is the tightest regression-lock available — the pre-fix export changed with the workspace preference, and the exploration confirmed the CSV and JSON download byte-identical (matching md5) with truncation on and off. It is dropped because driving it needs a workspace-level preference flip, which is shared state that this suite's per-test fixtures do not isolate: it would corrupt any spec running in parallel against the same workspace, and leave the workspace dirty if the test died mid-flip. It also largely duplicates the assertion the shipped spec already makes. Worth revisiting if the estate grows a safe way to scope a workspace preference to one test.

Follow-up worth doing (not done here)

ExportToButton renders an icon-only trigger with no accessible name and no data-testid, so the POM addresses it by the icon it renders, guarded with toHaveCount(1). A data-testid on that shared component is the better handle and conventions ask for one in the same change — I did not add it because this spec had to be run against an already-built deployment, where a brand-new front-end attribute does not exist, and an unrunnable spec is worse than a CSS selector. A reviewer adding that attribute and switching the POM to getByTestId in a follow-up would be a straight improvement.


Source PR: comet-ml/opik#8031 · OPIK-8125

…ison export

Covers experiments.export-comparison, which was uncovered: exporting the
selected rows of the experiment-comparison grid must download the whole
value, not the truncated copy the grid renders (opik#8031).

The defect this locks is silent — the grid looks identical either way,
because it is supposed to show truncated text, and the only place a
truncating export shows up is inside a downloaded file, on the path people
take precisely when they want the data whole.

Two limits are exercised, because they are separate mechanisms and the fix
has to hold for both: the experiment output is slimmed at 1 000 characters
with a "..." suffix, and the dataset item's data map is cut at 10 001 with
no suffix at all.

- fixtures/long-value-experiment.fixture.ts: one experiment over three
  dataset items whose values straddle both limits, seeded through the SDK
  bridge and the REST client, torn down after use().
- core/backend: listCompareItems(), the compare-items read with `truncate`
  under the caller's control, so the spec can prove the truncated read
  really does cut these values before it opens a browser.
- pom/compare-experiments.page.ts: row selection, cell text by row+column
  id, and Export → CSV/JSON with the download captured.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot added tests Including test files, or tests related like configuration. typescript *.ts *.tsx labels Aug 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📋 PR Linter Failed

Missing Section. The description is missing the ## Details section.


Missing Section. The description is missing the ## Change checklist section.


Missing Section. The description is missing the ## Issues section.


Missing Section. The description is missing the ## Testing section.


Missing Section. The description is missing the ## Documentation section.

@github-actions

Copy link
Copy Markdown
Contributor

⏱️ pre-commit per-hook timing

No linted files changed — nothing to run.

⏭️ 44 skipped (no matching files changed)
Hook Description Result
🐍 trim trailing whitespace — python sdk Strip trailing whitespace ⏭️
🐍 fix end of files — python sdk Ensure files end in a newline ⏭️
🐍 ruff — python sdk Lint + autofix Python (ruff) ⏭️
🐍 ruff-format — python sdk Format Python code (ruff) ⏭️
🐍 mypy — python sdk Static type check ⏭️
🤖 trim trailing whitespace — optimizer Strip trailing whitespace ⏭️
🤖 fix end of files — optimizer Ensure files end in a newline ⏭️
🤖 check yaml — optimizer Validate YAML syntax ⏭️
🤖 check json — optimizer Validate JSON syntax ⏭️
🤖 check toml — optimizer Validate TOML syntax ⏭️
🤖 check for added large files — optimizer Block large files (>1MB) ⏭️
🔐 detect private key — optimizer Block committed private keys ⏭️
🤖 check for merge conflicts — optimizer Block merge-conflict markers ⏭️
🤖 check for case conflicts — optimizer Block case-only name clashes ⏭️
🤖 pyupgrade — optimizer Modernize Python syntax ⏭️
🤖 ruff — optimizer Lint + autofix Python (ruff) ⏭️
🤖 ruff-format — optimizer Format Python code (ruff) ⏭️
🤖 mypy — optimizer Static type check ⏭️
📓 nbstripout — optimizer notebooks Strip notebook output ⏭️
📝 markdownlint — optimizer Lint Markdown ⏭️
🔤 codespell — optimizer Fix common misspellings ⏭️
📊 radon cc — optimizer Cyclomatic-complexity gate ⏭️
📊 radon raw — optimizer Raw size metrics gate ⏭️
📊 xenon — optimizer Fail on complexity thresholds ⏭️
📊 lizard — optimizer Cyclomatic-complexity gate ⏭️
🧹 vulture — optimizer Find dead code ⏭️
🛡️ trim trailing whitespace — guardrails Strip trailing whitespace ⏭️
🛡️ fix end of files — guardrails Ensure files end in a newline ⏭️
🛡️ ruff — guardrails Lint + autofix Python (ruff) ⏭️
🛡️ ruff-format — guardrails Format Python code (ruff) ⏭️
🛡️ mypy — guardrails Static type check ⏭️
⚓ helm-docs Regenerate Helm chart README ⏭️
block non-public FE plugins Block non-public FE plugins ⏭️
☕ spotless — java backend Format Java code ⏭️
🧪 pre-commit wrapper smoke tests Self-test the wrapper scripts ⏭️
🧪 rebaseline script tests Self-test the changelog re-baseline script ⏭️
🌐 eslint — frontend Lint + autofix JS/TS ⏭️
🌐 typecheck — frontend Whole-project tsc type check ⏭️
📘 eslint — typescript sdk Lint + autofix JS/TS ⏭️
📘 typecheck — typescript sdk Whole-project tsc type check ⏭️
⚙️ actionlint — github workflows Lint GitHub Actions workflows ⏭️
🐳 hadolint — dockerfiles Lint Dockerfiles ⏭️
🌈 zizmor — github workflows security Security-scan GitHub Actions workflows ⏭️
🛡️ semgrep — java backend sql Block SQL injection-prone string formatting ⏭️

Comment on lines +240 to +245
* The trigger is an icon-only button with no accessible name and no
* `data-testid`, so it is addressed by the icon it renders — its identity, not
* its position. A `data-testid` on `ExportToButton` would be the better handle
* and is worth adding; it is not added here because this spec has to run
* against already-built deployments, where a fresh front-end attribute does
* not exist yet. `toHaveCount(1)` guards against the locator widening.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Brittle export selector violates E2E contract

ExportToButton is located via structural CSS and svg.lucide-download, so icon, class, or menu-structure changes can break the export test while behavior still works — should we add a descriptive kebab-case data-testid to its trigger and use page.getByTestId(...), as .agents/skills/writing-e2e-tests/conventions.md and .agents/skills/playwright-pom-discovery/SKILL.md require?

Severity

Want Baz to fix this for you? Activate Fixer You can also update your AI coding guidelines based on this comment by apply pr to [branch name]

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
tests_end_to_end/e2e/pom/compare-experiments.page.ts around lines 240-245, update the
exportAs method to stop locating the Export menu trigger with structural CSS and
svg.lucide-download. Add a descriptive kebab-case data-testid to the ExportToButton
trigger in its shared component, then locate it with page.getByTestId(...) while
retaining the uniqueness and enabled-state assertions. Update the surrounding comment to
document the stable test ID instead of explaining why one is unavailable.

Comment on lines +19 to +21
export const SLIM_STRING_MAX_LENGTH = 1_000;
export const SLIM_TRUNCATION_SUFFIX = '...';
export const DATASET_TRUNCATION_SIZE = 10_001;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-default deployments fail truncation spec

DATASET_TRUNCATION_SIZE is assumed to be fixed, but config.yml defaults responseFormatting.truncationSize from OPIK_RESPONSE_TRUNCATION_CHAR_LIMIT, and DatasetItemVersionDAO.getItemsWithExperimentItems uses that runtime value for truncate=true while the e2e loader neither sets nor discovers it, so mismatched deployments seed artifacts before compare-export-truncation.spec.ts fails — should we resolve the active limit or make it explicit and validate it before seeding?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
tests_end_to_end/e2e/fixtures/long-value-experiment.fixture.ts around lines 19-21,
update the `DATASET_TRUNCATION_SIZE` and seed-validation logic so it does not assume the
backend limit is always 10,001. Make the fixture receive or discover the active
`responseFormatting.truncationSize`/`OPIK_RESPONSE_TRUNCATION_CHAR_LIMIT`, or require an
explicit test configuration, and validate that value before creating any artifacts.
Ensure the generated seed values and downstream assertions use the validated runtime
threshold and fail with a clear configuration-mismatch error.

Comment on lines +164 to +175
const dataset = await sdkClient.python.createDataset({
project_name: project.name,
name: datasetName,
description: 'Export of experiment comparison rows must not truncate',
items: SEED.map((row) => ({
label: row.label,
input: row.input,
expected_output: row.label,
})) as unknown as Array<Record<string, unknown>>,
});

// Read the ids back rather than generating them, and key them by the label

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Fixture setup leaks large test artifacts

A rejection from getDatasetItems or the creation helpers (createTraceWithSource, createExperiment, or createExperimentItems) occurs before await use(ref), so teardown is skipped and successfully created entities remain behind — should we register rollback actions after each creation or use an independent cleanup fixture while preserving the experiment → dataset → traces order?

Severity web_search

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
tests_end_to_end/e2e/fixtures/long-value-experiment.fixture.ts around lines 164-175,
update the `longValueExperiment` fixture setup so failures in dataset reads, trace
creation, experiment creation, or experiment-item linking cannot leak already-created
backend entities before `await use(ref)`. Register rollback actions immediately after
each successful creation, or move cleanup into an independently running teardown
fixture, and ensure partial trace creation is handled. Preserve the existing ordered
cleanup of experiment, then dataset, then traces.

Comment on lines +258 to +263
await use(ref);

if (!shouldLeaveArtifacts(testInfo)) {
const safe = async (what: string, fn: () => Promise<unknown>): Promise<void> => {
try {
await fn();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Fixture setup leaks partial test artifacts

A failure in getDatasetItems, createTraceWithSource, createExperiment, createExperimentItems, or a later attachment exits before cleanup, so earlier entities remain in the workspace. Could we register each successful entity immediately and perform best-effort reverse-order deletion from finally/fixture teardown while retaining shouldLeaveArtifacts(testInfo), as .agents/skills/writing-e2e-tests/conventions.md requires?

Severity

Want Baz to fix this for you? Activate Fixer You can also update your AI coding guidelines based on this comment by apply pr to [branch name]

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`tests_end_to_end/e2e/fixtures/long-value-experiment.fixture.ts` around lines 258-263,
refactor the `longValueExperiment` fixture lifecycle so cleanup cannot be skipped when
`getDatasetItems`, trace/experiment creation, experiment-item creation, or attachment
setup throws. Register each successfully created entity immediately and execute
best-effort deletion in reverse dependency order from a `finally`-based teardown path,
while retaining the `shouldLeaveArtifacts(testInfo)` guard so preserved-artifact runs
remain unchanged.

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

Labels

baz: pending 🔴 size/XL tests Including test files, or tests related like configuration. typescript *.ts *.tsx

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant