scc-universal 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. package/.claude-plugin/plugin.json +44 -0
  2. package/.cursor/agents/deep-researcher.md +142 -0
  3. package/.cursor/agents/doc-updater.md +219 -0
  4. package/.cursor/agents/eval-runner.md +335 -0
  5. package/.cursor/agents/learning-engine.md +210 -0
  6. package/.cursor/agents/loop-operator.md +245 -0
  7. package/.cursor/agents/refactor-cleaner.md +119 -0
  8. package/.cursor/agents/sf-admin-agent.md +127 -0
  9. package/.cursor/agents/sf-agentforce-agent.md +126 -0
  10. package/.cursor/agents/sf-apex-agent.md +117 -0
  11. package/.cursor/agents/sf-architect.md +426 -0
  12. package/.cursor/agents/sf-aura-reviewer.md +369 -0
  13. package/.cursor/agents/sf-bugfix-agent.md +101 -0
  14. package/.cursor/agents/sf-flow-agent.md +155 -0
  15. package/.cursor/agents/sf-integration-agent.md +141 -0
  16. package/.cursor/agents/sf-lwc-agent.md +123 -0
  17. package/.cursor/agents/sf-review-agent.md +357 -0
  18. package/.cursor/agents/sf-visualforce-reviewer.md +465 -0
  19. package/.cursor/hooks/adapter.js +81 -0
  20. package/.cursor/hooks/after-file-edit.js +26 -0
  21. package/.cursor/hooks/after-mcp-execution.js +12 -0
  22. package/.cursor/hooks/after-shell-execution.js +30 -0
  23. package/.cursor/hooks/after-tab-file-edit.js +12 -0
  24. package/.cursor/hooks/before-mcp-execution.js +11 -0
  25. package/.cursor/hooks/before-read-file.js +13 -0
  26. package/.cursor/hooks/before-shell-execution.js +29 -0
  27. package/.cursor/hooks/before-submit-prompt.js +23 -0
  28. package/.cursor/hooks/pre-compact.js +7 -0
  29. package/.cursor/hooks/session-end.js +10 -0
  30. package/.cursor/hooks/session-start.js +10 -0
  31. package/.cursor/hooks/stop.js +18 -0
  32. package/.cursor/hooks/subagent-start.js +10 -0
  33. package/.cursor/hooks/subagent-stop.js +10 -0
  34. package/.cursor/hooks.json +107 -0
  35. package/.cursor/skills/aside/SKILL.md +115 -0
  36. package/.cursor/skills/checkpoint/SKILL.md +50 -0
  37. package/.cursor/skills/configure-scc/SKILL.md +160 -0
  38. package/.cursor/skills/continuous-agent-loop/SKILL.md +260 -0
  39. package/.cursor/skills/mcp-server-patterns/SKILL.md +142 -0
  40. package/.cursor/skills/model-route/SKILL.md +81 -0
  41. package/.cursor/skills/prompt-optimizer/SKILL.md +366 -0
  42. package/.cursor/skills/refactor-clean/SKILL.md +133 -0
  43. package/.cursor/skills/resume-session/SKILL.md +111 -0
  44. package/.cursor/skills/save-session/SKILL.md +183 -0
  45. package/.cursor/skills/search-first/SKILL.md +140 -0
  46. package/.cursor/skills/security-scan/SKILL.md +142 -0
  47. package/.cursor/skills/sessions/SKILL.md +124 -0
  48. package/.cursor/skills/sf-agentforce-development/SKILL.md +449 -0
  49. package/.cursor/skills/sf-apex-async-patterns/SKILL.md +324 -0
  50. package/.cursor/skills/sf-apex-best-practices/SKILL.md +421 -0
  51. package/.cursor/skills/sf-apex-constraints/SKILL.md +79 -0
  52. package/.cursor/skills/sf-apex-cursor/SKILL.md +336 -0
  53. package/.cursor/skills/sf-apex-enterprise-patterns/SKILL.md +344 -0
  54. package/.cursor/skills/sf-apex-testing/SKILL.md +407 -0
  55. package/.cursor/skills/sf-api-design/SKILL.md +237 -0
  56. package/.cursor/skills/sf-approval-processes/SKILL.md +312 -0
  57. package/.cursor/skills/sf-aura-development/SKILL.md +260 -0
  58. package/.cursor/skills/sf-build-fix/SKILL.md +120 -0
  59. package/.cursor/skills/sf-data-modeling/SKILL.md +274 -0
  60. package/.cursor/skills/sf-debugging/SKILL.md +362 -0
  61. package/.cursor/skills/sf-deployment/SKILL.md +291 -0
  62. package/.cursor/skills/sf-deployment-constraints/SKILL.md +153 -0
  63. package/.cursor/skills/sf-devops-ci-cd/SKILL.md +322 -0
  64. package/.cursor/skills/sf-docs-lookup/SKILL.md +100 -0
  65. package/.cursor/skills/sf-e2e-testing/SKILL.md +321 -0
  66. package/.cursor/skills/sf-experience-cloud/SKILL.md +248 -0
  67. package/.cursor/skills/sf-flow-development/SKILL.md +376 -0
  68. package/.cursor/skills/sf-governor-limits/SKILL.md +319 -0
  69. package/.cursor/skills/sf-harness-audit/SKILL.md +139 -0
  70. package/.cursor/skills/sf-help/SKILL.md +156 -0
  71. package/.cursor/skills/sf-integration/SKILL.md +479 -0
  72. package/.cursor/skills/sf-lwc-constraints/SKILL.md +128 -0
  73. package/.cursor/skills/sf-lwc-development/SKILL.md +302 -0
  74. package/.cursor/skills/sf-lwc-testing/SKILL.md +387 -0
  75. package/.cursor/skills/sf-metadata-management/SKILL.md +285 -0
  76. package/.cursor/skills/sf-platform-events-cdc/SKILL.md +372 -0
  77. package/.cursor/skills/sf-quickstart/SKILL.md +170 -0
  78. package/.cursor/skills/sf-security/SKILL.md +330 -0
  79. package/.cursor/skills/sf-security-constraints/SKILL.md +125 -0
  80. package/.cursor/skills/sf-soql-constraints/SKILL.md +129 -0
  81. package/.cursor/skills/sf-soql-optimization/SKILL.md +353 -0
  82. package/.cursor/skills/sf-tdd-workflow/SKILL.md +332 -0
  83. package/.cursor/skills/sf-testing-constraints/SKILL.md +198 -0
  84. package/.cursor/skills/sf-trigger-constraints/SKILL.md +88 -0
  85. package/.cursor/skills/sf-trigger-frameworks/SKILL.md +343 -0
  86. package/.cursor/skills/sf-visualforce-development/SKILL.md +259 -0
  87. package/.cursor/skills/strategic-compact/SKILL.md +205 -0
  88. package/.cursor/skills/update-docs/SKILL.md +162 -0
  89. package/.cursor/skills/update-platform-docs/SKILL.md +86 -0
  90. package/.cursor-plugin/plugin.json +26 -0
  91. package/LICENSE +21 -0
  92. package/README.md +522 -0
  93. package/agents/deep-researcher.md +145 -0
  94. package/agents/doc-updater.md +222 -0
  95. package/agents/eval-runner.md +340 -0
  96. package/agents/learning-engine.md +211 -0
  97. package/agents/loop-operator.md +247 -0
  98. package/agents/refactor-cleaner.md +122 -0
  99. package/agents/sf-admin-agent.md +131 -0
  100. package/agents/sf-agentforce-agent.md +132 -0
  101. package/agents/sf-apex-agent.md +124 -0
  102. package/agents/sf-architect.md +435 -0
  103. package/agents/sf-aura-reviewer.md +372 -0
  104. package/agents/sf-bugfix-agent.md +105 -0
  105. package/agents/sf-flow-agent.md +159 -0
  106. package/agents/sf-integration-agent.md +146 -0
  107. package/agents/sf-lwc-agent.md +127 -0
  108. package/agents/sf-review-agent.md +366 -0
  109. package/agents/sf-visualforce-reviewer.md +468 -0
  110. package/assets/logo.svg +18 -0
  111. package/docs/ARCHITECTURE.md +133 -0
  112. package/docs/authoring-guide.md +373 -0
  113. package/docs/hook-development.md +578 -0
  114. package/docs/token-optimization.md +139 -0
  115. package/docs/workflow-examples.md +645 -0
  116. package/examples/agentforce-action/README.md +227 -0
  117. package/examples/apex-trigger-handler/README.md +114 -0
  118. package/examples/devops-pipeline/README.md +325 -0
  119. package/examples/flow-automation/README.md +188 -0
  120. package/examples/integration-pattern/README.md +416 -0
  121. package/examples/lwc-component/README.md +180 -0
  122. package/examples/platform-events/README.md +492 -0
  123. package/examples/scratch-org-setup/README.md +138 -0
  124. package/examples/security-audit/README.md +244 -0
  125. package/examples/visualforce-migration/README.md +314 -0
  126. package/hooks/hooks.json +338 -0
  127. package/hooks/memory-persistence/README.md +73 -0
  128. package/manifests/install-modules.json +217 -0
  129. package/manifests/install-profiles.json +17 -0
  130. package/mcp-configs/mcp-servers.json +19 -0
  131. package/package.json +89 -0
  132. package/schemas/hooks.schema.json +123 -0
  133. package/schemas/install-modules.schema.json +76 -0
  134. package/schemas/install-profiles.schema.json +28 -0
  135. package/schemas/install-state.schema.json +73 -0
  136. package/schemas/package-manager.schema.json +18 -0
  137. package/schemas/plugin.schema.json +112 -0
  138. package/schemas/scc-install-config.schema.json +29 -0
  139. package/schemas/state-store.schema.json +111 -0
  140. package/scripts/cli/install-apply.js +170 -0
  141. package/scripts/cli/uninstall.js +193 -0
  142. package/scripts/hooks/check-console-log.js +101 -0
  143. package/scripts/hooks/check-hook-enabled.js +17 -0
  144. package/scripts/hooks/check-platform-docs-age.js +48 -0
  145. package/scripts/hooks/cost-tracker.js +78 -0
  146. package/scripts/hooks/doc-file-warning.js +63 -0
  147. package/scripts/hooks/evaluate-session.js +98 -0
  148. package/scripts/hooks/governor-check.js +220 -0
  149. package/scripts/hooks/learning-observe.sh +206 -0
  150. package/scripts/hooks/mcp-health-check.js +588 -0
  151. package/scripts/hooks/post-bash-build-complete.js +34 -0
  152. package/scripts/hooks/post-bash-pr-created.js +43 -0
  153. package/scripts/hooks/post-edit-console-warn.js +61 -0
  154. package/scripts/hooks/post-edit-format.js +79 -0
  155. package/scripts/hooks/post-edit-typecheck.js +98 -0
  156. package/scripts/hooks/post-write.js +168 -0
  157. package/scripts/hooks/pre-bash-git-push-reminder.js +35 -0
  158. package/scripts/hooks/pre-bash-tmux-reminder.js +47 -0
  159. package/scripts/hooks/pre-compact.js +51 -0
  160. package/scripts/hooks/pre-tool-use.js +163 -0
  161. package/scripts/hooks/pre-write-doc-warn.js +9 -0
  162. package/scripts/hooks/quality-gate.js +251 -0
  163. package/scripts/hooks/run-with-flags-shell.sh +32 -0
  164. package/scripts/hooks/run-with-flags.js +135 -0
  165. package/scripts/hooks/session-end-marker.js +29 -0
  166. package/scripts/hooks/session-end.js +311 -0
  167. package/scripts/hooks/session-start.js +202 -0
  168. package/scripts/hooks/sfdx-scanner-check.js +142 -0
  169. package/scripts/hooks/sfdx-validate.js +119 -0
  170. package/scripts/hooks/stop-hook.js +170 -0
  171. package/scripts/hooks/suggest-compact.js +67 -0
  172. package/scripts/lib/agent-adapter.js +82 -0
  173. package/scripts/lib/apex-analysis.js +194 -0
  174. package/scripts/lib/hook-flags.js +74 -0
  175. package/scripts/lib/install-config.js +73 -0
  176. package/scripts/lib/install-executor.js +363 -0
  177. package/scripts/lib/install-state.js +121 -0
  178. package/scripts/lib/orchestration-session.js +299 -0
  179. package/scripts/lib/package-manager.js +124 -0
  180. package/scripts/lib/project-detect.js +228 -0
  181. package/scripts/lib/schema-validator.js +190 -0
  182. package/scripts/lib/skill-adapter.js +100 -0
  183. package/scripts/lib/state-store.js +376 -0
  184. package/scripts/lib/tmux-worktree-orchestrator.js +598 -0
  185. package/scripts/lib/utils.js +313 -0
  186. package/scripts/scc.js +164 -0
  187. package/skills/_reference/AGENTFORCE_PATTERNS.md +112 -0
  188. package/skills/_reference/APEX_CURSOR.md +159 -0
  189. package/skills/_reference/API_VERSIONS.md +78 -0
  190. package/skills/_reference/APPROVAL_PROCESSES.md +105 -0
  191. package/skills/_reference/ASYNC_PATTERNS.md +163 -0
  192. package/skills/_reference/AURA_COMPONENTS.md +146 -0
  193. package/skills/_reference/DATA_MIGRATION_PATTERNS.md +151 -0
  194. package/skills/_reference/DATA_MODELING.md +124 -0
  195. package/skills/_reference/DEBUGGING_TOOLS.md +140 -0
  196. package/skills/_reference/DEPLOYMENT_CHECKLIST.md +87 -0
  197. package/skills/_reference/DEPRECATIONS.md +79 -0
  198. package/skills/_reference/DOCKER_CI_PATTERNS.md +138 -0
  199. package/skills/_reference/ENTERPRISE_PATTERNS.md +122 -0
  200. package/skills/_reference/EXPERIENCE_CLOUD.md +143 -0
  201. package/skills/_reference/FLOW_PATTERNS.md +113 -0
  202. package/skills/_reference/GOVERNOR_LIMITS.md +77 -0
  203. package/skills/_reference/INTEGRATION_PATTERNS.md +105 -0
  204. package/skills/_reference/LWC_PATTERNS.md +79 -0
  205. package/skills/_reference/METADATA_TYPES.md +115 -0
  206. package/skills/_reference/NAMING_CONVENTIONS.md +84 -0
  207. package/skills/_reference/PACKAGE_DEVELOPMENT.md +150 -0
  208. package/skills/_reference/PLATFORM_EVENTS.md +121 -0
  209. package/skills/_reference/REPORTING_API.md +143 -0
  210. package/skills/_reference/SCRATCH_ORG_PATTERNS.md +126 -0
  211. package/skills/_reference/SECURITY_PATTERNS.md +127 -0
  212. package/skills/_reference/SHARING_MODEL.md +120 -0
  213. package/skills/_reference/SOQL_PATTERNS.md +119 -0
  214. package/skills/_reference/TESTING_STANDARDS.md +96 -0
  215. package/skills/_reference/TRIGGER_PATTERNS.md +114 -0
  216. package/skills/_reference/VISUALFORCE_PATTERNS.md +121 -0
  217. package/skills/aside/SKILL.md +118 -0
  218. package/skills/checkpoint/SKILL.md +53 -0
  219. package/skills/configure-scc/SKILL.md +163 -0
  220. package/skills/continuous-agent-loop/SKILL.md +264 -0
  221. package/skills/mcp-server-patterns/SKILL.md +146 -0
  222. package/skills/model-route/SKILL.md +84 -0
  223. package/skills/prompt-optimizer/SKILL.md +369 -0
  224. package/skills/refactor-clean/SKILL.md +136 -0
  225. package/skills/resume-session/SKILL.md +114 -0
  226. package/skills/save-session/SKILL.md +186 -0
  227. package/skills/search-first/SKILL.md +144 -0
  228. package/skills/security-scan/SKILL.md +146 -0
  229. package/skills/sessions/SKILL.md +127 -0
  230. package/skills/sf-agentforce-development/SKILL.md +450 -0
  231. package/skills/sf-apex-async-patterns/SKILL.md +326 -0
  232. package/skills/sf-apex-best-practices/SKILL.md +425 -0
  233. package/skills/sf-apex-constraints/SKILL.md +81 -0
  234. package/skills/sf-apex-cursor/SKILL.md +338 -0
  235. package/skills/sf-apex-enterprise-patterns/SKILL.md +348 -0
  236. package/skills/sf-apex-testing/SKILL.md +409 -0
  237. package/skills/sf-api-design/SKILL.md +238 -0
  238. package/skills/sf-approval-processes/SKILL.md +315 -0
  239. package/skills/sf-aura-development/SKILL.md +263 -0
  240. package/skills/sf-build-fix/SKILL.md +121 -0
  241. package/skills/sf-data-modeling/SKILL.md +278 -0
  242. package/skills/sf-debugging/SKILL.md +363 -0
  243. package/skills/sf-deployment/SKILL.md +295 -0
  244. package/skills/sf-deployment-constraints/SKILL.md +155 -0
  245. package/skills/sf-devops-ci-cd/SKILL.md +325 -0
  246. package/skills/sf-docs-lookup/SKILL.md +103 -0
  247. package/skills/sf-e2e-testing/SKILL.md +324 -0
  248. package/skills/sf-experience-cloud/SKILL.md +249 -0
  249. package/skills/sf-flow-development/SKILL.md +377 -0
  250. package/skills/sf-governor-limits/SKILL.md +323 -0
  251. package/skills/sf-harness-audit/SKILL.md +142 -0
  252. package/skills/sf-help/SKILL.md +159 -0
  253. package/skills/sf-integration/SKILL.md +483 -0
  254. package/skills/sf-lwc-constraints/SKILL.md +130 -0
  255. package/skills/sf-lwc-development/SKILL.md +303 -0
  256. package/skills/sf-lwc-testing/SKILL.md +388 -0
  257. package/skills/sf-metadata-management/SKILL.md +288 -0
  258. package/skills/sf-platform-events-cdc/SKILL.md +375 -0
  259. package/skills/sf-quickstart/SKILL.md +173 -0
  260. package/skills/sf-security/SKILL.md +334 -0
  261. package/skills/sf-security-constraints/SKILL.md +127 -0
  262. package/skills/sf-soql-constraints/SKILL.md +131 -0
  263. package/skills/sf-soql-optimization/SKILL.md +354 -0
  264. package/skills/sf-tdd-workflow/SKILL.md +336 -0
  265. package/skills/sf-testing-constraints/SKILL.md +200 -0
  266. package/skills/sf-trigger-constraints/SKILL.md +90 -0
  267. package/skills/sf-trigger-frameworks/SKILL.md +347 -0
  268. package/skills/sf-visualforce-development/SKILL.md +260 -0
  269. package/skills/strategic-compact/SKILL.md +208 -0
  270. package/skills/update-docs/SKILL.md +165 -0
  271. package/skills/update-platform-docs/SKILL.md +90 -0
@@ -0,0 +1,409 @@
1
+ ---
2
+ name: sf-apex-testing
3
+ description: "Apex unit testing — test structure, TestDataFactory, governor limit testing, async testing, mocks, coverage. Use when writing tests or improving coverage. Do NOT use for TDD workflow or LWC Jest tests."
4
+ origin: SCC
5
+ user-invocable: false
6
+ allowed-tools: Read, Grep, Glob, Edit, Write, Bash
7
+ ---
8
+
9
+ # Apex Testing
10
+
11
+ Procedures and patterns for writing effective Apex tests. Constraint rules (never/always lists for test isolation, assertions, SeeAllData) live in `sf-testing-constraints`. This skill covers the _how_ — test structure, factories, mocks, async testing, and coverage strategies.
12
+
13
+ Reference: @../_reference/TESTING_STANDARDS.md
14
+
15
+ ---
16
+
17
+ ## When to Use
18
+
19
+ - When writing test classes for new Apex code before deploying to production
20
+ - When existing tests pass but only cover happy paths
21
+ - When a deployment fails with "insufficient code coverage" errors
22
+ - When building mock patterns for callouts or dependency injection
23
+
24
+ > **Related:** For the TDD workflow (red-green-refactor process), see `sf-tdd-workflow`.
25
+
26
+ ---
27
+
28
+ ## @isTest Annotation
29
+
30
+ ```apex
31
+ @isTest
32
+ private class AccountServiceTest {
33
+
34
+ @TestSetup
35
+ static void makeData() {
36
+ // Runs once before any test method in this class
37
+ // Each test method gets a fresh transaction with this data
38
+ }
39
+
40
+ @isTest
41
+ static void testCreateAccount_validData_createsSuccessfully() {
42
+ // Arrange / Act / Assert
43
+ }
44
+ }
45
+ ```
46
+
47
+ Test classes do not count toward coverage calculations but DO count toward the 6 MB Apex code character limit. Use `@TestVisible` to make private members accessible in tests without changing access modifiers.
48
+
49
+ ---
50
+
51
+ ## @TestSetup
52
+
53
+ Runs once per test class. Each test method gets its own database rollback, so modifications in one test do not bleed into another.
54
+
55
+ ```apex
56
+ @TestSetup
57
+ static void makeData() {
58
+ Account acc = new Account(
59
+ Name = 'Test Corp',
60
+ Type = 'Customer',
61
+ AnnualRevenue = 1000000,
62
+ Customer_Tier__c = 'Standard'
63
+ );
64
+ insert acc;
65
+
66
+ List<Opportunity> opps = new List<Opportunity>();
67
+ for (Integer i = 0; i < 10; i++) {
68
+ opps.add(new Opportunity(
69
+ Name = 'Test Opp ' + i,
70
+ AccountId = acc.Id,
71
+ StageName = i < 5 ? 'Prospecting' : 'Qualification',
72
+ CloseDate = Date.today().addDays(30 + i),
73
+ Amount = 5000 * (i + 1)
74
+ ));
75
+ }
76
+ insert opps;
77
+ }
78
+ ```
79
+
80
+ ---
81
+
82
+ ## TestDataFactory Pattern
83
+
84
+ A central factory class creates test records consistently across the test suite, preventing duplicated record-creation logic.
85
+
86
+ ```apex
87
+ @isTest
88
+ public class TestDataFactory {
89
+
90
+ public static Account createAccount() {
91
+ return createAccount(new Map<String, Object>());
92
+ }
93
+
94
+ public static Account createAccount(Map<String, Object> overrides) {
95
+ Account acc = new Account(
96
+ Name = 'Test Account ' + generateUniqueString(),
97
+ Type = 'Customer',
98
+ Industry = 'Technology',
99
+ AnnualRevenue = 500000,
100
+ Customer_Tier__c = 'Standard'
101
+ );
102
+ applyOverrides(acc, overrides);
103
+ insert acc;
104
+ return acc;
105
+ }
106
+
107
+ public static List<Account> createAccounts(Integer count) {
108
+ List<Account> accounts = new List<Account>();
109
+ for (Integer i = 0; i < count; i++) {
110
+ accounts.add(new Account(
111
+ Name = 'Bulk Test Account ' + i,
112
+ Type = 'Customer',
113
+ Customer_Tier__c = 'Standard'
114
+ ));
115
+ }
116
+ insert accounts;
117
+ return accounts;
118
+ }
119
+
120
+ public static User createUserWithProfile(String profileName) {
121
+ Profile p = [SELECT Id FROM Profile WHERE Name = :profileName LIMIT 1];
122
+ User u = new User(
123
+ Alias = 'tstuser',
124
+ Email = generateUniqueString() + '@testfactory.example.com',
125
+ EmailEncodingKey = 'UTF-8',
126
+ LastName = 'Testing',
127
+ LanguageLocaleKey = 'en_US',
128
+ LocaleSidKey = 'en_US',
129
+ ProfileId = p.Id,
130
+ TimeZoneSidKey = 'America/Los_Angeles',
131
+ UserName = generateUniqueString() + '@testfactory.example.com'
132
+ );
133
+ insert u;
134
+ return u;
135
+ }
136
+
137
+ private static Integer uniqueCounter = 0;
138
+
139
+ private static String generateUniqueString() {
140
+ return String.valueOf(++uniqueCounter) + '_' +
141
+ String.valueOf(Datetime.now().getTime()).right(6);
142
+ }
143
+
144
+ private static void applyOverrides(SObject record, Map<String, Object> overrides) {
145
+ for (String fieldName : overrides.keySet()) {
146
+ record.put(fieldName, overrides.get(fieldName));
147
+ }
148
+ }
149
+ }
150
+ ```
151
+
152
+ ---
153
+
154
+ ## Test Structure: Arrange-Act-Assert
155
+
156
+ Every test method follows three phases with a blank line between them.
157
+
158
+ ```apex
159
+ @isTest
160
+ static void testCalculateDiscount_premiumTier_returns20Percent() {
161
+ // Arrange
162
+ Account acc = TestDataFactory.createAccount(
163
+ new Map<String, Object>{ 'Customer_Tier__c' => 'Premium' }
164
+ );
165
+ Decimal orderAmount = 10000;
166
+
167
+ // Act
168
+ Test.startTest();
169
+ Decimal discount = DiscountCalculator.calculate(acc.Id, orderAmount);
170
+ Test.stopTest();
171
+
172
+ // Assert
173
+ Assert.areEqual(2000, discount,
174
+ 'Premium tier accounts should receive a 20% discount on $10,000 orders');
175
+ }
176
+ ```
177
+
178
+ ### Test Method Naming
179
+
180
+ Format: `test{MethodName}_{scenario}_{expectedResult}`
181
+
182
+ ```
183
+ testCalculateDiscount_premiumTier_returns20Percent()
184
+ testCalculateDiscount_nullAmount_returnsZero()
185
+ testCreateAccount_duplicateName_addsFieldError()
186
+ ```
187
+
188
+ ---
189
+
190
+ ## Governor Limit Testing
191
+
192
+ Always test with 200 records (standard trigger batch size). A method that works with 1 record may fail at governor limits with 200.
193
+
194
+ ```apex
195
+ @isTest
196
+ static void testTrigger_bulkInsert_staysWithinLimits() {
197
+ List<Account> accounts = TestDataFactory.createAccounts(200);
198
+
199
+ List<Account> processed = [
200
+ SELECT Id, Customer_Tier__c FROM Account
201
+ WHERE Id IN :new Map<Id, Account>(accounts).keySet()
202
+ ];
203
+
204
+ System.assertEquals(200, processed.size(), 'All 200 accounts should be present');
205
+ }
206
+ ```
207
+
208
+ Use `Test.startTest()` / `Test.stopTest()` to reset governor limit counters, giving the code under test a fresh limit context.
209
+
210
+ ---
211
+
212
+ ## Exception Testing
213
+
214
+ ```apex
215
+ @isTest
216
+ static void testUpgradeToPremium_insufficientRevenue_throwsUpgradeException() {
217
+ Account acc = TestDataFactory.createAccount(
218
+ new Map<String, Object>{ 'AnnualRevenue' => 10000 }
219
+ );
220
+
221
+ Test.startTest();
222
+ try {
223
+ AccountsService.upgradeToPremium(new Set<Id>{ acc.Id });
224
+ Assert.fail('Expected UpgradeException was not thrown');
225
+ } catch (AccountsService.UpgradeException e) {
226
+ Assert.isTrue(
227
+ e.getMessage().contains('Annual revenue must be at least'),
228
+ 'Exception message should explain the reason. Got: ' + e.getMessage()
229
+ );
230
+ }
231
+ Test.stopTest();
232
+ }
233
+ ```
234
+
235
+ Use the `Assert` class (see @../_reference/API_VERSIONS.md for minimum version): `Assert.areEqual`, `Assert.isTrue`, `Assert.isNotNull`, `Assert.fail`.
236
+
237
+ ---
238
+
239
+ ## Permission Testing with System.runAs
240
+
241
+ ```apex
242
+ @isTest
243
+ static void testViewRestrictedReport_standardUser_throwsException() {
244
+ User standardUser = TestDataFactory.createUserWithProfile('Standard User');
245
+ Restricted_Report__c report = new Restricted_Report__c(Name = 'Confidential Q4');
246
+ insert report;
247
+
248
+ Test.startTest();
249
+ System.runAs(standardUser) {
250
+ try {
251
+ ReportService.viewReport(report.Id);
252
+ Assert.fail('Standard user should not be able to view restricted reports');
253
+ } catch (ReportService.AccessDeniedException e) {
254
+ Assert.isTrue(true, 'Expected AccessDeniedException thrown correctly');
255
+ }
256
+ }
257
+ Test.stopTest();
258
+ }
259
+ ```
260
+
261
+ ---
262
+
263
+ ## Async Testing
264
+
265
+ ### @future and Queueable
266
+
267
+ `Test.startTest()` / `Test.stopTest()` forces @future and Queueable jobs to execute synchronously.
268
+
269
+ ```apex
270
+ @isTest
271
+ static void testFutureCallout_sendsRequest() {
272
+ Test.setMock(HttpCalloutMock.class, new MockERPCallout(200, '{"status":"ok"}'));
273
+ Account acc = TestDataFactory.createAccount();
274
+
275
+ Test.startTest();
276
+ ExternalDataSync.syncAccountToERP(acc.Id);
277
+ Test.stopTest();
278
+
279
+ List<Integration_Error_Log__c> errors = [
280
+ SELECT Id FROM Integration_Error_Log__c WHERE Account__c = :acc.Id
281
+ ];
282
+ System.assertEquals(0, errors.size(), 'No errors should be logged');
283
+ }
284
+ ```
285
+
286
+ ### Batch Apex
287
+
288
+ ```apex
289
+ @isTest
290
+ static void testBatch_processesAllRecords() {
291
+ // insert 200 records
292
+ Test.startTest();
293
+ Database.executeBatch(new AccountAnnualReviewBatch(), 200);
294
+ Test.stopTest(); // start(), execute(), finish() all run synchronously
295
+ // Assert results
296
+ }
297
+ ```
298
+
299
+ ---
300
+
301
+ ## Mock Patterns
302
+
303
+ ### HttpCalloutMock
304
+
305
+ ```apex
306
+ @isTest
307
+ public class MockERPCallout implements HttpCalloutMock {
308
+ private Integer statusCode;
309
+ private String responseBody;
310
+
311
+ public MockERPCallout(Integer statusCode, String responseBody) {
312
+ this.statusCode = statusCode;
313
+ this.responseBody = responseBody;
314
+ }
315
+
316
+ public HttpResponse respond(HttpRequest req) {
317
+ HttpResponse res = new HttpResponse();
318
+ res.setStatusCode(statusCode);
319
+ res.setBody(responseBody);
320
+ res.setHeader('Content-Type', 'application/json');
321
+ return res;
322
+ }
323
+ }
324
+ ```
325
+
326
+ ### Multi-Callout Mock
327
+
328
+ ```apex
329
+ @isTest
330
+ public class MultiCalloutMock implements HttpCalloutMock {
331
+ private Map<String, HttpResponse> responses = new Map<String, HttpResponse>();
332
+
333
+ public MultiCalloutMock addResponse(String urlPattern, Integer statusCode, String body) {
334
+ HttpResponse res = new HttpResponse();
335
+ res.setStatusCode(statusCode);
336
+ res.setBody(body);
337
+ responses.put(urlPattern, res);
338
+ return this;
339
+ }
340
+
341
+ public HttpResponse respond(HttpRequest req) {
342
+ for (String pattern : responses.keySet()) {
343
+ if (req.getEndpoint().contains(pattern)) {
344
+ return responses.get(pattern);
345
+ }
346
+ }
347
+ throw new CalloutException('No mock response configured for: ' + req.getEndpoint());
348
+ }
349
+ }
350
+ ```
351
+
352
+ ### Stub API (System.StubProvider)
353
+
354
+ For mocking dependencies without HTTP, use the `System.StubProvider` interface with `Test.createStub()`.
355
+
356
+ ```apex
357
+ IAccountsSelector mockSelector = (IAccountsSelector)
358
+ Test.createStub(IAccountsSelector.class, new MockAccountsSelector(mockAccounts));
359
+ ```
360
+
361
+ > **Note:** The instance field must be typed as the interface (e.g., `IAccountsSelector`), not the concrete class. Casting a stub proxy to a concrete class throws TypeException at runtime.
362
+
363
+ ---
364
+
365
+ ## Coverage Strategy
366
+
367
+ Line coverage is misleading. A method with an `if` statement can show 100% line coverage if you only test the `true` branch. Test every branch.
368
+
369
+ ```apex
370
+ // This method has 4 branches — test each:
371
+ // 1. testCalculateDiscount_premiumTier
372
+ // 2. testCalculateDiscount_standardTier
373
+ // 3. testCalculateDiscount_unknownTier (else)
374
+ // 4. testCalculateDiscount_nullTier
375
+ public Decimal calculateDiscount(String tier, Decimal amount) {
376
+ if (tier == 'Premium') return amount * 0.20;
377
+ else if (tier == 'Standard') return amount * 0.10;
378
+ else return 0;
379
+ }
380
+ ```
381
+
382
+ ---
383
+
384
+ ## RunRelevantTests and @testFor (Spring '26)
385
+
386
+ > Requires the minimum API version for this feature (see @../_reference/API_VERSIONS.md). If `sfdx-project.json` specifies a `sourceApiVersion` below it, `RunRelevantTests` silently falls back to `RunLocalTests`.
387
+
388
+ `@testFor` explicitly declares which production class a test class covers, improving `RunRelevantTests` selection accuracy.
389
+
390
+ ```apex
391
+ @isTest
392
+ @testFor(AccountService)
393
+ private class AccountServiceTest {
394
+ // tests for AccountService
395
+ }
396
+ ```
397
+
398
+ **Rules:** Reference any top-level Apex class (not inner classes). Cannot be placed on non-test classes. Use separate test classes for each target — Apex does not support stacking duplicate annotations.
399
+
400
+ ---
401
+
402
+ ## Related
403
+
404
+ - **Agent**: `sf-apex-agent` — For interactive, in-depth guidance
405
+ - **Skills**: `sf-tdd-workflow` — TDD workflow for Apex
406
+
407
+ ### Guardrails
408
+
409
+ - `sf-testing-constraints` — Enforces test isolation, assertion requirements, SeeAllData prohibition, and coverage thresholds
@@ -0,0 +1,238 @@
1
+ ---
2
+ name: sf-api-design
3
+ description: "Salesforce API design — custom REST endpoints, batch operations, Composite API, error envelopes, auth. Use when designing APIs exposed from Salesforce. Do NOT use for outbound callouts or Platform Events."
4
+ origin: SCC
5
+ user-invocable: false
6
+ ---
7
+
8
+ # Salesforce API Design
9
+
10
+ Patterns for designing and implementing custom APIs on the Salesforce platform. Callout limits, Composite API limits, and Named Credential details live in the reference file.
11
+
12
+ @../_reference/INTEGRATION_PATTERNS.md
13
+
14
+ ## When to Use
15
+
16
+ - Designing custom REST or SOAP APIs exposed from Salesforce using Apex
17
+ - Establishing consistent API response envelopes and error handling patterns
18
+ - Configuring authentication for inbound API access
19
+ - Building batch REST endpoints for bulk operations
20
+ - Reviewing Apex API code for security (CRUD/FLS, input validation, status codes)
21
+
22
+ ---
23
+
24
+ ## Custom REST API Pattern
25
+
26
+ ```apex
27
+ @RestResource(urlMapping='/api/accounts/*')
28
+ global with sharing class AccountAPI {
29
+
30
+ @HttpGet
31
+ global static void getAccount() {
32
+ RestRequest req = RestContext.request;
33
+ RestResponse res = RestContext.response;
34
+
35
+ String accountId = req.requestURI.substringAfterLast('/');
36
+
37
+ try {
38
+ Account acc = [
39
+ SELECT Id, Name, Industry, AnnualRevenue
40
+ FROM Account WHERE Id = :accountId
41
+ WITH USER_MODE LIMIT 1
42
+ ];
43
+
44
+ res.statusCode = 200;
45
+ res.responseBody = Blob.valueOf(JSON.serialize(
46
+ new ApiResponse(true, acc, null)));
47
+ } catch (QueryException e) {
48
+ res.statusCode = 404;
49
+ res.responseBody = Blob.valueOf(JSON.serialize(
50
+ new ApiResponse(false, null, 'Account not found')));
51
+ } catch (Exception e) {
52
+ res.statusCode = 500;
53
+ res.responseBody = Blob.valueOf(JSON.serialize(
54
+ new ApiResponse(false, null, 'An internal error occurred')));
55
+ }
56
+ }
57
+
58
+ @HttpPost
59
+ global static void createAccount() {
60
+ RestRequest req = RestContext.request;
61
+ RestResponse res = RestContext.response;
62
+
63
+ try {
64
+ Object parsed = JSON.deserializeUntyped(req.requestBody.toString());
65
+ if (!(parsed instanceof Map<String, Object>)) {
66
+ res.statusCode = 400;
67
+ res.responseBody = Blob.valueOf(JSON.serialize(
68
+ new ApiResponse(false, null,
69
+ 'Expected JSON object, got ' +
70
+ (parsed instanceof List<Object> ? 'array' : 'primitive')
71
+ )));
72
+ return;
73
+ }
74
+ Map<String, Object> body = (Map<String, Object>) parsed;
75
+ Account acc = new Account(
76
+ Name = (String) body.get('name'),
77
+ Industry = (String) body.get('industry')
78
+ );
79
+
80
+ // stripInaccessible — check getRemovedFields() to avoid silent data loss
81
+ SObjectAccessDecision decision = Security.stripInaccessible(
82
+ AccessType.CREATABLE, new List<Account>{acc});
83
+ if (!decision.getRemovedFields().isEmpty()) {
84
+ res.statusCode = 403;
85
+ res.responseBody = Blob.valueOf(JSON.serialize(
86
+ new ApiResponse(false, null,
87
+ 'Insufficient field permissions for: ' +
88
+ decision.getRemovedFields())));
89
+ return;
90
+ }
91
+ insert decision.getRecords();
92
+
93
+ res.statusCode = 201;
94
+ res.responseBody = Blob.valueOf(JSON.serialize(
95
+ new ApiResponse(true, decision.getRecords()[0], null)));
96
+ } catch (Exception e) {
97
+ res.statusCode = 400;
98
+ res.responseBody = Blob.valueOf(JSON.serialize(
99
+ new ApiResponse(false, null, e.getMessage())));
100
+ }
101
+ }
102
+
103
+ global class ApiResponse {
104
+ public Boolean success;
105
+ public Object data;
106
+ public String error;
107
+
108
+ public ApiResponse(Boolean success, Object data, String error) {
109
+ this.success = success;
110
+ this.data = data;
111
+ this.error = error;
112
+ }
113
+ }
114
+ }
115
+ ```
116
+
117
+ ---
118
+
119
+ ## Batch REST Endpoint Pattern
120
+
121
+ ```apex
122
+ @HttpPost
123
+ global static void bulkCreate() {
124
+ RestRequest req = RestContext.request;
125
+ RestResponse res = RestContext.response;
126
+
127
+ try {
128
+ List<Object> items = (List<Object>) JSON.deserializeUntyped(
129
+ req.requestBody.toString());
130
+ List<Account> accounts = new List<Account>();
131
+
132
+ for (Object item : items) {
133
+ Map<String, Object> fields = (Map<String, Object>) item;
134
+ accounts.add(new Account(
135
+ Name = (String) fields.get('name'),
136
+ Industry = (String) fields.get('industry')
137
+ ));
138
+ }
139
+
140
+ SObjectAccessDecision decision = Security.stripInaccessible(
141
+ AccessType.CREATABLE, accounts);
142
+ List<Database.SaveResult> results =
143
+ Database.insert(decision.getRecords(), false);
144
+
145
+ List<Object> response = new List<Object>();
146
+ for (Integer i = 0; i < results.size(); i++) {
147
+ Map<String, Object> row = new Map<String, Object>();
148
+ row.put('index', i);
149
+ row.put('success', results[i].isSuccess());
150
+ row.put('id', results[i].isSuccess() ? results[i].getId() : null);
151
+ if (!results[i].isSuccess()) {
152
+ row.put('errors', results[i].getErrors()[0].getMessage());
153
+ }
154
+ response.add(row);
155
+ }
156
+
157
+ res.statusCode = 200;
158
+ res.responseBody = Blob.valueOf(JSON.serialize(
159
+ new ApiResponse(true, response, null)));
160
+ } catch (Exception e) {
161
+ res.statusCode = 400;
162
+ res.responseBody = Blob.valueOf(JSON.serialize(
163
+ new ApiResponse(false, null, e.getMessage())));
164
+ }
165
+ }
166
+ ```
167
+
168
+ ---
169
+
170
+ ## Error Handling Patterns
171
+
172
+ Structured error codes for API consumers:
173
+
174
+ ```apex
175
+ global class ApiError {
176
+ public String code;
177
+ public String message;
178
+ public String field;
179
+
180
+ public ApiError(String code, String message, String field) {
181
+ this.code = code;
182
+ this.message = message;
183
+ this.field = field;
184
+ }
185
+ }
186
+
187
+ // Standard error codes:
188
+ // FIELD_REQUIRED — Missing required field
189
+ // RECORD_NOT_FOUND — Record ID doesn't exist or no access
190
+ // GOVERNOR_LIMIT — Operation would exceed governor limits
191
+ // INSUFFICIENT_ACCESS — User lacks CRUD/FLS permission
192
+ // VALIDATION_FAILED — Validation rule or trigger prevented save
193
+ // DUPLICATE_VALUE — Unique field constraint violated
194
+ ```
195
+
196
+ ---
197
+
198
+ ## Authentication for Inbound APIs
199
+
200
+ | Method | Use When | Setup |
201
+ |--------|----------|-------|
202
+ | Named Principal | All API users share one Salesforce user | Connected App + single auth |
203
+ | Per-User | Each API caller maps to a Salesforce user | Connected App + OAuth per user |
204
+ | JWT Bearer | Server-to-server, no user interaction | Connected App + X.509 certificate |
205
+ | API Key (Custom) | Simple external tools | Custom Metadata + header validation |
206
+
207
+ ---
208
+
209
+ ## Anti-Patterns
210
+
211
+ | Anti-Pattern | Problem | Fix |
212
+ |-------------|---------|-----|
213
+ | God endpoint (all CRUD in one method) | Hard to maintain and test | One method per operation (@HttpGet, @HttpPost) |
214
+ | No pagination | Timeouts, governor limits | Add LIMIT + OFFSET or cursor-based pagination |
215
+ | Exposing internal Salesforce IDs | Security risk, breaks across orgs | Use external IDs or custom identifiers |
216
+ | No error codes | Consumers can't programmatically handle errors | Return structured error codes |
217
+ | No API versioning | Breaking changes affect all consumers | Version via URL path: `/api/v1/accounts/` |
218
+ | `WITHOUT SHARING` on API class | Bypasses record-level security | Use `WITH SHARING` on REST resources |
219
+ | Returning all fields | Wastes bandwidth, exposes sensitive data | Return only requested/needed fields |
220
+
221
+ ---
222
+
223
+ ## Best Practices
224
+
225
+ - Use `WITH USER_MODE` in SOQL and `AccessLevel.USER_MODE` in DML
226
+ - Use `Security.stripInaccessible()` when you need field-level enforcement on DML -- check `getRemovedFields()` for critical fields
227
+ - Return consistent response envelopes (success, data, error)
228
+ - Use proper HTTP status codes (200, 201, 400, 404, 500)
229
+ - Implement rate limiting awareness (API request limits)
230
+ - Version APIs via URL path (`/api/v1/accounts/`)
231
+ - Use `Database.insert(records, false)` for bulk APIs to support partial success
232
+
233
+ ---
234
+
235
+ ## Related
236
+
237
+ - Constraints: sf-security-constraints
238
+ - Reference: @../_reference/INTEGRATION_PATTERNS.md