Let CarrierWave own the agent plan payload and expose hasContent

What this does

Adds has_content? on WorkItems::AgentPlan and hasContent on the agent plan widget. So you can check if a work item has a plan without reading object storage. Part of #616229.

Since !249644 (merged) a work_item_agent_plans row is created when AI planning is enabled, before any plan exists. A row is not proof that content exists.

What changed after review

Thanks @msaleiko and @kivikakk. The first version added a filename column by hand on top of the custom uploader code. Now the concern uses mount_file_store_uploader, so CarrierWave owns the filename column, the destroy cleanup and the reload wiring. skip_store_file: true keeps the upload out of the save transaction, same as ImportExportUpload.

The uploader had to lose its #filename override. CarrierWave takes the mount identifier from #filename, so if it is always set the column is never NULL and tells us nothing. Before this, a plan without content reported has_content? = true. Models now use #external_payload_filename, so existing payloads stay on the same path. No blob migration needed.

Also fixed: we wrote a payload for every row without content. CacheMarkdownField renders nil content to an empty string on create, which counts as a change, so {"content":null,"content_html":""} went to object storage and to backups on every workItemEnableAiPlanning call. Clearing content now deletes the payload.

Known problem

readiness_score_feedback landed on master while this was in review. It shares the same payload as content. The filename column only says a payload exists, not which fields are in it. So a plan with only feedback reports hasContent: true. A spec pins this: is wrongly true for a plan that has only readiness score feedback. A fix needs one payload per field, or a flag from content alone. For the same reason the callback that picks the create or update event still reads content.

Rows written before the column existed get backfilled from the deterministic filename. A row that only enabled AI planning has no payload and SQL cannot see the difference, so those stay wrong until content is written. The feature is behind a beta flag that is off by default, so this is rare.

Note on size

The concern did not get smaller: 222 lines against 185 on master. The manual uploader building, retrieve_from_store! and the memoisation are gone, but a file_store hook, a filename hook and a manual uploader rebuild in the remove path had to stay. Maybe the file_store hook can go and use FileStoreMounter's own update_file_store. That saves about 20 lines, but then file_store is written after commit instead of with the row.

How to test locally

# rails console
$io = Hash.new(0)
WorkItems::AgentPlan.prepend(Module.new do
  def read_external_payload; $io[:read] += 1; super; end
end)

wi = WorkItem.find(<id of a work item with a plan>)

$io.clear; wi.reset.get_widget(:agent_plan).has_content_for_widget?
$io   # => {} , no object storage call

$io.clear; wi.reset.agent_plan.content.present?
$io   # => {:read=>1} , the old check

Specs: spec/lib/gitlab/externally_stored_field_spec.rb and the agent plan specs under ee/spec. Result: 158 examples, 0 failures.

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist.

Edited by Dennis Meister

Merge request reports

Loading
Loading