mirror of
https://github.com/forcedotcom/afv-library.git
synced 2026-08-08 16:25:58 +08:00
167 lines
9.6 KiB
Bash
167 lines
9.6 KiB
Bash
#!/usr/bin/env bash
|
|
#
|
|
# test-opp.sh — shared test-opportunity + grounding-note resolution for the two
|
|
# admin verification scripts (flow-debug-and-verify.sh and
|
|
# verify-prompt-generation.sh).
|
|
#
|
|
# The skill invokes those two scripts SEPARATELY (prompt path vs. flow path), but
|
|
# both must converge on ONE shared test Opportunity and ONE shared grounding Note
|
|
# so a suggestion generated on one path is visible on the other and duplicates
|
|
# never pile up. Keeping this logic in a single sourced lib is what guarantees the
|
|
# two stay byte-for-byte identical (they drifted before this was extracted).
|
|
#
|
|
# Contract:
|
|
# - Sourced, not executed. Relies on the CALLER having defined:
|
|
# * $ORG_ALIAS (target org)
|
|
# * log_info / log_pass / log_warn (helpers — resolved at call time)
|
|
# - Honors optional env var $PM_GROUNDING_NOTE: an agent/LLM-composed grounding
|
|
# note body. When set it is what grounds the AI suggestion; absent, a generic
|
|
# literal is used so standalone runs still work (a contextless note grounds on
|
|
# nothing and returns an empty suggestion, making a working setup look broken).
|
|
# - resolve_test_opportunity sets the global OPP_ID (empty if none could be
|
|
# resolved) and seeds/refreshes the grounding note on it.
|
|
#
|
|
# NOTE: no `set -euo pipefail` here — the caller owns shell options. Every sf/jq
|
|
# pipeline is guarded with `|| echo ""` so a transient CLI/network blip can't trip
|
|
# the caller's `set -e` and abort the whole verification run.
|
|
|
|
# Canonical name for the reusable test opp. The `[PM-TEST]` prefix makes every
|
|
# opp this tooling creates findable in one place (Opportunity WHERE Name LIKE
|
|
# '[PM-TEST]%') so they can be spotted/cleaned up and never confused with real
|
|
# pipeline in Flow Builder's Debug record picker or Pipeline Inspection.
|
|
PM_TEST_OPP_NAME='[PM-TEST] Test Pipeline Verification Opp'
|
|
# Title of the grounding note this tooling manages (used to find/refresh it).
|
|
PM_GROUNDING_NOTE_TITLE='PM grounding note'
|
|
# base64 of "Test grounding data for Pipeline Management verification." — the
|
|
# fallback body when the driver passes no PM_GROUNDING_NOTE.
|
|
PM_GROUNDING_NOTE_FALLBACK_B64='VGVzdCBncm91bmRpbmcgZGF0YSBmb3IgUGlwZWxpbmUgTWFuYWdlbWVudCB2ZXJpZmljYXRpb24u'
|
|
|
|
# Resolve the shared test opportunity, setting the global OPP_ID.
|
|
# 1. Reuse the stable [PM-TEST] opp if one exists (and roll its CloseDate +30d
|
|
# so it stays flow-eligible over repeated sessions).
|
|
# 2. Else create it (owner = current user; Pipeline Inspection is owner-scoped,
|
|
# so a suggestion on someone else's opp is invisible to the admin verifying).
|
|
# 3. Else fall back to any user-owned open opp.
|
|
# 4. Seed or refresh the grounding note against whichever opp resolved.
|
|
resolve_test_opportunity() {
|
|
local current_user_id close_date stage_label opp_values
|
|
|
|
current_user_id=$(NO_COLOR=1 FORCE_COLOR=0 sf org display user --target-org "$ORG_ALIAS" --json 2>/dev/null | jq -r '.result.id // empty' 2>/dev/null || echo "")
|
|
|
|
# macOS/BSD `date -v+30d`; GNU `date -d '+30 days'`. Empty => skip create/roll.
|
|
close_date=$(date -v+30d '+%Y-%m-%d' 2>/dev/null || date -d '+30 days' '+%Y-%m-%d' 2>/dev/null || echo "")
|
|
|
|
# 1) Reuse the stable, prefixed test opp if it exists.
|
|
if [[ -n "$current_user_id" ]]; then
|
|
OPP_ID=$(sf data query -q "SELECT Id FROM Opportunity WHERE Name='${PM_TEST_OPP_NAME}' AND OwnerId='${current_user_id}' AND IsClosed=false AND CloseDate>=TODAY AND CloseDate<=NEXT_N_DAYS:90 ORDER BY CreatedDate DESC LIMIT 1" \
|
|
--target-org "$ORG_ALIAS" --json 2>/dev/null | jq -r '.result.records[0].Id // empty' || echo "")
|
|
if [[ -n "$OPP_ID" ]]; then
|
|
log_pass "Reusing existing test opportunity: $OPP_ID ($PM_TEST_OPP_NAME)"
|
|
# Roll CloseDate forward so the reused opp keeps meeting the flow's
|
|
# "CloseDate within 90 days" eligibility filter across sessions.
|
|
if [[ -n "$close_date" ]]; then
|
|
if sf data update record --sobject Opportunity --record-id "$OPP_ID" \
|
|
--values "CloseDate='${close_date}'" --target-org "$ORG_ALIAS" --json 2>/dev/null >/dev/null; then
|
|
log_info "Rolled CloseDate to ${close_date}"
|
|
else
|
|
log_warn "Could not roll CloseDate on $OPP_ID (continuing)"
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# 2) Create the prefixed test opp if none exists yet.
|
|
if [[ -z "${OPP_ID:-}" && -n "$close_date" ]]; then
|
|
stage_label=$(sf data query -q "SELECT MasterLabel FROM OpportunityStage WHERE IsActive=true AND IsClosed=false ORDER BY SortOrder LIMIT 1" \
|
|
--target-org "$ORG_ALIAS" --json 2>/dev/null | jq -r '.result.records[0].MasterLabel // empty')
|
|
# Skip creation if the stage label contains a single quote — it would break
|
|
# --values parsing.
|
|
if [[ -n "$stage_label" ]] && [[ "$stage_label" != *"'"* ]]; then
|
|
opp_values="Name='${PM_TEST_OPP_NAME}' StageName='${stage_label}' CloseDate='${close_date}'"
|
|
[[ -n "$current_user_id" ]] && opp_values="${opp_values} OwnerId='${current_user_id}'"
|
|
OPP_ID=$(sf data create record --sobject Opportunity --values "$opp_values" \
|
|
--target-org "$ORG_ALIAS" --json 2>/dev/null | jq -r '.result.id // empty' || echo "")
|
|
[[ -n "$OPP_ID" ]] && log_pass "Created test opportunity: $OPP_ID ($PM_TEST_OPP_NAME)"
|
|
fi
|
|
fi
|
|
|
|
# 3) Fall back to a user-owned open opp (owner filter only when user is known).
|
|
if [[ -z "${OPP_ID:-}" ]]; then
|
|
if [[ -n "$current_user_id" ]]; then
|
|
OPP_ID=$(sf data query -q "SELECT Id FROM Opportunity WHERE OwnerId='${current_user_id}' AND IsClosed=false AND CloseDate>=TODAY AND CloseDate<=NEXT_N_DAYS:90 ORDER BY LastModifiedDate DESC LIMIT 1" \
|
|
--target-org "$ORG_ALIAS" --json 2>/dev/null | jq -r '.result.records[0].Id // empty' || echo "")
|
|
else
|
|
OPP_ID=$(sf data query -q "SELECT Id FROM Opportunity WHERE IsClosed=false AND CloseDate>=TODAY AND CloseDate<=NEXT_N_DAYS:90 ORDER BY LastModifiedDate DESC LIMIT 1" \
|
|
--target-org "$ORG_ALIAS" --json 2>/dev/null | jq -r '.result.records[0].Id // empty' || echo "")
|
|
fi
|
|
[[ -n "$OPP_ID" ]] && log_pass "Using opportunity: $OPP_ID"
|
|
fi
|
|
|
|
# 4) Seed / refresh the grounding note against the FINALLY-resolved opp so a
|
|
# freshly created opp, a reused opp, AND a fallback opp all end up grounded.
|
|
[[ -n "${OPP_ID:-}" ]] && seed_grounding_note "$OPP_ID"
|
|
}
|
|
|
|
# Ensure the shared grounding note exists (and matches the driver's note) on the
|
|
# given opp. Idempotent: seeds when absent, refreshes only when the composed note
|
|
# differs from what's stored, and otherwise leaves the existing note untouched.
|
|
seed_grounding_note() {
|
|
local opp_id="$1"
|
|
local note_b64 existing_doc_id existing_content note_id
|
|
|
|
# base64 line-wraps at 76 cols on macOS/BSD; `tr -d '\n'` keeps it single-line
|
|
# (GNU's `-w0` isn't portable).
|
|
if [[ -n "${PM_GROUNDING_NOTE:-}" ]]; then
|
|
note_b64=$(printf '%s' "$PM_GROUNDING_NOTE" | base64 | tr -d '\n')
|
|
else
|
|
note_b64="$PM_GROUNDING_NOTE_FALLBACK_B64"
|
|
fi
|
|
|
|
# Find a grounding note this tooling previously seeded on the opp. ContentNote's
|
|
# own 069-prefix Id IS the ContentDocumentId, so the link's ContentDocumentId is
|
|
# the note's Id. `|| echo ""` keeps a query blip from tripping the caller's set -e.
|
|
existing_doc_id=$(sf data query -q "SELECT ContentDocumentId FROM ContentDocumentLink WHERE LinkedEntityId='${opp_id}' AND ContentDocument.Title='${PM_GROUNDING_NOTE_TITLE}' ORDER BY ContentDocument.CreatedDate DESC LIMIT 1" \
|
|
--target-org "$ORG_ALIAS" --json 2>/dev/null | jq -r '.result.records[0].ContentDocumentId // empty' || echo "")
|
|
|
|
if [[ -n "$existing_doc_id" ]]; then
|
|
# A grounding note already exists. Refresh it ONLY when the driver passed a
|
|
# note whose content differs from what's stored — otherwise reuse it so we
|
|
# don't churn the note (and its ContentVersion history) every run.
|
|
# ContentNote.Content is a base64Url BLOB field: SOQL returns the REST *path*
|
|
# to the blob, not the bytes, so we fetch the decoded body via that endpoint
|
|
# and compare it to the raw PM_GROUNDING_NOTE plaintext (not base64).
|
|
if [[ -n "${PM_GROUNDING_NOTE:-}" ]]; then
|
|
existing_content=$(sf api request rest "/services/data/v64.0/sobjects/ContentNote/${existing_doc_id}/Content" \
|
|
--target-org "$ORG_ALIAS" 2>/dev/null || echo "")
|
|
if [[ -n "$existing_content" && "$existing_content" != "$PM_GROUNDING_NOTE" ]]; then
|
|
# ContentNote.Content isn't reliably updatable in place (it's
|
|
# ContentVersion-backed), so replace by delete + recreate: dropping the
|
|
# ContentDocument cascades its link and versions.
|
|
sf data delete record --sobject ContentDocument --record-id "$existing_doc_id" \
|
|
--target-org "$ORG_ALIAS" --json 2>/dev/null >/dev/null || true
|
|
existing_doc_id=""
|
|
log_info "Grounding note changed — refreshing it on $opp_id"
|
|
else
|
|
log_info "Reusing existing grounding note on $opp_id"
|
|
fi
|
|
else
|
|
log_info "Reusing existing grounding note on $opp_id"
|
|
fi
|
|
fi
|
|
|
|
# Seed when none exists (fresh opp) or the stale one was just dropped above.
|
|
if [[ -z "$existing_doc_id" ]]; then
|
|
note_id=$(sf data create record --sobject ContentNote \
|
|
--values "Title='${PM_GROUNDING_NOTE_TITLE}' Content='${note_b64}'" \
|
|
--target-org "$ORG_ALIAS" --json 2>/dev/null | jq -r '.result.id // empty' || echo "")
|
|
if [[ -n "$note_id" ]]; then
|
|
sf data create record --sobject ContentDocumentLink \
|
|
--values "ContentDocumentId='${note_id}' LinkedEntityId='${opp_id}' ShareType='V'" \
|
|
--target-org "$ORG_ALIAS" --json 2>/dev/null >/dev/null || true
|
|
log_info "Seeded ContentNote for grounding on test opportunity"
|
|
else
|
|
log_warn "Could not seed grounding ContentNote (Notes may not be enabled on this org) — suggestion quality may be lower"
|
|
fi
|
|
fi
|
|
}
|