Repository navigation
fix(rest)!: an async import job whose every row failed ends failed, not succeeded - #27795
Conversation
…y row failed Claude-Session: https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F Co-authored-by: Claude <noreply@anthropic.com>
…ot succeeded Claude-Session: https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F Co-authored-by: Claude <noreply@anthropic.com>
…l-rows-failed-job
…pin waits for the worker's final write Claude-Session: https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 19 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin af733913c96aab70991eb330d6071937eb87c871 && git checkout af733913c96aab70991eb330d6071937eb87c871
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e00374a3488b2f517f6efcf86a097a9dda16ac8a 2c10836a274fe9447b920426a47ebc9490a6c599 && git checkout -B drift-repro e00374a3488b2f517f6efcf86a097a9dda16ac8a && git merge --no-ff 2c10836a274fe9447b920426a47ebc9490a6c599
node scripts/docs-audit/affected-docs.mjs --json e00374a3488b2f517f6efcf86a097a9dda16ac8a |
Claude-Session: https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F Contract-review-claim: session_01L6bhgUCizoYWQVJWjhdR6F · head 2c10836 · 2026-10-30T01:00Z |
|
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F Contract reviewServed-tier: Reviewed 2026-10-30T01:13Z against maintainer ruling B (record 6509036166, ruled 2026-10-29T21:42Z), the claim 6509513333 and its amendment 6512284153, the os-dev-report 6512268257, the PR body, the file list, the net diff against ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentThe narrowing itself, against the published spec on
Must-fix: none. Should-fix: one (the late-cancel pin's loud-exhaustion assertion), non-blocking. Implemented-by: VERDICT: PASS |
|
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F 维护者速读(席位终稿)
席位意见: ACCEPT。谓词逐字按裁定(不是 席位核对ACCEPT · head
|
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F
Requested by Sales · project thread
Fixes #27676
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F
Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none — no hand-written docs page or published skill states the async import job's terminal-status rule (
git grepofcontent/docsoutsidereferences//releases/and ofskills/for the job status vocabulary: 0 hits;import-mappings.mdxdocuments per-row outcomes only). TheImportJobStatusdocblock and the generated reference are the spec seat's card #27734.维护者速读
POST /data/OBJ/import/jobs)在未取消、有行、且没有任何一行被新建/更新/跳过时,终态记为failed(原来是succeeded),error写一句话说明每行都失败、去看results。status的客户端说「成功」。succeeded,取消仍优先;撤销不受影响(这种作业本来就不可撤销)。回滚即删掉data-action-routes.ts里的一个判断。packages/rest/src/data-action-routes.ts里everyRowFailed那段;packages/rest/src/import-job-integration.test.ts末尾新增的 describe;packages/rest/src/import-row-cell-errors.test.ts的轮询期望。pnpm --filter @objectstack/rest exec vitest run src/import-job-integration.test.ts,预期 21/21 通过。Ruling (binding)
Maintainer ruling B, record 6509036166 on #27676, execution parameters quoted:
What changes
packages/rest/src/data-action-routes.ts, the async import worker's terminal status. The existing cancel resolution runs first and is unchanged:summary.cancelled, then the in-memorycancelledImportJobsflag, then the durable row re-read. Only a status stillsucceededafter all three is tested by the new predicate, so a cancel always wins, including a late one.ImportRunSummary extends ImportProgress,packages/core/src/utils/import-runner.ts): rulingcancelled= the resolved status is notcancelled(summary.cancelledplus the two late re-checks);total=summary.total(rows.length);created/updated/skipped=summary.created/summary.updated/summary.skipped.summary.okis not read: it counts created and updated rows only.status: 'failed'anderror: "Every row of this import failed and nothing was imported; each row's reason is in the job's results." Counts,resultsandcompleted_atare written exactly as before. A dry run takes the same path (its valid rows count as created / updated in the runner's preview).packages/specchange: theImportJobStatus/errordocblocks are spec:ImportJobStatusandImportJobProgressSchema.errordocblocks sayfailedis only a fatal abort; after #27676 B an all-rows-failed job also endsfailed#27734.@objectstack/restpatch, BREAKING note, ADR-0087not-required (no-migration-prescription)marker.Readers of
failedvssucceededin this lane, measuredimportJobUndoable,rest-server.ts): admitssucceeded/cancelledonly, and also needs a non-empty undo log. A job that created and updated nothing has an empty log, so it was not undoable before and is not now. Pinned: the all-failed job readsundoable: falseand undo answers 422NOT_UNDOABLE.pending/runningonly; unaffected.?status=: passes the string through; pinned that the all-failed job lists understatus=failed.ImportWizard.tsximportResultOutcome): already reads "nothing imported and something errored" asfailed, andjobStatus === 'failed'asfailed; the synthetic row at theprog.status === 'failed'branch fires only when no per-row error arrived, which this change never produces.failedis already in its closed terminal set. No UI change.Tests
packages/rest/src/import-job-integration.test.ts, a new describe on the file's harness (realRestServerroute, realObjectQLengine +ObjectStackProtocolImplementation, SQLite:memory:, realsys_import_job):failed,errors === total(3),errorpresent and namingresults;resultshas 3 failed rows; nothing landed;undoable: false, undo 422NOT_UNDOABLE; listed understatus=failed.writeMode: 'update', no match) →succeeded, noerror.succeeded, noerror.failed,dryRun: true.succeeded.cancelled, witherrors: 3(it waits for the worker's own final write ofresultsbefore reading).succeeded: the existing first case of the file, unchanged.packages/rest/src/import-row-cell-errors.test.ts: its async poll helper assertedsucceededfor every job; the two one-row jobs whose only row fails (NO_NAME) now expectfailed, per the ruling. A stale fixture, a direct consequence of this change.Acceptance notes
RestServer, not an HTTP transport, the harness the file's existing cases use.resultswrite instead. Re-run green in the full suite.POST /data/:object/import) has no status and is untouched.Verification
Readings are at head
2fa11634unless a line names another sha.Red before the fix (e536326, tests only)
Ablations (scripts/ablation-replace.mjs; each restored blob == HEAD, `git diff HEAD` empty)
The suites import
./rest-serverfrom source, so no dist rebuild was involved.if (everyRowFailed) finalStatus = 'failed';→ red: the two all-failed pins plus the twoimport-row-cell-errorspolls (Tests 4 failed | 23 passed (27)).summary.ok === 0→ red: "every row skipped keeps succeeded" (expected "succeeded", received "failed").!summary.cancelled(ignoring the late re-checks) → red: the late-cancel control.&& summary.total > 0deleted → red: "a job with no rows keeps succeeded". (A first attempt with an empty--replacementwas refused by the tool before running anything; re-run with--delete.)Package suites and gates
pnpm --filter @objectstack/rest test: lock VERDICT command-exit 0, 342/342 files, 6701 passed, 326 skipped.pnpm --filter @objectstack/rest typecheck: exit 0, 0 test-layer errors (at 138a620; the commit after it touches two test files that the full suite above compiled and ran).eslint packages/rest --no-inline-config: exit 0.dispatch-gates --commands: 59 families derived; results in the os-dev-report on rest: an async import job whose every row failed endsfailed, notsucceeded(ruled B; split from #27501 (b)) #27676.check:dual-build-cjs-loadsexits 3 PREREQUISITE NOT MET (it reads every package's dist): NOT MEASURED locally, carried by CILint & Repo Gates.data-action-routes.ts,import-job-integration.test.tsorimport-row-cell-errors.test.ts(REST read of all 35 open PRs' files).(narrowing)arm) at the final head2c10836a: os-verify-lock VERDICT exit 0 (held 31m29s), 355 files passed and 3 skipped (358), 2867 tests passed and 20 skipped (2887), no failures (os-dev-report 6512268257). An earlier run at2fa11634had 2 failures, both the stale dogfood poll inpackages/qa/dogfood/test/import-reference-exposure.dogfood.test.ts, which2c10836aupdates (it waited forsucceededon an all-rows-failed job); that file is added to the claim by amendment.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F