Repository navigation
fix(history): flag a failed execution class listing as truncated and expose the actionable cap - #1276
Conversation
…expose the actionable cap fetchExecutionsByClass logged a listing error and skipped the class, so a failed actionable query hid every pending approval while the response kept truncated=false. The class fetch now reports failure and fetchExecutionsAsHistory raises the incomplete-results signal (including on the no-executions early return). HistoryResponse gains actionable_executions_limit (config.MaxListLimit) next to executions_limit (terminal cap, DefaultListLimit), so the UI banner can quote the cap that was actually hit instead of always quoting the terminal cap. The banner text itself lives in frontend/ and is left to the UI owner. Refs LeanerCloud#686 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bring the branch up to date with main. Conflicted files: - internal/api/handler_history.go: main (LeanerCloud#1200) replaced summarizePurchaseHistory(all) in the getHistory response with the scope-aware SQL summary plus SummaryComplete; the branch added ActionableExecutionsLimit to the same literal. Kept both: main's Summary/SummaryComplete and the branch's ActionableExecutionsLimit. - internal/api/types.go: both sides rewrote the Truncated doc comment. Combined them: main's note that Summary still counts rows beyond the caps while SummaryComplete is true, and the branch's note that a failed execution class listing also sets Truncated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merged origin/main (2514c01) into this branch. New head: ee67abd. A trial merge with the current main (c1f1781) is clean and passes Conflict resolution:
Checks on the merged head:
#1277, which is stacked on this branch, now merges this exact head, so the two resolutions are the same. |
This branch is stacked on fix/issue-686 (LeanerCloud#1276). That branch was brought up to date with origin/main in ee67abd, so merging it here brings in main and the LeanerCloud#686 conflict resolutions in one step. Conflicted files: - internal/api/main_nonet_test.go, internal/api/main_integration_test.go: both sides added a teardown hook list for the shared Postgres server (this branch: afterAllTests; main LeanerCloud#1400: afterTests). Took main's files unchanged; this branch only needed the hook, which main now provides. - internal/api/handler_history.go, getHistory: main (LeanerCloud#1200) loads the account scope once (loadHistoryScope), filters rows with scope.filter, defers the stale-approval sweep until after the summary aggregate, and returns the scope-aware SQL summary with SummaryComplete. This branch pages the stream (fetchHistoryPage) and lists the actionable class only on the first page. Combined: the page is fetched first, the scope is loaded once and applied to the actionable rows, staleExecs is declared outside the Cursor == nil branch so the sweep stays deferred (and does not run on cursor pages), and the response carries main's Summary/SummaryComplete plus this branch's paging fields. The summary is now whole-scope on every page. - internal/api/handler_history.go, fetchPurchaseHistory: main replaced the GetAllPurchaseHistory/GetPurchaseHistoryFiltered split with the shared historyFilters.purchaseHistoryFilter; this branch reads one keyset batch through ListPurchaseHistoryPage. Kept both: purchaseHistoryFilter is kept (the summary uses it) and the batch read now starts from it with Limit set to the batch size. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): VERDICT: APPROVE Blocking:
Non-blocking:
Attacked:
Body-stale:
Reviewer: rB11. Bar while the maintainers are away (set by IAmJSD as acting owner): one approving independent review on the head, two for money, auth and security paths. |
|
Local verification at |
|
Merged by IAmJSD's agent at IAmJSD's request (IAmJSD is acting owner while the maintainers are away), under the bar IAmJSD set: 1 approving independent review(s) on |
Brings the branch up to date with main now that LeanerCloud#1276 (the base this PR was stacked on), LeanerCloud#1100 (route documentation) and their neighbours have merged. Conflicted files: - internal/api/handler_history.go, internal/api/handler_history_actionable_starvation_test.go: main's copies are byte-identical to the LeanerCloud#1276 tip (ee67abd) this branch already contained, so the branch's paged versions are kept unchanged. - internal/api/types.go: HistoryResponse keeps the branch's ExecutionsLimit comment and the HasMore/NextCursor fields; main's other type changes (RecommendationsResponse.NextCursor, UnrestrictedAccountAccess, nullable CurrentCoverage) merged cleanly. - internal/api/openapi.yaml: GET /api/history takes main's corrected parameter list from LeanerCloud#1100 (provider, account_ids, account_id, start/end dates, no stray interval parameter) and the 400 response, plus the branch's description, the paged limit (maximum 1000) and the cursor parameter. HistoryResponse keeps the branch's limit and executions_limit text and its has_more/next_cursor properties. Also, internal/api/handler_history_truncation_test.go merged without a conflict marker but declared TestHandler_getHistory_ActionableExecutionsLimitExposed twice: LeanerCloud#1276 and the branch each added it at a different offset. Main's copy equals the LeanerCloud#1276 tip, so the branch's version (one declaration) is kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summary
Backend half of #686. Both gaps were left by #632 PR1 (#684).
Root cause
fetchExecutionsByClass(internal/api/handler_history.go) logged aGetExecutionsByStatuseserror and returnednil.fetchExecutionsAsHistoryonly derived its cap signal from row counts, and itslen(executions) == 0early return hard-codedfalse. So if the actionable query failed, every pending approval disappeared from/api/historywhiletruncatedstayedfalse.HistoryResponseexposedexecutions_limit(config.DefaultListLimit, 100), which since fix(history): fetch actionable executions separately so terminal rows cannot starve them #684 caps only the terminal class. The actionable class (pending, notified, scheduled, approved, running, paused) has its own cap,config.MaxListLimit(1000), and the API never returned it. That left the UI banner nothing to quote except 100.Fix
fetchExecutionsByClassnow returns(rows, ok). A new helper,fetchExecutionClasses, fetches both classes and returnsincomplete = actionable listing failed || terminal listing failed || actionable class at MaxListLimit.fetchExecutionsAsHistoryORs that into its cap signal, including on the no-executions early return. The result istruncated=truewhenever a class listing errored, as the issue suggests. Completed history and the other class still render as before. Splitting out the helper keepsfetchExecutionsAsHistoryat gocyclo 10.HistoryResponsegainsactionable_executions_limit(config.MaxListLimit), next tolimitandexecutions_limit. This is the "or the API exposes both caps" option from the issue.truncatedis still a single boolean. No per-class flag or count is added, so a scoped user learns nothing more about other tenants' row volume than before.openapi.yamland theHistoryResponsedoc comments now describe the new field and the listing-error case.Refs #686
This uses
Refs, notCloses: the banner text the user sees is rendered infrontend/src/history.ts, which this PR does not change. The issue should stay open until the frontend follow-up below lands.Verification
Unit tests with mock stores only. This is not a DB-level bug, so I added no integration test.
New and changed tests in
internal/api:TestHandler_getHistory_ClassListingErrorKeepsOtherClass: each subtest now also assertstruncated == true.TestHandler_getHistory_BothClassListingsFailSetTruncated(new): covers the early-return path when both listings fail.TestHandler_getHistory_EmptyClassListingsNotTruncated(new): two successful but empty listings must not raise the banner.TestHandler_getHistory_ActionableExecutionsLimitExposed(new):actionable_executions_limit == MaxListLimit,executions_limit == DefaultListLimit, andtruncatedis set at the actionable cap.Fail-before, run on the unfixed handler:
Pass-after: the same command passes all 4 tests (including both subtests).
Mutation probes, each reverted afterwards. Every one is caught:
!actionableOKfromincompletefailsClassListingErrorKeepsOtherClass/actionable_error_keeps_terminal!terminalOKfailsClassListingErrorKeepsOtherClass/terminal_error_keeps_actionablefalseon the no-executions early return failsBothClassListingsFailSetTruncatedCheap checks, run on this branch with
AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null AWS_EC2_METADATA_DISABLED=true:Combined-tree verification. This branch was tested together with the other queued branches on a local, never-pushed tree:
queue/platform@936996ad= origin/maina14dc8f2plus these heads, merged in this order:fix/issue-72andfix/issue-312conflict on one line ininternal/api/handler_dashboard.go(both change thecalculateCommitmentMetricscall). The tree used #72's side with #312's validatedprovidervariable; whichever PR merges second takes that same resolution.Linux, Go 1.26.9,
AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null AWS_EC2_METADATA_DISABLED=true; Docker Postgres via testcontainers:936996ad:go build ./...,go vet ./...,go vet -tags integration ./...and the fullgo test -race -short -count=1 ./....936996ad: golangci-lint v2.10.1 found 0 issues on every changed package, with and without--build-tags integration.tests/e2eis a separate module, so it was vetted under its owne2etag.gocyclo -over 10is silent on the changed non-test files.-tags integration -count=1, 60-minute local timeout because the VM was shared):936996ad, these pass:internal/server,internal/credentials,internal/scheduler,internal/database/postgres/migrations,internal/purchaseandinternal/api.internal/configpasses exceptTestPostgresStoreDB_ResolveStaleRIExchange_ValidatesCloudCheck, which fails the same way on unmodified main (fixed by fix(config): compare resolve checked_at at the database's microsecond precision #1252).26795376, same branches at earlier heads):internal/analytics,internal/auth,internal/commitmentopts,internal/databaseandcmd/lambda/clear-rate-limitall pass. The later fixes don't touch those packages.httpToLambdaRequestsignature change;scripts/setup-git-secrets.shpatterns in a fresh repo with an isolated HOME, found nothing in the changed files, and a planted fake key was flagged.scripts/test-git-secrets-allowlist.sh: 137 PASS, 0 FAIL.Out of scope
frontend/). See below.truncatedis reused for the listing-error case, as the issue proposes, rather than adding a separateincompleteflag. The banner's "capped at" wording is therefore slightly off when the cause is an error rather than a cap. The follow-up below covers this.fix/issue-532changehandler_purchases.goand other files, nothandler_history.goortypes.go.fix/issue-532also editsopenapi.yaml, but in different hunks (around lines 640 and 760; this PR edits about line 4030).Frontend follow-up for the UI owner
frontend/src/history.tsrenderTruncationNotices(around line 398) currently renders:Suggested change, which could be folded into the frontend PR the issue mentions:
actionable_executions_limit?: number;to the history response type infrontend/src/types.ts(next toexecutions_limit, around line 206).truncatedis now also set when an execution listing fails.frontend/src/__tests__/history-truncation-notice.test.tsmocks to includeactionable_executions_limit: 1000, and assert that the banner quotes 1000 for the actionable class.Labels
Maintainer: please mirror the issue's triage labels onto this PR:
triaged,priority/p3,severity/low,urgency/eventually,impact/few,effort/s,type/bug.🤖 Generated with Claude Code