Skip to content

[OPIK-8262] [QA] Proposed e2e spec from the #8162 exploration: thread-scoped rule fan-out - #8167

Draft
CometActions wants to merge 2 commits into
thiagohora/OPIK-8262-retry-classification-and-fanoutfrom
comet-qa-bot/OPIK-8262/thread-fanout-e2e
Draft

[OPIK-8262] [QA] Proposed e2e spec from the #8162 exploration: thread-scoped rule fan-out#8167
CometActions wants to merge 2 commits into
thiagohora/OPIK-8262-retry-classification-and-fanoutfrom
comet-qa-bot/OPIK-8262/thread-fanout-e2e

Conversation

@CometActions

@CometActions CometActions commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

What this is

One new e2e spec proposed by the QA test-radar side flow, written from a human-verified
exploration of #8162 (OPIK-8262 classify provider errors by status and stop collapsing
trace-thread fan-out).

This PR targets thiagohora/OPIK-8262-retry-classification-and-fanout, not main. The spec was
written and run against that branch's head, c20c9e018a23b9fc45927d0224c80959cc7208ae, which is
also the commit deployed to the PR environment the exploration used. It is a regression guard on
the fan-out this PR rewrote, so it belongs on top of the change it tests; retargeting it to main
once #8162 merges is a rebase, not a rewrite.

Generated by an automated QA flow. It needs human review before merge and is deliberately a
draft.
No reviewers requested — a human promotes it.

Where it came from

The exploration ran on the PR's own deployment (pr-8162.dev.comet.com, 2.2.53-8162-merge-3189,
OSS install, workspace default) and reached both halves of the change by hand before any spec
existed. It produced two strong candidates; this PR contains the one of them that can be written
as a deterministic, self-contained spec. The other is dropped, with reasons, below.

The spec

tests/online-evaluation/online-evaluation-thread-fanout.spec.ts — ✅ passes

@t2-cuj · @area:online-evaluation · @cap:online-evaluation.rule-scope-thread-span

What it asserts. A manual evaluation naming N threads against one thread-scoped rule becomes
N independent scoring cycles — the behaviour OnlineScorePublisher.enqueueThreadMessage now
produces by emitting one stream entry per thread id instead of one entry carrying the whole list.
Concretely, over a 3-thread cohort:

  • the request answers entities_queued: 3, rules_applied: 1;
  • the rule's log stream carries exactly 3 "to Python evaluator" lines — not 1 (an id lost in
    the split) and not 9 (each entry still carrying all three) — and each thread id appears in
    exactly one of them;
  • every thread carries exactly one feedback score, value 1.0, source: online_scoring, compared
    as the whole score set per thread rather than by looking this rule's score up among others;
  • the project holds exactly the three seeded threads and nothing else;
  • each of the three threads renders that score in its detail panel's Feedback scores tab.

The evaluator-call count is the load-bearing assertion. All three failure modes end with every
thread carrying a score — the duplicate case overwrites its own value — so the scores alone cannot
tell a correct fan-out from a collapsed one. Only the call count can.

Why it is deterministic. A constant-1.0 python thread metric: no provider key, no model
verdict, no wall-clock dependence. The rule sits at sampling_rate: 0, so the production sampler
can never fire it and the manual request is the only possible trigger — and the spec asserts every
thread is unscored before the request, so the later assertions are attributable to the fan-out
rather than to ordinary sampling.

What it does not prove. On the happy path the pre-#8162 code also scored all three threads: it
iterated the id list inside a single stream entry. So this is a regression guard on the split
(nothing lost, nothing duplicated), not a before/after discriminator. The behaviour only the
new code has — a per-thread retry budget, so one thread's retryable failure replays only that
thread — needs a retryable failure in the thread scorer, which the exploration could not provoke on
this estate (the python evaluator's failures are terminal 400s, and the thread LLM path needs a
provider key). It is deliberately out of scope here rather than asserted weakly.

Supporting changes

  • fixtures/thread-cohort.fixture.ts (new) — seeds three independent threads (2 turns each)
    through the public SDK bridge and resolves each to its thread_model_id, which is what
    manual-evaluation/threads addresses. It polls until all three threads have been aggregated and
    throws naming the missing ones if they never are: a spec handed a short cohort would assert
    over whatever arrived and pass having tested a smaller fan-out than it claims. No teardown — the
    traces, threads and scores all live in the project fixture's project, and the rule is cleaned
    up by the existing automationRulesCleanup fixture. The suite's own run-prefix sweep reported
    nothing left behind after the verification run.
  • core/backend/client.tscreateAutomationRule gains a type so it can build a
    trace_thread_user_defined_metric_python rule (thread code carries a metric and no argument
    map; passing one is now a hard error rather than a field sent and ignored); new
    evaluateThreadsManually, which the pinned SDK cannot express at all; ThreadRowRef gains
    threadModelId.
  • core/metrics/python-metric-source.tsbuildConstantThreadScoreMetric. A separate builder
    from buildConstantScoreMetric because the python backend dispatches the two differently:
    metric.score(data) positionally for trace_thread_* rules, metric.score(**data) for
    trace-scoped ones.
  • coverage/taxonomy.yaml — spec added to the area's specs: list;
    online-evaluation.rule-scope-thread-span flipped to covered: true, tier: t2-cuj with a note
    scoping it to thread scope only — no spec creates a span-scoped rule, so that half stays
    untested and the note says so rather than letting the key read as fully covered.
    online-evaluation.automation-logs is deliberately left covered: false: this spec reads the
    rule log stream over GET /automations/evaluators/{id}/logs and never opens the
    /$workspaceName/automation-logs page that key names, which is exactly the distinction the
    existing comment on that entry asks callers to respect.

Verification

Run against the PR's own deployment — the same environment and build the exploration used:

cd tests_end_to_end/e2e
OPIK_DEPLOYMENT=oss OPIK_BASE_URL=https://pr-8162.dev.comet.com OPIK_WORKSPACE=default \
OPIK_API_KEY=oss-no-auth WORKERS=1 \
  npx playwright test tests/online-evaluation/online-evaluation-thread-fanout.spec.ts --reporter=list
# 1 passed (24.0s)

OPIK_API_KEY is set only because this target's hostname ends in comet.com, which makes the
pinned TS SDK demand a key even though the install has no auth. It is not needed for a local OSS
run and the spec does not depend on it.

Also run:

  • python3 tests_end_to_end/coverage/tag_lint.py --taxonomy tests_end_to_end/coverage/taxonomy.yaml --estate tests_end_to_end60 specs checked, 1 exempt, 0 problem(s).

  • A deliberate mutation of the evaluator-call assertion (34) was run and failed as
    intended, so that assertion is known to discriminate rather than merely to pass.

  • tests/online-evaluation/ in full, because core/backend/client.ts and fixtures/index.ts
    are shared: 7 passed, 1 skipped, 1 failed (1.7m).

    The one failure is online-evaluation-python-metric-errors.spec.ts, and it is not caused by
    this change
    — it fails identically on the unmodified branch, verified by re-running it with
    these changes stashed. On this deployment a python metric that exits 0 without a result line is
    still reported as Python evaluation failed (HTTP '500'): 500 Internal Server Error: Failed to execute code, where that spec expects the classified 400 Bad Request: Execution failed: the metric produced no output. Worth someone's attention — either the classification fix is not in
    this build's python backend, or it has regressed — but it is out of scope here and nothing in
    this PR touches it.

Typecheck, with a caveat you should know about. npx tsc --noEmit does not run on this branch
today, and did not before this change: tsconfig.json still sets baseUrl, which the pinned
typescript@^7.0.2 removed (error TS5102). It was typechecked with TypeScript 5.9 instead, which
reports exactly one error — a pre-existing duplicate deleteDashboard in core/backend/client.ts
present on the unmodified branch too. Zero new errors from this change. Both are worth fixing, but
not in a QA spec PR.

What was deliberately not written

The exploration's other strong candidate — "a permanent provider failure is attempted once; a
transient one is retried"
, the retry-classification half of this PR — is dropped. It is the
more valuable of the two and it was verified by hand (401 → 1 delivery held over 30 minutes;
429 → 3 deliveries at exact 10-minute intervals), but it cannot currently be written as a spec this
suite should own:

  1. It needs a backend-reachable endpoint that answers a chosen HTTP status. The suite's only
    such facility is the mock gateway in services/mock-token-auth/, which answers 401 only and,
    per core/mock-auth.ts's mockAuthSkipReason(), is reachable only from a local backend —
    never from a deployment, including the one where this behaviour can be observed. The
    exploration's substitute was https://httpbingo.org/status/<code>?x=; a permanent spec whose
    verdict depends on a third-party service is a flake source and is ruled out by the estate's own
    determinism rule.
  2. Even with that endpoint, the discriminating assertion costs a full
    onlineScoring.pendingMessageDuration
    — 10 minutes, deployment configuration, not settable
    from a test. Both halves of the pair need it: without waiting out a redelivery window, "attempted
    once" and "attempted once so far" are the same observation.

Asserting only the permanent half would be worse than nothing: it would pass equally well if every
provider error had been made non-retryable, which is the direction that silently loses evaluations.

The prerequisite is a small one — teach services/mock-token-auth/mock_token_auth_service.py to
answer a caller-chosen status, then write the spec against mockGatewayUrlForBackend on a local
OSS run where REDIS_SCORING_PENDING_MESSAGE_DURATION can be turned down. That is a change worth
making, and it needs a local OSS backend to verify on, which this flow did not have. Shipping the
spec unverified against that unbuilt facility would have been a guess.

Also not covered, and named here so the list reads as filtered rather than exhausted: the automatic
thread-close fan-out path (TraceThreadOnlineScorerPublisher — same split, same scorer, different
caller; gated behind a 15-minute inactivity timeout), the VertexAI/GAX branch, and the legacy
multi-id migrate branch. The exploration could not reach any of them either.

Review notes

  • Read against .agents/skills/writing-e2e-tests/SKILL.md and conventions.md on main. One step
    of that workflow was not run: step 3, live-UI discovery via the Playwright MCP, which is not
    available to this flow. No new page object was written to compensate — the spec drives the
    existing LogsPage and ThreadPanelPage only, and adds no new selector.
  • The Threads table's own feedback-score column is not asserted. Enabling it needs a
    column-picker interaction no POM in the estate models, and the thread panel's Feedback scores tab
    is where a user actually reads a thread-level score (the table hides those columns by default) —
    so the spec asserts there, for all three threads rather than a sample.

Related: #8162

Proposed by the QA test-radar side flow from a human-verified exploration
of #8162, and written against that PR's head.

A manual evaluation naming N threads against one thread-scoped rule must
become N independent scoring cycles -- the behaviour enqueueThreadMessage
now produces by emitting one stream entry per thread id rather than one
entry carrying the whole list. The spec asserts entities_queued, exactly
one evaluator call per thread (not 1, not 9), the score on every thread
server-side, and the score rendered in every thread's panel.

The evaluator-call count is the load-bearing assertion: all three
outcomes -- correct, collapsed, duplicated -- end with every thread
carrying a score, because the duplicate case overwrites its own value.

Supporting: a thread-cohort fixture that seeds three conversations and
resolves their thread model ids (throwing if any never aggregates), a
thread-scoped variant of the constant-score python metric, and backend
client support for trace_thread_user_defined_metric_python rules and
POST /v1/private/manual-evaluation/threads.

Taxonomy: online-evaluation.rule-scope-thread-span flipped to covered
for THREAD scope only; span scope stays untested and the note says so.
automation-logs deliberately stays false -- this spec reads the log
stream over the API and never opens the page that key names.

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 🟠 size/L labels Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

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

github-actions Bot commented Sep 4, 2026

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 +1532 to +1543
arguments?: Record<string, string>;
/**
* Which evaluator the rule is. Defaults to the trace-scoped python
* metric; `trace_thread_user_defined_metric_python` is the thread-scoped
* one, which fires per conversation rather than per trace.
*/
type?: 'user_defined_metric_python' | 'trace_thread_user_defined_metric_python';
triggerScope?: 'production' | 'experiment' | 'both';
enabled?: boolean;
}): Promise<string> {
const type = args.type ?? 'user_defined_metric_python';
const isThreadScoped = type === 'trace_thread_user_defined_metric_python';

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.

Empty arguments bypass local validation

The trace-scoped guard accepts an explicitly empty arguments object because it checks only truthiness, so a valid TypeScript call reaches rawFetch with code.arguments: {} and PythonEvaluatorService.evaluate rejects the empty data map before the metric runs — should we reject empty maps with Object.keys(args.arguments ?? {}).length === 0 and add a regression test that rawFetch is not called?

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/core/backend/client.ts` around lines 1532-1543, update
`createAutomationRule` so trace-scoped evaluators reject `arguments` when it is missing
or an empty object, not just when it is falsy. Validate the map by checking its key
count before calling `rawFetch`, while preserving the thread-scoped restriction. Add a
regression test asserting that an empty argument map throws and `rawFetch` is not
called.

Comment on lines 1728 to +1730
threads: (page.content ?? []).map((t) => ({
id: String(t.id ?? ''),
threadModelId: t.threadModelId ?? null,

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.

Prefilter test misses wrong thread UUID

listThreads exposes threadModelId, but comparable() in thread-id-prefilter.spec.ts omits it, so EQUAL, CONTAINS, and windowed differential checks can pass with incorrect model IDs — should we add threadModelId: row.threadModelId to the projection?

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/core/backend/client.ts` around lines 1728-1730, ensure the new
`listThreads` `threadModelId` field is covered by the shared `comparable()` projection
used by `thread-id-prefilter.spec.ts`. Add `row.threadModelId` alongside the other
projected thread fields so EQUAL, CONTAINS, and windowed differential assertions detect
incorrect or missing operational UUIDs.

Comment on lines +81 to +84
// The first place the split is observable. A request that collapsed to a
// single entry still answers 202.
expect(queued.entitiesQueued, 'one entry per thread named in the request').toBe(
threads.length,

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.

Queued count falsely proves fan-out

entitiesQueued counts submitted entities because evaluateThreads returns threadModelIds.size() while discarding enqueueThreadMessage's Mono<Void>, so packing all three IDs into one Redis entry still returns 3 and passes the assertion. Could we keep it as confirmation that three entities were accepted and update the comment/assertion message to use the evaluator-call/per-thread log assertions as fan-out evidence instead?

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/tests/online-evaluation/online-evaluation-thread-fanout.spec.ts`
around lines 81-84, update the `entitiesQueued` assertion in the manual evaluation step
because it confirms only that the requested entities were accepted, not that one Redis
stream entry was created per thread. Keep the assertion as an accepted-entity-count
check, but revise the comment and assertion message so they do not call it evidence of
splitting; rely on the evaluator-call and per-thread log assertions later in the test
for fan-out verification.

… assertions

Review of the thread fan-out spec. Three contained readability fixes; no
behaviour change:

- state the 300s timeout budget, as both neighbouring online-evaluation
  specs do, and why it sits just above the inner 180s poll
- give the cohort set-comparison and the per-thread panel score assertion
  failure messages, so a 3am failure names which thread rendered wrong
  rather than only "expected 1, received 0" inside a loop

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@CometActions

CometActions commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator Author

🔍 Generated-test review

PR #8167OPIK-8262 [QA] Proposed e2e spec from the #8162 exploration: thread-scoped rule fan-out

CometActions · draft: yes · 7 files, 1 new spec

VERDICT: ready with notes — one taxonomy decision needs a human before merge.

The spec is genuinely good. It asserts the thing its tag claims, its teardown is in
fixtures, its POM reuse is correct, and — unusually — its log-string assertions
verify against the real backend source. I applied three readability fixes and found
nothing to fix on suite integrity or reliability. The one open item is a coverage
denominator question I am not allowed to decide.


Blockers (0)

None.


Should fix (1) — needs a human decision, not fixable by me

1. rule-scope-thread-span flips to covered: true on thread scope alone —
tests_end_to_end/coverage/taxonomy.yaml:659

The capability key names two scopes. The spec covers thread scope only; the PR
says so candidly, in both a note: and a five-line comment. But the coverage
builder reads covered: true, so span-scoped rules — which no spec anywhere
creates — become invisible to the radar permanently. That is the estate's own
"permanent false green", applied to half a capability.

This is exactly the case the brief forbids me to fix myself: splitting the key into
rule-scope-thread and rule-scope-span would be inventing a taxonomy capability,
and TESTING-TAGS.md is explicit that the taxonomy "is a reviewed file: it
defines 100%". So the choice is yours:

  • (a) split into rule-scope-thread (covered) and rule-scope-span
    (covered: false) — accurate denominator, coverage dips by design, and the radar
    keeps surfacing span scope as a gap; or
  • (b) keep it as-is and accept the scoping note: as the record — precedent
    exists (sampling-rate, python-rule-scores, thread-level-metrics all carry
    scoping notes on covered entries), at the cost of the radar never raising span
    scope again.

I lean (a), because a note is prose that no tool reads, and span scope is a real
untested surface rather than a nuance of a tested one. But it changes the
denominator, so it is your call.


Notes (2)

1. The tsc gate failure is not this PR's, and the gate is currently vacuous.

tsconfig.json(13,5): error TS5102: Option 'baseUrl' has been removed — the repo
pins typescript@^7.0.2 (resolved 7.0.2), which removed baseUrl. tsconfig.json
is byte-identical to origin/main and is not in this PR's diff. Because this is a
config-level error, tsc aborts before type-checking anything, so the gate has been
reporting FAIL without checking a single file — for every PR, not just this one.

I re-ran the typecheck against a repaired config (baseUrl dropped, paths made
tsconfig-relative, types: ["node"]) to get a real answer:

Both fixes are estate-wide and belong in their own PR, not stapled to a test PR —
repairing the config would immediately surface the duplicate-method error and drag
unrelated repair work in. Happy to open that PR (drop baseUrl, add
types: ["node"], dedupe deleteDashboard) — say the word. It is worth doing
soon: the e2e suite has effectively had no typecheck since the TS 7 bump.

2. The PR is stacked on an unmerged backend branch.

Base is thiagohora/OPIK-8262-retry-classification-and-fanout (BE commit
c20c9e018a, "classify provider errors by status and split thread fan-out"), which
is not on main yet. The head is 0 commits behind its base, so nothing to merge
in. But this spec asserts the fan-out that BE commit implements, so it cannot be
retargeted to main until that lands. Legitimate stacking, not a defect — just
sequence it, and retarget after the BE PR merges.


What I verified (and how)

Check Result
Placement tests/online-evaluation/ = @area:online-evaluation = spec_dir: online-evaluation. ✅ Spec is added to the area's specs: list.
Tag key exists online-evaluation.rule-scope-thread-span is real, not invented. Tags are string literals, not computed.
Tag honesty The test creates a trace_thread_user_defined_metric_python rule and asserts per-thread evaluator calls, server-side scores, and UI rendering. If thread-scoped fan-out broke, this fails. Scope caveat is the should-fix above.
Tier @t2-cuj correct — no LLM spend (constant python metric, no provider key), but up to 300s. No tier inflation.
Fixed sleeps The 50ms turn-spacing sleep (thread-cohort.fixture.ts:84) is the estate's existing convention for the same reason — identical lines in conversation.fixture.ts:52 and evaluated-thread.fixture.ts:119. Not a finding. No waitForTimeout anywhere.
Teardown In fixtures (automationRulesCleanup, project), not the test body. No try/finally, no trailing cleanup step. ✅
Namespacing Entities derive from testNamespace (base.fixture.ts:39), so they match the sweep contract by construction.
Polls Both bounded, both with diagnostic messages naming what was missing.
Log-string assertions Verified against backend source, not assumed: OnlineScoringTraceThreadUserDefinedMetricPythonScorer.java:279 emits Sending threadId '{}' to Python evaluator… and :307 emits Scores for threadId '{}' stored successfully. The quoting and the fact that it is the thread_id string (not the model id) both match what the spec matches on. Log-text matching is also existing convention — online-evaluation-python-metric-errors.spec.ts:152 uses the same EVALUATOR_CALL_LINE constant.
Non-spec files (Step 9) All bucket 1/2 test support, no production code. evaluateThreadsManually + threadModelId on the client, a thread metric builder, the cohort fixture. No bucket-4 changes.
Blast radius createAutomationRule's arguments went required → optional, guarded at runtime. All 4 existing callers pass arguments and are unaffected; a trace-scoped caller that omits it now throws early instead of failing opaquely — an improvement. ThreadRowRef.threadModelId is purely additive; the other consumer (thread-id-prefilter.spec.ts) does no exhaustive object compare.
Fixture duplication Justified. The existing conversation fixture seeds one thread, and N=1 cannot distinguish a correct fan-out from a collapsed one. Not a near-duplicate.

Fixes applied and pushed

Commit e3b8adc906 on comet-qa-bot/OPIK-8262/thread-fanout-e2e — three contained
readability fixes, no behaviour change:

  1. Documented the test.setTimeout(300_000) budget and why it sits just above the
    inner 180s poll. Both neighbouring online-evaluation specs justify their timeout
    this way; this one did not.
  2. Gave the cohort set-comparison (:154) a failure message.
  3. Gave the per-thread panel score assertion (:178) a message naming the thread —
    it runs inside a loop, so toBeCloseTo alone would fail with
    "expected 1, received 0" and no indication of which thread.

Bot comments

bot-comments.json is an empty array — no bot review comments on this PR.
Nothing to triage, defer, or reply to.

# Finding Verdict Disposition
(none posted)

Estate gates

Re-run after my edits, all three:

tag_lint PASS · tsc FAIL (pre-existing, not this PR) · playwright --list PASS

  • tag_lint60 specs checked, 1 exempt, 0 problem(s), exit 0.
  • tsc — exit 1, but solely TS5102 baseUrl at tsconfig.json:13, unchanged from
    main and independent of this PR. Under a repaired config the PR's own files are
    clean; see Note 1.
  • playwright --list — collects, Total: 1 test in 1 file.

What I could not verify

  • I did not execute the spec. There is no running Opik stack in this workspace,
    so every runtime claim is static analysis plus source reading. In particular I
    could not confirm the timing budgets hold on a real box (the 60s thread-aggregation
    poll and the 180s scoring poll), nor that entities_queued is actually 3 rather
    than 1 — which is the PR's whole thesis.
  • Whether trace_thread_user_defined_metric_python is enabled on the target
    deployment.
    The spec's own poll message anticipates it being disabled. If it is,
    this spec fails at the 180s poll for an environmental reason.
  • The deleteDashboard duplicate's runtime impact. Two methods of that name in
    one object literal means the second silently wins. Pre-existing and out of scope
    here, but it is a live bug in the shared client, not just a type error.

Do not merge — and I have not marked it ready

Per the standing rule I have not flipped the draft flag and have not merged,
and would not have even with all gates green. The PR is in good shape and, once you
settle the rule-scope-thread-span question above, a human can mark it ready. Note
also it cannot target main until the BE branch it is stacked on lands.


review_generated_tests.yml ·
this PR stays a draft — a human decides when it is ready.

Comment on lines +42 to +47
// Budget for the longest chain: the cohort fixture's 60s thread-aggregation
// poll, the 180s wait for the rule to store scores, then three thread panels
// opened in sequence. Kept just above the 180s inner poll so that one fires
// first — it fails naming the threads that never got scored, which beats an
// opaque "test timeout exceeded".
test.setTimeout(300_000);

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.

300-second timeout cannot cover UI chain

The outer timeout leaves only 60s after fixture and scoring, while the three serial waitForFullyLoaded calls can consume 180s before navigation and assertions, so valid runs can terminate before the final panel assertion — should we increase the budget to cover the full chain and overhead?

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/tests/online-evaluation/online-evaluation-thread-fanout.spec.ts`
around lines 42-47, update the outer test timeout budget for the thread fanout scenario.
It must exceed the fixture and scoring phases (up to 240 seconds), all three serial
`waitForFullyLoaded` calls (up to 60 seconds each), and navigation/assertion overhead,
so a valid run cannot time out before the final panel assertion. Update the accompanying
comment to document the revised worst-case budget.

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

Labels

🟠 size/L 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