Expose readinessScoreFeedback on WorkItemWidgetAgentPlan (read-only)

What does this MR do and why?

Exposes readinessScoreFeedback and its rendered counterpart readinessScoreFeedbackHtml as read-only fields on WorkItemWidgetAgentPlan, gated on the existing workplan_score feature flag.

Part of #617982 (closed) — MR 2 of 3 (GraphQL read). Depends on !252883 (merged) (model field); retarget to master once that merges.

Until MR 3 (write path) lands, both fields resolve to nil in production, which is harmless behind the flag.

What changes

  • ee/app/graphql/types/work_items/widgets/agent_plan_type.rb: adds readiness_score_feedback (String, nullable, experiment: { milestone: '19.4' }, FieldCallCount limit: 1, scopes [:api, :read_api, :ai_workflows]) and readiness_score_feedback_html as a markdown_field with the same nullability, scopes, milestone, and call-count limit. Both resolvers short-circuit before touching the attribute when the flag is off, so a disabled flag pays no object storage cost.
  • markdown_field generates its own resolver and takes no feature-flag argument, so the HTML field is gated by a prepended ReadinessScoreFeedbackHtmlGating module. Without it the rendered feedback would be readable while workplan_score is disabled, even though the raw field is gated. The module also maps the empty string that markdown_field renders for nil markdown back to nil, to keep the field's null contract.
  • ee/app/models/work_items/agent_plan.rb: adds cache_markdown_field :readiness_score_feedback, storage: :external and a CONTENT_LENGTH_MAX length validation.
  • ee/app/models/work_items/widgets/agent_plan.rb: delegates readiness_score_feedback and readiness_score_feedback_html to the agent plan record.
  • ee/app/services/work_items/data_sync/widgets/agent_plan.rb: rewrites and copies the feedback markdown when a work item moves, alongside content.
  • scripts/lint/keela_excluded.yml: records readiness_score_feedback_html as an intentionally unused-looking delegation (it is read by the GraphQL markdown field).
  • doc/api/graphql/reference/_index.md and public/-/graphql/introspection_result.json: regenerated GraphQL reference docs and introspection snapshot.
  • Specs: field presence, nullability, scopes, call-count limits, and flag on/off resolver behavior for both fields; model validation and external-storage round-trip coverage; data-sync copy coverage.

Performance note

content, readiness_score_feedback, and the cached HTML all live in the same object storage blob, and Gitlab::ExternallyStoredField loads every externally stored field in a single fetch and memoizes it per record. So the first of these fields read on a given agent plan costs a round-trip, and the rest are in-memory attribute reads. A query asking for all of them on one work item costs one fetch, not three.

The FieldCallCount limit: 1 guard on both new fields is still needed, because that memoization is per record: without the limit, requesting either field across a list of work items would be one blob fetch per work item. The feature-flag check short-circuits before the attribute is touched, so a disabled flag pays nothing.

How to set up and validate locally

The source branch already includes the model change from !252883 (merged), so checking it out is enough.

  1. Check out the branch and make sure migrations are up to date:

    git checkout duo/feature/617982-readiness-score-feedback-graphql-read
    bin/rails db:migrate
  2. In rails console, enable the flag for a group and seed some feedback on a work item's agent plan (the namespace is derived from the work item):

    group = Group.find_by_full_path('flightjs')
    Feature.enable(:workplan_score, group)
    
    work_item = group.work_items.first # or any work item under that group
    plan = WorkItems::AgentPlan.find_or_initialize_by(work_item: work_item)
    plan.update!(readiness_score: 42, readiness_score_feedback: 'Add **acceptance criteria**.')
    
    work_item.to_global_id.to_s
  3. Open the GraphiQL explorer at /-/graphql-explorer and run the query with the global ID from the previous step:

    query {
      workItem(id: "gid://gitlab/WorkItem/1") {
        widgets {
          ... on WorkItemWidgetAgentPlan {
            readinessScore
            readinessScoreFeedback
            readinessScoreFeedbackHtml
          }
        }
      }
    }

    Expected: readinessScoreFeedback returns the raw markdown "Add **acceptance criteria**.", and readinessScoreFeedbackHtml returns the rendered HTML with a <strong> around acceptance criteria.

  4. Verify the flag gate. Run Feature.disable(:workplan_score) in the console and re-run the query. Expected: both readinessScoreFeedback and readinessScoreFeedbackHtml are null while the rest of the widget still resolves.

  5. Verify the null contract for the HTML field. Clear the feedback (plan.update!(readiness_score_feedback: nil)) with the flag enabled and re-run the query. Expected: readinessScoreFeedbackHtml is null, not an empty string.

  6. Verify the call-count limits by requesting each field for two work items in a single query (for example, through group.workItems). Expected: a "readinessScoreFeedback" field can only be requested for one work item at a time-style error (and the equivalent for readinessScoreFeedbackHtml) rather than one object storage read per work item.

  7. Run the specs and the GraphQL docs check:

    bin/rspec ee/spec/graphql/types/work_items/widgets/agent_plan_type_spec.rb \
      ee/spec/models/work_items/agent_plan_spec.rb \
      ee/spec/models/work_items/widgets/agent_plan_spec.rb \
      ee/spec/services/work_items/data_sync/widgets/agent_plan_spec.rb
    bundle exec rake gitlab:graphql:check_docs

References

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Naman Jagdish Gala

Merge request reports

Loading
Loading