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,407 @@
1
+ ---
2
+ name: sf-apex-testing
3
+ description: >-
4
+ 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.
5
+ ---
6
+
7
+ # Apex Testing
8
+
9
+ 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.
10
+
11
+ Reference: @../_reference/TESTING_STANDARDS.md
12
+
13
+ ---
14
+
15
+ ## When to Use
16
+
17
+ - When writing test classes for new Apex code before deploying to production
18
+ - When existing tests pass but only cover happy paths
19
+ - When a deployment fails with "insufficient code coverage" errors
20
+ - When building mock patterns for callouts or dependency injection
21
+
22
+ > **Related:** For the TDD workflow (red-green-refactor process), see `sf-tdd-workflow`.
23
+
24
+ ---
25
+
26
+ ## @isTest Annotation
27
+
28
+ ```apex
29
+ @isTest
30
+ private class AccountServiceTest {
31
+
32
+ @TestSetup
33
+ static void makeData() {
34
+ // Runs once before any test method in this class
35
+ // Each test method gets a fresh transaction with this data
36
+ }
37
+
38
+ @isTest
39
+ static void testCreateAccount_validData_createsSuccessfully() {
40
+ // Arrange / Act / Assert
41
+ }
42
+ }
43
+ ```
44
+
45
+ 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.
46
+
47
+ ---
48
+
49
+ ## @TestSetup
50
+
51
+ Runs once per test class. Each test method gets its own database rollback, so modifications in one test do not bleed into another.
52
+
53
+ ```apex
54
+ @TestSetup
55
+ static void makeData() {
56
+ Account acc = new Account(
57
+ Name = 'Test Corp',
58
+ Type = 'Customer',
59
+ AnnualRevenue = 1000000,
60
+ Customer_Tier__c = 'Standard'
61
+ );
62
+ insert acc;
63
+
64
+ List<Opportunity> opps = new List<Opportunity>();
65
+ for (Integer i = 0; i < 10; i++) {
66
+ opps.add(new Opportunity(
67
+ Name = 'Test Opp ' + i,
68
+ AccountId = acc.Id,
69
+ StageName = i < 5 ? 'Prospecting' : 'Qualification',
70
+ CloseDate = Date.today().addDays(30 + i),
71
+ Amount = 5000 * (i + 1)
72
+ ));
73
+ }
74
+ insert opps;
75
+ }
76
+ ```
77
+
78
+ ---
79
+
80
+ ## TestDataFactory Pattern
81
+
82
+ A central factory class creates test records consistently across the test suite, preventing duplicated record-creation logic.
83
+
84
+ ```apex
85
+ @isTest
86
+ public class TestDataFactory {
87
+
88
+ public static Account createAccount() {
89
+ return createAccount(new Map<String, Object>());
90
+ }
91
+
92
+ public static Account createAccount(Map<String, Object> overrides) {
93
+ Account acc = new Account(
94
+ Name = 'Test Account ' + generateUniqueString(),
95
+ Type = 'Customer',
96
+ Industry = 'Technology',
97
+ AnnualRevenue = 500000,
98
+ Customer_Tier__c = 'Standard'
99
+ );
100
+ applyOverrides(acc, overrides);
101
+ insert acc;
102
+ return acc;
103
+ }
104
+
105
+ public static List<Account> createAccounts(Integer count) {
106
+ List<Account> accounts = new List<Account>();
107
+ for (Integer i = 0; i < count; i++) {
108
+ accounts.add(new Account(
109
+ Name = 'Bulk Test Account ' + i,
110
+ Type = 'Customer',
111
+ Customer_Tier__c = 'Standard'
112
+ ));
113
+ }
114
+ insert accounts;
115
+ return accounts;
116
+ }
117
+
118
+ public static User createUserWithProfile(String profileName) {
119
+ Profile p = [SELECT Id FROM Profile WHERE Name = :profileName LIMIT 1];
120
+ User u = new User(
121
+ Alias = 'tstuser',
122
+ Email = generateUniqueString() + '@testfactory.example.com',
123
+ EmailEncodingKey = 'UTF-8',
124
+ LastName = 'Testing',
125
+ LanguageLocaleKey = 'en_US',
126
+ LocaleSidKey = 'en_US',
127
+ ProfileId = p.Id,
128
+ TimeZoneSidKey = 'America/Los_Angeles',
129
+ UserName = generateUniqueString() + '@testfactory.example.com'
130
+ );
131
+ insert u;
132
+ return u;
133
+ }
134
+
135
+ private static Integer uniqueCounter = 0;
136
+
137
+ private static String generateUniqueString() {
138
+ return String.valueOf(++uniqueCounter) + '_' +
139
+ String.valueOf(Datetime.now().getTime()).right(6);
140
+ }
141
+
142
+ private static void applyOverrides(SObject record, Map<String, Object> overrides) {
143
+ for (String fieldName : overrides.keySet()) {
144
+ record.put(fieldName, overrides.get(fieldName));
145
+ }
146
+ }
147
+ }
148
+ ```
149
+
150
+ ---
151
+
152
+ ## Test Structure: Arrange-Act-Assert
153
+
154
+ Every test method follows three phases with a blank line between them.
155
+
156
+ ```apex
157
+ @isTest
158
+ static void testCalculateDiscount_premiumTier_returns20Percent() {
159
+ // Arrange
160
+ Account acc = TestDataFactory.createAccount(
161
+ new Map<String, Object>{ 'Customer_Tier__c' => 'Premium' }
162
+ );
163
+ Decimal orderAmount = 10000;
164
+
165
+ // Act
166
+ Test.startTest();
167
+ Decimal discount = DiscountCalculator.calculate(acc.Id, orderAmount);
168
+ Test.stopTest();
169
+
170
+ // Assert
171
+ Assert.areEqual(2000, discount,
172
+ 'Premium tier accounts should receive a 20% discount on $10,000 orders');
173
+ }
174
+ ```
175
+
176
+ ### Test Method Naming
177
+
178
+ Format: `test{MethodName}_{scenario}_{expectedResult}`
179
+
180
+ ```
181
+ testCalculateDiscount_premiumTier_returns20Percent()
182
+ testCalculateDiscount_nullAmount_returnsZero()
183
+ testCreateAccount_duplicateName_addsFieldError()
184
+ ```
185
+
186
+ ---
187
+
188
+ ## Governor Limit Testing
189
+
190
+ Always test with 200 records (standard trigger batch size). A method that works with 1 record may fail at governor limits with 200.
191
+
192
+ ```apex
193
+ @isTest
194
+ static void testTrigger_bulkInsert_staysWithinLimits() {
195
+ List<Account> accounts = TestDataFactory.createAccounts(200);
196
+
197
+ List<Account> processed = [
198
+ SELECT Id, Customer_Tier__c FROM Account
199
+ WHERE Id IN :new Map<Id, Account>(accounts).keySet()
200
+ ];
201
+
202
+ System.assertEquals(200, processed.size(), 'All 200 accounts should be present');
203
+ }
204
+ ```
205
+
206
+ Use `Test.startTest()` / `Test.stopTest()` to reset governor limit counters, giving the code under test a fresh limit context.
207
+
208
+ ---
209
+
210
+ ## Exception Testing
211
+
212
+ ```apex
213
+ @isTest
214
+ static void testUpgradeToPremium_insufficientRevenue_throwsUpgradeException() {
215
+ Account acc = TestDataFactory.createAccount(
216
+ new Map<String, Object>{ 'AnnualRevenue' => 10000 }
217
+ );
218
+
219
+ Test.startTest();
220
+ try {
221
+ AccountsService.upgradeToPremium(new Set<Id>{ acc.Id });
222
+ Assert.fail('Expected UpgradeException was not thrown');
223
+ } catch (AccountsService.UpgradeException e) {
224
+ Assert.isTrue(
225
+ e.getMessage().contains('Annual revenue must be at least'),
226
+ 'Exception message should explain the reason. Got: ' + e.getMessage()
227
+ );
228
+ }
229
+ Test.stopTest();
230
+ }
231
+ ```
232
+
233
+ Use the `Assert` class (see @../_reference/API_VERSIONS.md for minimum version): `Assert.areEqual`, `Assert.isTrue`, `Assert.isNotNull`, `Assert.fail`.
234
+
235
+ ---
236
+
237
+ ## Permission Testing with System.runAs
238
+
239
+ ```apex
240
+ @isTest
241
+ static void testViewRestrictedReport_standardUser_throwsException() {
242
+ User standardUser = TestDataFactory.createUserWithProfile('Standard User');
243
+ Restricted_Report__c report = new Restricted_Report__c(Name = 'Confidential Q4');
244
+ insert report;
245
+
246
+ Test.startTest();
247
+ System.runAs(standardUser) {
248
+ try {
249
+ ReportService.viewReport(report.Id);
250
+ Assert.fail('Standard user should not be able to view restricted reports');
251
+ } catch (ReportService.AccessDeniedException e) {
252
+ Assert.isTrue(true, 'Expected AccessDeniedException thrown correctly');
253
+ }
254
+ }
255
+ Test.stopTest();
256
+ }
257
+ ```
258
+
259
+ ---
260
+
261
+ ## Async Testing
262
+
263
+ ### @future and Queueable
264
+
265
+ `Test.startTest()` / `Test.stopTest()` forces @future and Queueable jobs to execute synchronously.
266
+
267
+ ```apex
268
+ @isTest
269
+ static void testFutureCallout_sendsRequest() {
270
+ Test.setMock(HttpCalloutMock.class, new MockERPCallout(200, '{"status":"ok"}'));
271
+ Account acc = TestDataFactory.createAccount();
272
+
273
+ Test.startTest();
274
+ ExternalDataSync.syncAccountToERP(acc.Id);
275
+ Test.stopTest();
276
+
277
+ List<Integration_Error_Log__c> errors = [
278
+ SELECT Id FROM Integration_Error_Log__c WHERE Account__c = :acc.Id
279
+ ];
280
+ System.assertEquals(0, errors.size(), 'No errors should be logged');
281
+ }
282
+ ```
283
+
284
+ ### Batch Apex
285
+
286
+ ```apex
287
+ @isTest
288
+ static void testBatch_processesAllRecords() {
289
+ // insert 200 records
290
+ Test.startTest();
291
+ Database.executeBatch(new AccountAnnualReviewBatch(), 200);
292
+ Test.stopTest(); // start(), execute(), finish() all run synchronously
293
+ // Assert results
294
+ }
295
+ ```
296
+
297
+ ---
298
+
299
+ ## Mock Patterns
300
+
301
+ ### HttpCalloutMock
302
+
303
+ ```apex
304
+ @isTest
305
+ public class MockERPCallout implements HttpCalloutMock {
306
+ private Integer statusCode;
307
+ private String responseBody;
308
+
309
+ public MockERPCallout(Integer statusCode, String responseBody) {
310
+ this.statusCode = statusCode;
311
+ this.responseBody = responseBody;
312
+ }
313
+
314
+ public HttpResponse respond(HttpRequest req) {
315
+ HttpResponse res = new HttpResponse();
316
+ res.setStatusCode(statusCode);
317
+ res.setBody(responseBody);
318
+ res.setHeader('Content-Type', 'application/json');
319
+ return res;
320
+ }
321
+ }
322
+ ```
323
+
324
+ ### Multi-Callout Mock
325
+
326
+ ```apex
327
+ @isTest
328
+ public class MultiCalloutMock implements HttpCalloutMock {
329
+ private Map<String, HttpResponse> responses = new Map<String, HttpResponse>();
330
+
331
+ public MultiCalloutMock addResponse(String urlPattern, Integer statusCode, String body) {
332
+ HttpResponse res = new HttpResponse();
333
+ res.setStatusCode(statusCode);
334
+ res.setBody(body);
335
+ responses.put(urlPattern, res);
336
+ return this;
337
+ }
338
+
339
+ public HttpResponse respond(HttpRequest req) {
340
+ for (String pattern : responses.keySet()) {
341
+ if (req.getEndpoint().contains(pattern)) {
342
+ return responses.get(pattern);
343
+ }
344
+ }
345
+ throw new CalloutException('No mock response configured for: ' + req.getEndpoint());
346
+ }
347
+ }
348
+ ```
349
+
350
+ ### Stub API (System.StubProvider)
351
+
352
+ For mocking dependencies without HTTP, use the `System.StubProvider` interface with `Test.createStub()`.
353
+
354
+ ```apex
355
+ IAccountsSelector mockSelector = (IAccountsSelector)
356
+ Test.createStub(IAccountsSelector.class, new MockAccountsSelector(mockAccounts));
357
+ ```
358
+
359
+ > **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.
360
+
361
+ ---
362
+
363
+ ## Coverage Strategy
364
+
365
+ 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.
366
+
367
+ ```apex
368
+ // This method has 4 branches — test each:
369
+ // 1. testCalculateDiscount_premiumTier
370
+ // 2. testCalculateDiscount_standardTier
371
+ // 3. testCalculateDiscount_unknownTier (else)
372
+ // 4. testCalculateDiscount_nullTier
373
+ public Decimal calculateDiscount(String tier, Decimal amount) {
374
+ if (tier == 'Premium') return amount * 0.20;
375
+ else if (tier == 'Standard') return amount * 0.10;
376
+ else return 0;
377
+ }
378
+ ```
379
+
380
+ ---
381
+
382
+ ## RunRelevantTests and @testFor (Spring '26)
383
+
384
+ > 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`.
385
+
386
+ `@testFor` explicitly declares which production class a test class covers, improving `RunRelevantTests` selection accuracy.
387
+
388
+ ```apex
389
+ @isTest
390
+ @testFor(AccountService)
391
+ private class AccountServiceTest {
392
+ // tests for AccountService
393
+ }
394
+ ```
395
+
396
+ **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.
397
+
398
+ ---
399
+
400
+ ## Related
401
+
402
+ - **Agent**: `sf-apex-agent` — For interactive, in-depth guidance
403
+ - **Skills**: `sf-tdd-workflow` — TDD workflow for Apex
404
+
405
+ ### Guardrails
406
+
407
+ - `sf-testing-constraints` — Enforces test isolation, assertion requirements, SeeAllData prohibition, and coverage thresholds
@@ -0,0 +1,237 @@
1
+ ---
2
+ name: sf-api-design
3
+ description: >-
4
+ 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.
5
+ ---
6
+
7
+ # Salesforce API Design
8
+
9
+ 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.
10
+
11
+ @../_reference/INTEGRATION_PATTERNS.md
12
+
13
+ ## When to Use
14
+
15
+ - Designing custom REST or SOAP APIs exposed from Salesforce using Apex
16
+ - Establishing consistent API response envelopes and error handling patterns
17
+ - Configuring authentication for inbound API access
18
+ - Building batch REST endpoints for bulk operations
19
+ - Reviewing Apex API code for security (CRUD/FLS, input validation, status codes)
20
+
21
+ ---
22
+
23
+ ## Custom REST API Pattern
24
+
25
+ ```apex
26
+ @RestResource(urlMapping='/api/accounts/*')
27
+ global with sharing class AccountAPI {
28
+
29
+ @HttpGet
30
+ global static void getAccount() {
31
+ RestRequest req = RestContext.request;
32
+ RestResponse res = RestContext.response;
33
+
34
+ String accountId = req.requestURI.substringAfterLast('/');
35
+
36
+ try {
37
+ Account acc = [
38
+ SELECT Id, Name, Industry, AnnualRevenue
39
+ FROM Account WHERE Id = :accountId
40
+ WITH USER_MODE LIMIT 1
41
+ ];
42
+
43
+ res.statusCode = 200;
44
+ res.responseBody = Blob.valueOf(JSON.serialize(
45
+ new ApiResponse(true, acc, null)));
46
+ } catch (QueryException e) {
47
+ res.statusCode = 404;
48
+ res.responseBody = Blob.valueOf(JSON.serialize(
49
+ new ApiResponse(false, null, 'Account not found')));
50
+ } catch (Exception e) {
51
+ res.statusCode = 500;
52
+ res.responseBody = Blob.valueOf(JSON.serialize(
53
+ new ApiResponse(false, null, 'An internal error occurred')));
54
+ }
55
+ }
56
+
57
+ @HttpPost
58
+ global static void createAccount() {
59
+ RestRequest req = RestContext.request;
60
+ RestResponse res = RestContext.response;
61
+
62
+ try {
63
+ Object parsed = JSON.deserializeUntyped(req.requestBody.toString());
64
+ if (!(parsed instanceof Map<String, Object>)) {
65
+ res.statusCode = 400;
66
+ res.responseBody = Blob.valueOf(JSON.serialize(
67
+ new ApiResponse(false, null,
68
+ 'Expected JSON object, got ' +
69
+ (parsed instanceof List<Object> ? 'array' : 'primitive')
70
+ )));
71
+ return;
72
+ }
73
+ Map<String, Object> body = (Map<String, Object>) parsed;
74
+ Account acc = new Account(
75
+ Name = (String) body.get('name'),
76
+ Industry = (String) body.get('industry')
77
+ );
78
+
79
+ // stripInaccessible — check getRemovedFields() to avoid silent data loss
80
+ SObjectAccessDecision decision = Security.stripInaccessible(
81
+ AccessType.CREATABLE, new List<Account>{acc});
82
+ if (!decision.getRemovedFields().isEmpty()) {
83
+ res.statusCode = 403;
84
+ res.responseBody = Blob.valueOf(JSON.serialize(
85
+ new ApiResponse(false, null,
86
+ 'Insufficient field permissions for: ' +
87
+ decision.getRemovedFields())));
88
+ return;
89
+ }
90
+ insert decision.getRecords();
91
+
92
+ res.statusCode = 201;
93
+ res.responseBody = Blob.valueOf(JSON.serialize(
94
+ new ApiResponse(true, decision.getRecords()[0], null)));
95
+ } catch (Exception e) {
96
+ res.statusCode = 400;
97
+ res.responseBody = Blob.valueOf(JSON.serialize(
98
+ new ApiResponse(false, null, e.getMessage())));
99
+ }
100
+ }
101
+
102
+ global class ApiResponse {
103
+ public Boolean success;
104
+ public Object data;
105
+ public String error;
106
+
107
+ public ApiResponse(Boolean success, Object data, String error) {
108
+ this.success = success;
109
+ this.data = data;
110
+ this.error = error;
111
+ }
112
+ }
113
+ }
114
+ ```
115
+
116
+ ---
117
+
118
+ ## Batch REST Endpoint Pattern
119
+
120
+ ```apex
121
+ @HttpPost
122
+ global static void bulkCreate() {
123
+ RestRequest req = RestContext.request;
124
+ RestResponse res = RestContext.response;
125
+
126
+ try {
127
+ List<Object> items = (List<Object>) JSON.deserializeUntyped(
128
+ req.requestBody.toString());
129
+ List<Account> accounts = new List<Account>();
130
+
131
+ for (Object item : items) {
132
+ Map<String, Object> fields = (Map<String, Object>) item;
133
+ accounts.add(new Account(
134
+ Name = (String) fields.get('name'),
135
+ Industry = (String) fields.get('industry')
136
+ ));
137
+ }
138
+
139
+ SObjectAccessDecision decision = Security.stripInaccessible(
140
+ AccessType.CREATABLE, accounts);
141
+ List<Database.SaveResult> results =
142
+ Database.insert(decision.getRecords(), false);
143
+
144
+ List<Object> response = new List<Object>();
145
+ for (Integer i = 0; i < results.size(); i++) {
146
+ Map<String, Object> row = new Map<String, Object>();
147
+ row.put('index', i);
148
+ row.put('success', results[i].isSuccess());
149
+ row.put('id', results[i].isSuccess() ? results[i].getId() : null);
150
+ if (!results[i].isSuccess()) {
151
+ row.put('errors', results[i].getErrors()[0].getMessage());
152
+ }
153
+ response.add(row);
154
+ }
155
+
156
+ res.statusCode = 200;
157
+ res.responseBody = Blob.valueOf(JSON.serialize(
158
+ new ApiResponse(true, response, null)));
159
+ } catch (Exception e) {
160
+ res.statusCode = 400;
161
+ res.responseBody = Blob.valueOf(JSON.serialize(
162
+ new ApiResponse(false, null, e.getMessage())));
163
+ }
164
+ }
165
+ ```
166
+
167
+ ---
168
+
169
+ ## Error Handling Patterns
170
+
171
+ Structured error codes for API consumers:
172
+
173
+ ```apex
174
+ global class ApiError {
175
+ public String code;
176
+ public String message;
177
+ public String field;
178
+
179
+ public ApiError(String code, String message, String field) {
180
+ this.code = code;
181
+ this.message = message;
182
+ this.field = field;
183
+ }
184
+ }
185
+
186
+ // Standard error codes:
187
+ // FIELD_REQUIRED — Missing required field
188
+ // RECORD_NOT_FOUND — Record ID doesn't exist or no access
189
+ // GOVERNOR_LIMIT — Operation would exceed governor limits
190
+ // INSUFFICIENT_ACCESS — User lacks CRUD/FLS permission
191
+ // VALIDATION_FAILED — Validation rule or trigger prevented save
192
+ // DUPLICATE_VALUE — Unique field constraint violated
193
+ ```
194
+
195
+ ---
196
+
197
+ ## Authentication for Inbound APIs
198
+
199
+ | Method | Use When | Setup |
200
+ |--------|----------|-------|
201
+ | Named Principal | All API users share one Salesforce user | Connected App + single auth |
202
+ | Per-User | Each API caller maps to a Salesforce user | Connected App + OAuth per user |
203
+ | JWT Bearer | Server-to-server, no user interaction | Connected App + X.509 certificate |
204
+ | API Key (Custom) | Simple external tools | Custom Metadata + header validation |
205
+
206
+ ---
207
+
208
+ ## Anti-Patterns
209
+
210
+ | Anti-Pattern | Problem | Fix |
211
+ |-------------|---------|-----|
212
+ | God endpoint (all CRUD in one method) | Hard to maintain and test | One method per operation (@HttpGet, @HttpPost) |
213
+ | No pagination | Timeouts, governor limits | Add LIMIT + OFFSET or cursor-based pagination |
214
+ | Exposing internal Salesforce IDs | Security risk, breaks across orgs | Use external IDs or custom identifiers |
215
+ | No error codes | Consumers can't programmatically handle errors | Return structured error codes |
216
+ | No API versioning | Breaking changes affect all consumers | Version via URL path: `/api/v1/accounts/` |
217
+ | `WITHOUT SHARING` on API class | Bypasses record-level security | Use `WITH SHARING` on REST resources |
218
+ | Returning all fields | Wastes bandwidth, exposes sensitive data | Return only requested/needed fields |
219
+
220
+ ---
221
+
222
+ ## Best Practices
223
+
224
+ - Use `WITH USER_MODE` in SOQL and `AccessLevel.USER_MODE` in DML
225
+ - Use `Security.stripInaccessible()` when you need field-level enforcement on DML -- check `getRemovedFields()` for critical fields
226
+ - Return consistent response envelopes (success, data, error)
227
+ - Use proper HTTP status codes (200, 201, 400, 404, 500)
228
+ - Implement rate limiting awareness (API request limits)
229
+ - Version APIs via URL path (`/api/v1/accounts/`)
230
+ - Use `Database.insert(records, false)` for bulk APIs to support partial success
231
+
232
+ ---
233
+
234
+ ## Related
235
+
236
+ - Constraints: sf-security-constraints
237
+ - Reference: @../_reference/INTEGRATION_PATTERNS.md