Skip to content

fix(app listing): attach before polling the scan — a bad image now fails in ~2s, not ~120s - #273

Open
ZacxDev wants to merge 1 commit into
mainfrom
zach/270-attach-before-scan
Open

fix(app listing): attach before polling the scan — a bad image now fails in ~2s, not ~120s#273
ZacxDev wants to merge 1 commit into
mainfrom
zach/270-attach-before-scan

Conversation

@ZacxDev

@ZacxDev ZacxDev commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Sub-item 4 of #270 — the CLI-side reorder, which needs nothing from the platform.

The problem

internal/cmd/app_listing.go ran the media-attach flow as ingest → pollScan (up to scanPollTimeout = 120s) → attach.

The platform validates geometry, aspect, MIME and byte size at ATTACH, not at ingest — validateListingImage runs inside loadValidatedImage before the ingestion-status gate, and all three attach procs (setListingIcon / setListingCover / addListingScreenshot) pass allowPending: true, so an image whose scan is still in flight is written and flagged scanPending, not refused.

So an author with a 512×256 icon uploaded, waited out the entire content scan, and only then got:

icon must be square-ish (aspect 2.00 outside 0.9–1.1)

The fix

ingest → attach → pollScan. The verdict now arrives in about one round-trip.

No platform constants are vendored — that is the point. The CLI does not predict the rejection, it asks sooner and relays the server's own message verbatim. The existing local byte caps and the DecodeImageInfo preflight are untouched.

Keeping the scan signal

The scan is still waited on, so success is never reported on a pending or blocked scan.

  • The poll is driven by the server's own scanPending. AttachResult already carried the field; it is now load-bearing rather than diagnostic. The attach proc sets it only on the still-scanning branch and omits the key once ingestion == Scanned, so absent and false mean the same thing and an already-scanned attach skips the poll entirely.
  • One verdict, never two. A terminal-Blocked / NotFound image still throws BAD_REQUEST at attach and that error is what the user sees (the CLI's own blocked-scan wording does not also fire). A scan that flips to Blocked after the attach is caught by the post-attach poll instead. A test asserts the double does not happen.
  • Attaching first means a blocked image can be written before the verdict arrives — impossible under the old order, so a failure now states what it left behind:
    • live listing → the revision is not submitted for moderator review, and the output says the live listing is unchanged;
    • screenshot → the row exists, so the failure hands over civitai app listing rm-screenshot <id>;
    • draft icon/cover → a re-run overwrites it.
  • {status: "pending"} is the legacy allowPending: false shape and writes nothing. Today's procs never return it, but attaching first would silently no-op and print success if that ever changed — so the CLI falls back to the pre-App listing media: no published dimension or aspect requirements, and no drift-proof way to learn them #270 order (wait out the scan, then re-attach) rather than lie.

Test matrix — red at origin/main, green at HEAD

Eight new cases in internal/cmd/app_listing_test.go, each run against unmodified origin/main production code (1eb4095) with the new test file dropped in:

test at origin/main at HEAD
TestAppListingAttachIsObservedBeforeScanPoll FAIL — sequence was … ingest scan scan setIcon … PASS
TestAppListingGeometryRejectionDoesNotWaitOutTheScan FAIL — got the 300 ms poll timeout, never the server's message; 268 scan polls, setIcon never called PASS
TestAppListingBlockedAtAttachIsReported FAIL — poll timeout instead of the server's blocked message PASS
TestAppListingBlockedDuringPostAttachPoll FAILsetIcon never called PASS
TestAppListingScreenshotBlockedNamesItsRemoval FAILaddScreenshot never called PASS
TestAppListingSkipsScanPollWhenAttachReportsScanned FAIL — poll timeout PASS
TestAppListingLegacyPendingAttachFallsBackToScanFirst FAIL — 1 setIcon call, no retry PASS
TestAppListingLiveScanFailureLeavesTheRevisionUnsubmitted FAIL — no "live listing is unchanged" line (the no-submit half already held, so that half is an invariant guard) PASS

Base run: RUN=21 PASS=8 FAIL=8. HEAD: RUN=21 PASS=21 FAIL=0.

The load-bearing assertion is the sequence, not a pair of booleans — the pre-change order made both calls too. callLog.requireBefore fails if either call is missing, so it cannot pass vacuously.

TestAppListingSetIconBlockedNoAttach is inverted into TestAppListingBlockedDuringPostAttachPoll: its premise ("setIcon must NOT be called after a blocked scan") is exactly what this change reverses, and the replacement pins that the blocked signal survives the move.

Mutations (each killed by the test that owns it)

mutation killed by
revert to scan-then-attach the base run above — the sequence guard
always poll (ignore scanPending) …SkipsScanPollWhenAttachReportsScanned only
never poll after attach SetIconDraft, …AttachIsObservedBeforeScanPoll, …BlockedDuringPostAttachPoll, …ScreenshotBlockedNamesItsRemoval, …LiveScanFailure…
drop the legacy-pending fallback …LegacyPendingAttachFallsBackToScanFirst only
silence each scanFailure context line …ScreenshotBlockedNamesItsRemoval + …LiveScanFailure…

Full suite

make ci green. go test ./... -v -count=1: RUN=2699, PASS=2695, FAIL=0, SKIP=4 (the 4 skips are the pre-existing env-gated TestScanDirFromEnv, TestSubmissionsCapDriftAgainstLiveAPI, TestScaffoldPinsSatisfyPublished, TestScaffoldedReadyAckActuallyFires). No panic: test timed out in the log. gofmt -s -l prints nothing.

Also updated

README.md and the app listing help text described the old order ("wait for the content scan, and attach it"). The README now says the platform's dimension/aspect rules are the platform's, that the CLI deliberately does not publish or enforce them, and that the order is what makes the rejection fast.

Refs #270

🤖 Generated with Claude Code


Rebase note. The branch was originally cut from a stale main (aeceb6b) and has been rebased onto 1eb4095. #268 had trimmed the app listing command-reference row in the same lines this PR touched — a real textual conflict, resolved in favour of #268's shortened row (the attach-order detail belongs in the prose section, where it now lives, not in the scannable table). Full suite re-run on the rebased tree: RUN=2717, PASS=2713, FAIL=0, SKIP=4, gofmt -s -l clean.

…s in ~2s not ~120s

`civitai app listing set-icon|set-cover|add-screenshot` ran
ingest -> pollScan (up to 120s) -> attach. But the platform validates
GEOMETRY, ASPECT, MIME and BYTE SIZE at ATTACH, not at ingest:
`validateListingImage` runs inside `loadValidatedImage` BEFORE the
ingestion-status gate, and all three attach procs pass `allowPending: true`,
so a still-scanning image is written and flagged `scanPending` rather than
refused. An author with a 512x256 icon therefore waited out the entire
content scan before hearing
`icon must be square-ish (aspect 2.00 outside 0.9-1.1)`.

Reordered to ingest -> attach -> pollScan. The CLI vendors no platform
constants for this and does not need to: it asks sooner and relays the
server's own message.

The scan is still waited on, so nothing reports success on a pending or
blocked scan:

- The poll is driven by the server's own `scanPending` flag. The attach proc
  sets it only on the still-scanning branch and omits the key once
  `ingestion == Scanned`, so an already-scanned attach skips the poll
  entirely.
- A terminal-blocked image still throws BAD_REQUEST at attach and that is the
  single message the user sees; a scan that flips to Blocked afterwards is
  caught by the post-attach poll. Exactly one verdict either way.
- Attaching first means a blocked image can be written before the verdict
  arrives, which the old order made impossible. A failure now states what it
  left behind: a live listing's revision is not submitted (and says so), and
  a blocked screenshot's failure names the `rm-screenshot <id>` that removes
  it.
- `{status:"pending"}` (the legacy `allowPending:false` shape) writes nothing.
  Today's procs never return it, but attaching first would silently no-op and
  print success if that changed, so the CLI falls back to the pre-#270 order
  and re-attaches.

Tests: 8 new cases in `internal/cmd/app_listing_test.go`, all measured RED at
origin/main (1eb4095) and GREEN at HEAD. The load-bearing one asserts the
observed call SEQUENCE against the fake server, not that both calls happened
- the pre-change order made both calls too.
`TestAppListingSetIconBlockedNoAttach` is inverted into
`TestAppListingBlockedDuringPostAttachPoll`: its "setIcon must NOT be called
after a blocked scan" premise is what this change reverses.

Mutations verified to kill a test for their own reason: reverting the order
(the sequence guard), always-poll (the fast-path guard), never-poll (four
guards), dropping the legacy-pending fallback, and silencing each
`scanFailure` context line.

Refs #270

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ZacxDev
ZacxDev force-pushed the zach/270-attach-before-scan branch from 3359296 to 5f49fa7 Compare August 7, 2026 21:41
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.

1 participant