mirror of
https://github.com/forcedotcom/afv-library.git
synced 2026-08-07 00:02:34 +08:00
173 lines
8.4 KiB
Bash
Executable File
173 lines
8.4 KiB
Bash
Executable File
#!/bin/bash
|
|
# Decision test for `sf-context skills-first-advisory` (issue #286).
|
|
#
|
|
# The advisory reads a PreToolUse `tool_input` payload from stdin and emits a
|
|
# WARN-ONLY nudge toward the owning skill on bypass-prone operations (raw
|
|
# metadata edits, raw `sf apex/retrieve/data` calls). It must NEVER block — every
|
|
# response carries `continue: true`. This test asserts, fully offline (no org):
|
|
# - bypass-prone ops emit an advisory naming the expected skill
|
|
# - unrelated ops stay silent (advisory text absent)
|
|
# - every response is non-blocking (continue:true, no permissionDecision)
|
|
#
|
|
# Run: bash plugins/sfdx-core/test/skills-first-advisory.test.sh
|
|
|
|
set -uo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
CTX="$ROOT/sf-context"
|
|
PASS=0
|
|
FAIL=0
|
|
|
|
# Parse the hook JSON, print a compact "<has-advisory>|<skill-or->|<blocking>" triple:
|
|
# has-advisory: "warn" if additionalContext present, else "quiet"
|
|
# skill: first `skill-name` mentioned in the advisory (backtick-wrapped), or "-"
|
|
# blocking: "block" if a permissionDecision is present, else "ok"
|
|
parse() {
|
|
python3 -c "
|
|
import json,sys,re
|
|
d=json.load(sys.stdin)
|
|
ctx=d.get('hookSpecificOutput',{}).get('additionalContext','')
|
|
warn='warn' if ctx else 'quiet'
|
|
m=re.search(r'\`([a-z][a-z0-9-]+)\`', ctx)
|
|
skill=m.group(1) if m else '-'
|
|
blocking='block' if d.get('hookSpecificOutput',{}).get('permissionDecision') else 'ok'
|
|
cont=d.get('continue')
|
|
# continue must always be true; fold a violation into the blocking field so it fails loudly
|
|
if cont is not True: blocking='no-continue'
|
|
print(f'{warn}|{skill}|{blocking}')
|
|
"
|
|
}
|
|
|
|
# check <expected-warn> <expected-skill> <description> <payload-json>
|
|
check() {
|
|
local ewarn="$1" eskill="$2" desc="$3" payload="$4"
|
|
local out got expected
|
|
out=$(printf '%s' "$payload" | "$CTX" skills-first-advisory)
|
|
got=$(printf '%s' "$out" | parse)
|
|
expected="${ewarn}|${eskill}|ok"
|
|
if [ "$got" = "$expected" ]; then
|
|
PASS=$((PASS + 1)); printf ' ok %-44s → %s\n' "$desc" "$got"
|
|
else
|
|
FAIL=$((FAIL + 1)); printf ' FAIL %-44s → got "%s", expected "%s"\n' "$desc" "$got" "$expected"
|
|
printf ' raw: %s\n' "$out"
|
|
fi
|
|
}
|
|
|
|
echo "sf-context skills-first-advisory — decision (offline, no org)"
|
|
|
|
# --- bypass-prone Apex source edits → warn, name platform-apex-generate (#413) ---
|
|
check warn platform-apex-generate "Apex .cls edit" \
|
|
'{"tool_name":"Edit","tool_input":{"file_path":"force-app/main/default/classes/MyService.cls"}}'
|
|
|
|
check warn platform-apex-test-generate "Apex *Test.cls edit" \
|
|
'{"tool_name":"Write","tool_input":{"file_path":"force-app/main/default/classes/MyServiceTest.cls"}}'
|
|
|
|
check warn platform-apex-generate "Apex .trigger edit" \
|
|
'{"tool_name":"Edit","tool_input":{"file_path":"force-app/main/default/triggers/AccountTrigger.trigger"}}'
|
|
|
|
check quiet - "Apex .cls-meta.xml sidecar stays quiet" \
|
|
'{"tool_name":"Edit","tool_input":{"file_path":"force-app/main/default/classes/MyService.cls-meta.xml"}}'
|
|
|
|
# --- bypass-prone metadata edits → warn, name the owning platform metadata skill ---
|
|
check warn platform-custom-field-generate "custom field-meta.xml edit" \
|
|
'{"tool_name":"Edit","tool_input":{"file_path":"force-app/main/default/objects/Account/fields/Foo__c.field-meta.xml"}}'
|
|
|
|
check warn platform-custom-object-generate "custom object-meta.xml edit" \
|
|
'{"tool_name":"Write","tool_input":{"file_path":"force-app/main/default/objects/Widget__c/Widget__c.object-meta.xml"}}'
|
|
|
|
check warn platform-permission-set-generate "permissionset-meta.xml edit" \
|
|
'{"tool_name":"Edit","tool_input":{"file_path":"force-app/main/default/permissionsets/Admin.permissionset-meta.xml"}}'
|
|
|
|
check warn automation-flow-generate "flow-meta.xml edit" \
|
|
'{"tool_name":"Edit","tool_input":{"file_path":"force-app/main/default/flows/My_Flow.flow-meta.xml"}}'
|
|
|
|
# --- report metadata now has an owning skill (#445) ---
|
|
check warn platform-report-generate "report-meta.xml edit" \
|
|
'{"tool_name":"Edit","tool_input":{"file_path":"force-app/main/default/reports/ComplianceReports/Cases_By_Month.report-meta.xml"}}'
|
|
|
|
# --- owner-less metadata types stay quiet (no phantom-skill nudge, #445 item 3) ---
|
|
check quiet - "reportFolder-meta.xml has no owning skill" \
|
|
'{"tool_name":"Write","tool_input":{"file_path":"force-app/main/default/reports/ComplianceReports.reportFolder-meta.xml"}}'
|
|
|
|
check quiet - "labels-meta.xml has no owning skill" \
|
|
'{"tool_name":"Edit","tool_input":{"file_path":"force-app/main/default/labels/CustomLabels.labels-meta.xml"}}'
|
|
|
|
check quiet - "layout-meta.xml has no owning skill" \
|
|
'{"tool_name":"Edit","tool_input":{"file_path":"force-app/main/default/layouts/Account-Account Layout.layout-meta.xml"}}'
|
|
|
|
# --- bypass-prone raw CLI calls → warn, name the owning skill ---
|
|
check warn platform-apex-test-run "sf apex run test" \
|
|
'{"tool_name":"Bash","tool_input":{"command":"sf apex run test --synchronous --json"}}'
|
|
|
|
check warn platform-apex-anonymous-run "sf apex run (anon)" \
|
|
'{"tool_name":"Bash","tool_input":{"command":"sf apex run --file scripts/anon.apex"}}'
|
|
|
|
check warn platform-metadata-retrieve "sf project retrieve" \
|
|
'{"tool_name":"Bash","tool_input":{"command":"sf project retrieve start --metadata ApexClass"}}'
|
|
|
|
check warn platform-soql-query "sf data query" \
|
|
'{"tool_name":"Bash","tool_input":{"command":"sf data query --query \"SELECT Id FROM Account\" --json"}}'
|
|
|
|
# --- unrelated ops → quiet (no advisory) ---
|
|
check quiet - "non-metadata Edit (README)" \
|
|
'{"tool_name":"Edit","tool_input":{"file_path":"README.md"}}'
|
|
|
|
check quiet - "unrelated Bash (ls)" \
|
|
'{"tool_name":"Bash","tool_input":{"command":"ls -la"}}'
|
|
|
|
check quiet - "deploy is handled by verify-org, not here" \
|
|
'{"tool_name":"Bash","tool_input":{"command":"sf project deploy start --source-dir force-app"}}'
|
|
|
|
check quiet - "empty payload" '{}'
|
|
|
|
# --- turn-aware suppression (#415) -------------------------------------------
|
|
# Once the owning skill has dispatched THIS turn, the advisory stays quiet for
|
|
# that skill's owned ops; a different owner still warns; a new turn (reset) or a
|
|
# different session re-arms it. The ledger lives at `.sf/skill-dispatch-state.json`
|
|
# relative to CWD, so run this block in an isolated temp dir to avoid writing a
|
|
# `.sf/` into the repo and to keep each assertion's state explicit.
|
|
echo ""
|
|
echo " turn-aware suppression (#415):"
|
|
TMPDIR_415="$(mktemp -d)"
|
|
trap 'rm -rf "$TMPDIR_415"' EXIT
|
|
pushd "$TMPDIR_415" >/dev/null
|
|
|
|
CLS='{"tool_name":"Edit","tool_input":{"file_path":"force-app/main/default/classes/Foo.cls"},"session_id":"s1"}'
|
|
PSET='{"tool_name":"Edit","tool_input":{"file_path":"force-app/main/default/permissionsets/Admin.permissionset-meta.xml"},"session_id":"s1"}'
|
|
|
|
# Clean slate: no ledger → first .cls edit warns.
|
|
check warn platform-apex-generate "1st .cls edit warns (no dispatch yet)" "$CLS"
|
|
|
|
# Record a platform-apex-generate dispatch for session s1 (the Skill-tool hook's job).
|
|
# The Skill tool carries a plugin-qualified name; the hook normalizes on the last
|
|
# ":"-segment, so the plugin prefix here is our plugin, not the upstream sfdx-apex.
|
|
printf '%s' '{"session_id":"s1","tool_input":{"skill":"salesforce-development:platform-apex-generate"}}' \
|
|
| "$CTX" record-skill-dispatch >/dev/null
|
|
|
|
# Same skill, same turn → quiet.
|
|
check quiet - "2nd .cls edit quiet after platform-apex-generate dispatch" "$CLS"
|
|
|
|
# Different owner (permission set) → still warns (per-skill scope).
|
|
check warn platform-permission-set-generate "permissionset edit still warns (per-skill scope)" "$PSET"
|
|
|
|
# Different session → warns (no cross-session suppression).
|
|
check warn platform-apex-generate ".cls edit in another session warns" \
|
|
'{"tool_name":"Edit","tool_input":{"file_path":"force-app/main/default/classes/Foo.cls"},"session_id":"s2"}'
|
|
|
|
# New turn (UserPromptSubmit reset) → re-arms the nudge for s1.
|
|
printf '%s' '{"session_id":"s1"}' | "$CTX" reset-dispatch-turn >/dev/null
|
|
check warn platform-apex-generate ".cls edit warns again after turn reset" "$CLS"
|
|
|
|
# Hardening: a session-less ledger (malformed reset/record payload) must never
|
|
# suppress a session-less advisory call — suppression requires a real match.
|
|
printf '%s' '{}' | "$CTX" reset-dispatch-turn >/dev/null
|
|
printf '%s' '{"tool_input":{"skill":"platform-apex-generate"}}' | "$CTX" record-skill-dispatch >/dev/null
|
|
check warn platform-apex-generate "session-less ledger does not suppress session-less call" \
|
|
'{"tool_name":"Edit","tool_input":{"file_path":"force-app/main/default/classes/Foo.cls"}}'
|
|
|
|
popd >/dev/null
|
|
|
|
echo ""
|
|
echo " $PASS passed, $FAIL failed"
|
|
[ "$FAIL" -eq 0 ]
|