Skip to content

feat: dynamic result classification (backend)#344

Open
okt-konst wants to merge 30 commits into
ts-factory:mainfrom
okt-konst:feat/result-classification
Open

feat: dynamic result classification (backend)#344
okt-konst wants to merge 30 commits into
ts-factory:mainfrom
okt-konst:feat/result-classification

Conversation

@okt-konst

Copy link
Copy Markdown

Summary

Backend for dynamic result classification — marking/classifying failed test results so they're inherited by future runs and can suppress known failures from unexpected counts, coexisting with static TRC.

Core model:

  • IssueExt (external bug cache), Issue (global cause identity, open/closed lifecycle), IssueRule (per-project: category + tri-state expected + matcher + active flag), ResultClassification (per-result stamp, unique(result, rule)).
  • A result is effectively expected iff a stamp's rule.expected=True AND rule.issue.state='open'. This suppression predicate is centralized in one helper and applied across all run-stats / dashboard / history NOK-derivation seams.

API (interfaces/api_v2/classification.py): IssueViewSet / IssueRuleViewSet / ResultClassifyViewSet / apply-rules, plus:

  • classify a result (creates issue+rule+stamp), manual apply-rules-to-run, issue lifecycle (close/reopen with rule auto-deactivation).
  • issue picker (recent + title/key search), per-run issue summary + drill-down.
  • per-rule results and per-issue results (GET /issues/:id/results/, unions a issue's rules across tests).
  • tri-state expected (nullable): none marks an issue without changing disposition.
  • rules applied on import; run-stats invalidated on issue/rule/stamp change.

History gains category / issue / untriaged filters and exposes effective_expected + issues per result.

Test Plan

  • python manage.py test bublik.tests.test_classification{,_engine,_api,_readside}62 tests green against current upstream main (branch rebased onto latest).
  • Reviewer: CI green.

Rebased onto current main (no workflow changes). Frontend lives in a separate PR against ts-factory/bublik-ui.

okt-kostik added 28 commits July 7, 2026 10:32
…on models

Foundation for dynamic result classification: external bug cache (IssueExt),
global cause identity (Issue), per-project triage decision with category and
expected flag (IssueRule), and the per-result stamp (ResultClassification).
ClassVar is only meaningful inside a class body; the category->expected
default map is a module-level constant.
A failed result stamped by an expected rule on an open issue no longer counts
as unexpected; closing the issue or an unexpected rule leaves it counted.
is_result_unexpected no longer references Meta.objects after switching to a
meta__type lookup.
…k counts

get_run_stats and the dashboard per-run nok aggregate counted every result
with an err meta, ignoring classification suppression, so effectively-expected
failures (expected rule on an open issue) were still reported as unexpected.
Exclude suppressed results via the shared helper; nok uses Exists subqueries
to avoid join multiplication from combining two multi-valued relations.
…ion filter

filter_by_result_classification annotated _has_err/_suppressed on its
returned queryset. The history pipeline later feeds that queryset to
HistoryService._finalize_queryset, which re-annotates the same names.
Use inline Exists conditions in filter/exclude so the returned queryset
carries no annotations, removing fragility and redundant subqueries.
Adds a regression test exercising the real chained production path.
… invalidation

Adds IssueViewSet and IssueRuleViewSet with close/reopen/deactivate/activate
lifecycle actions, admin-gated writes, and run-stats cache invalidation on
state changes. filter_backends is disabled on both viewsets because the global
auto FilterSet cannot build a filter for IssueRule.parameters (JSONField);
queryset narrowing is done explicitly instead.
…sify input

IssueViewSet/IssueRuleViewSet were ModelViewSets without DEFAULT_PERMISSION_CLASSES,
so DRF defaulted to AllowAny. They guarded create/update/custom-actions but inherited
DRF's default destroy (DELETE), partial_update (PATCH), and (for IssueRuleViewSet) update
(PUT) unguarded, letting unauthenticated clients delete or edit issues/rules and skip
cache invalidation. Override these handlers with check_action_permission and
invalidate_run_stats.

Also validate classify category/scope (reject unknown values with 400), derive active and
origin consistently from scope, and use get_object_or_404 with a required project query
param for robust 404s. Move mid-file imports to the top.
Add GET /runs/<run_id>/issues/ returning one row per issue with
classified results in the run, the distinct result count, the
(category, expected) pairs seen, and the optional external bug key.
Needed so the run view can show which issues account for its results
without the client aggregating raw stamps. Returns a bare list (no
pagination) and is ungated, matching the existing issue GETs.
Add GET /api/v2/runs/<run_id>/issues/<issue_id>/results/ returning a bare
list of results in a run classified under an issue, each with its run-tree
package path, name, obtained result and verdicts. Lets the frontend render
the per-issue results as a tree linking back to each result log.
The classify endpoint already accepted a matcher, but _capture_tags always
captured only important tags, so a match_all_tags rule would gate on the wrong
(too small) tag set and never match runs whose relevant tags differ.
The per-issue rules page needs each rule's test name and, on demand, the latest
failures a rule has classified across the project's runs.
Each linear history result now carries its stamped classifications (issue id,
title, state, external bug key, category, expected) so the UI can show them in
the obtained-result column.
…ppressing

expected becomes nullable: True=expected (suppresses), False=unexpected,
None=none (marker only). Classify respects an explicit null; suppression math
(rule.expected=True) is unchanged.
Add GET /issues/:id/results/ to list every result classified under an
issue across all its rules/tests. The History page can't serve this view:
it requires a single test name (500s without one) while an issue spans
multiple tests. Unions ResultClassification stamps, de-dupes by result,
newest-run first. Extract a shared row serializer reused by the per-rule
results action.
Apply ruff formatting and resolve ruff check findings on the touched
Python files: noqa PLR0913 on the widened history filter helper (matches
existing convention), ternary for the classify disposition default,
rename pre-existing self-reassignments in TestIterationResultQuerySet,
and keep the intentional circular-import-avoiding lazy imports (noqa
PLC0415). classification models must import after EventLog (noqa I001).
The [tool.ruff.lint].exclude entry for 'tests' is a no-op (exclude is only
honored under [tool.ruff]); with force-exclude the linter was checking
explicitly-passed test files. Our PR is the first to add test files that hit
this. Tests use Django unittest-style TestCase, so flake8-pytest rules (PT009/
PT027) and B017 don't apply; exclude them at the level that actually works,
mirroring how migrations are handled.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants