Skip to content

Commit 7e77b42

Browse files
MajorTalclaude
andcommitted
fix(sdk): re-plan after commit for image puts to surface v1.49 variants
The gateway generates image variants AT COMMIT TIME (parent gateway change Section 5 — prepareStagedAssetVariants runs before the activation txn opens). The plan response that funds DeployResult.assets is built BEFORE commit, so its asset_entries[].asset_ref doesn't carry variant fields. Consumers calling r.assets.put or r.project(id).apply({ assets: { put: [<image>] } }) saw an AssetRef without variants/blurhash/width_px/etc. for first uploads — even though the gateway DID generate them and bytes were live. Fix: in applyOnce, after the commit succeeds and result.assets is built, do a dry-run re-plan with the same spec if any put entry has an image/* content_type. The recheck hits /apply/v1/plans?dry_run=true; the gateway looks up internal.blob_image_variants by source_sha256, builds an AssetImageData, threads it through buildAssetRefForPlan, and returns asset_entries with the full variant shape. The SDK merges those fields into the existing manifest entries. Dry-run keeps the recheck cheap: no plan_id or operation_id rows are created. The recheck is wrapped in try/catch so a failure can't break a successful apply — variants stay empty rather than the apply throwing. Also fixes a test assertion that compared display_url (wire mutable) to the SDK's cdnUrl alias (wire immutable). Non-HEIC parity is: ref.display_url === ref.cdnMutableUrl ref.display_immutable_url === ref.cdnUrl Both image-variant e2e tests now pass against api.run402.com. Closes asset-image-variants-client tasks 9.1 / 9.2 strictly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0262011 commit 7e77b42

2 files changed

Lines changed: 57 additions & 2 deletions

File tree

fullstack-integration.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -767,8 +767,11 @@ describe("Run402 full-stack integration (live API, no mocks)", { timeout: 900_00
767767
assert.equal(putRef.variants.medium?.format, "webp");
768768
assert.equal(putRef.variants.large?.format, "webp");
769769
assert.equal(putRef.variants.display_jpeg, undefined, "non-HEIC sources must NOT include display_jpeg");
770-
// Non-HEIC: display_url should equal cdn_url (browser-renderable directly).
771-
assert.equal(putRef.display_url, putRef.cdnUrl ?? putRef.cdn_immutable_url ?? undefined, "non-HEIC display_url should equal cdn_url");
770+
// Non-HEIC: display_url equals the wire's mutable cdn_url (which the
771+
// SDK exposes as `cdnMutableUrl` — `cdnUrl` aliases the immutable
772+
// form). display_immutable_url equals the SDK's `cdnUrl`.
773+
assert.equal(putRef.display_url, putRef.cdnMutableUrl, "non-HEIC display_url should equal the wire cdn_url (mutable form)");
774+
assert.equal(putRef.display_immutable_url, putRef.cdnUrl, "non-HEIC display_immutable_url should equal the wire cdn_immutable_url");
772775
// SDK convenience getters are present for image refs.
773776
assert.equal(putRef.thumbUrl, putRef.variants.thumb?.cdn_url);
774777
assert.equal(putRef.displayUrl, putRef.display_url);

sdk/src/namespaces/deploy.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,58 @@ async function applyOnce(
603603
// Release-only applies leave `result.assets` undefined.
604604
if (plan.asset_entries && plan.asset_entries.length > 0) {
605605
result.assets = buildAssetManifestFromPlanEntries(plan.asset_entries);
606+
607+
// v1.49 image-variant follow-up: variants are generated AT COMMIT TIME
608+
// (parent gateway change Section 5 — `prepareStagedAssetVariants` runs
609+
// before the activation txn opens). The plan that funded `result.assets`
610+
// was built BEFORE commit, so its `asset_entries[].asset_ref` doesn't
611+
// include the variant fields (the gateway only threads `image_data`
612+
// through `buildAssetRefForPlan` for SHAs that ALREADY have variants
613+
// in `internal.blob_image_variants`).
614+
//
615+
// For image puts, do a dry-run re-plan with the same spec. Bytes are
616+
// now in CAS (the commit just landed) AND variant rows exist in the
617+
// DB. The new plan response surfaces the variants. Dry-run keeps it
618+
// cheap: no new plan_id or operation_id rows are created.
619+
//
620+
// Best-effort: a re-plan failure shouldn't fail the apply. The bytes
621+
// are committed, the release is live, and the variant fields are
622+
// strictly additive — leaving them empty when the recheck errors is
623+
// worse than failing the apply.
624+
const hasImagePut = spec.assets?.put?.some((entry) => {
625+
const ct = ("content_type" in entry && typeof entry.content_type === "string")
626+
? entry.content_type
627+
: "";
628+
return ct.startsWith("image/");
629+
});
630+
if (hasImagePut) {
631+
try {
632+
const { plan: recheck } = await planInternal(client, spec, undefined, true);
633+
if (recheck.asset_entries && recheck.asset_entries.length > 0) {
634+
for (const recheckEntry of recheck.asset_entries) {
635+
const existing = result.assets.byKey[recheckEntry.key];
636+
if (!existing) continue;
637+
const ref = recheckEntry.asset_ref;
638+
if (ref.width_px !== undefined) existing.width_px = ref.width_px;
639+
if (ref.height_px !== undefined) existing.height_px = ref.height_px;
640+
if (ref.blurhash !== undefined) existing.blurhash = ref.blurhash;
641+
if (ref.variant_spec_version !== undefined) {
642+
existing.variant_spec_version = ref.variant_spec_version;
643+
}
644+
if (ref.display_url !== undefined) existing.display_url = ref.display_url;
645+
if (ref.display_immutable_url !== undefined) {
646+
existing.display_immutable_url = ref.display_immutable_url;
647+
}
648+
if (ref.variants !== undefined) existing.variants = ref.variants;
649+
}
650+
}
651+
} catch {
652+
// Best-effort: leave variant fields unpopulated rather than
653+
// failing a successful apply. Consumers can re-call
654+
// `r.assets.put` with the same bytes (dedup) to trigger the
655+
// plan-time surfacing if variants are missing.
656+
}
657+
}
606658
}
607659

608660
return result;

0 commit comments

Comments
 (0)