@coralai/sps-cli 0.41.2 → 0.43.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 (168) hide show
  1. package/README.md +34 -3
  2. package/dist/commands/cardAdd.d.ts +1 -1
  3. package/dist/commands/cardAdd.d.ts.map +1 -1
  4. package/dist/commands/cardAdd.js +16 -6
  5. package/dist/commands/cardAdd.js.map +1 -1
  6. package/dist/commands/cardDashboard.js +1 -1
  7. package/dist/commands/cardDashboard.js.map +1 -1
  8. package/dist/commands/doctor.d.ts +9 -0
  9. package/dist/commands/doctor.d.ts.map +1 -1
  10. package/dist/commands/doctor.js +3 -314
  11. package/dist/commands/doctor.js.map +1 -1
  12. package/dist/commands/hookCommand.d.ts.map +1 -1
  13. package/dist/commands/hookCommand.js +6 -7
  14. package/dist/commands/hookCommand.js.map +1 -1
  15. package/dist/commands/pmCommand.js +1 -1
  16. package/dist/commands/pmCommand.js.map +1 -1
  17. package/dist/commands/projectInit.d.ts.map +1 -1
  18. package/dist/commands/projectInit.js +60 -37
  19. package/dist/commands/projectInit.js.map +1 -1
  20. package/dist/commands/setup.d.ts.map +1 -1
  21. package/dist/commands/setup.js +3 -30
  22. package/dist/commands/setup.js.map +1 -1
  23. package/dist/commands/skillCommand.d.ts +2 -0
  24. package/dist/commands/skillCommand.d.ts.map +1 -0
  25. package/dist/commands/skillCommand.js +235 -0
  26. package/dist/commands/skillCommand.js.map +1 -0
  27. package/dist/commands/tick.js +1 -1
  28. package/dist/commands/tick.js.map +1 -1
  29. package/dist/core/checklist.d.ts +22 -0
  30. package/dist/core/checklist.d.ts.map +1 -0
  31. package/dist/core/checklist.js +38 -0
  32. package/dist/core/checklist.js.map +1 -0
  33. package/dist/core/checklist.test.d.ts +2 -0
  34. package/dist/core/checklist.test.d.ts.map +1 -0
  35. package/dist/core/checklist.test.js +74 -0
  36. package/dist/core/checklist.test.js.map +1 -0
  37. package/dist/core/config.d.ts +1 -1
  38. package/dist/core/config.d.ts.map +1 -1
  39. package/dist/core/config.js +1 -1
  40. package/dist/core/config.js.map +1 -1
  41. package/dist/core/config.test.js +7 -4
  42. package/dist/core/config.test.js.map +1 -1
  43. package/dist/core/context.d.ts +1 -1
  44. package/dist/core/context.d.ts.map +1 -1
  45. package/dist/core/skillStore.d.ts +46 -0
  46. package/dist/core/skillStore.d.ts.map +1 -0
  47. package/dist/core/skillStore.js +197 -0
  48. package/dist/core/skillStore.js.map +1 -0
  49. package/dist/core/skillStore.test.d.ts +2 -0
  50. package/dist/core/skillStore.test.d.ts.map +1 -0
  51. package/dist/core/skillStore.test.js +190 -0
  52. package/dist/core/skillStore.test.js.map +1 -0
  53. package/dist/engines/EventHandler.test.js +3 -3
  54. package/dist/engines/EventHandler.test.js.map +1 -1
  55. package/dist/engines/MonitorEngine.js +2 -2
  56. package/dist/engines/MonitorEngine.js.map +1 -1
  57. package/dist/engines/SchedulerEngine.js +1 -1
  58. package/dist/engines/SchedulerEngine.js.map +1 -1
  59. package/dist/engines/StageEngine.js +3 -3
  60. package/dist/engines/StageEngine.js.map +1 -1
  61. package/dist/engines/engine-pipeline-adapter.test.js +2 -2
  62. package/dist/engines/engine-pipeline-adapter.test.js.map +1 -1
  63. package/dist/interfaces/TaskBackend.d.ts +3 -1
  64. package/dist/interfaces/TaskBackend.d.ts.map +1 -1
  65. package/dist/main.js +19 -17
  66. package/dist/main.js.map +1 -1
  67. package/dist/models/types.d.ts +16 -1
  68. package/dist/models/types.d.ts.map +1 -1
  69. package/dist/providers/MarkdownTaskBackend.d.ts +2 -1
  70. package/dist/providers/MarkdownTaskBackend.d.ts.map +1 -1
  71. package/dist/providers/MarkdownTaskBackend.js +28 -5
  72. package/dist/providers/MarkdownTaskBackend.js.map +1 -1
  73. package/dist/providers/registry.d.ts.map +1 -1
  74. package/dist/providers/registry.js +5 -7
  75. package/dist/providers/registry.js.map +1 -1
  76. package/package.json +1 -1
  77. package/project-template/.claude/hooks/start.sh +44 -0
  78. package/project-template/.claude/settings.json +1 -1
  79. package/skills/architecture-decision-records/SKILL.md +207 -0
  80. package/skills/backend/SKILL.md +62 -0
  81. package/skills/backend/references/api-design.md +168 -0
  82. package/skills/backend/references/caching.md +181 -0
  83. package/skills/backend/references/data-access.md +173 -0
  84. package/skills/backend/references/layering.md +181 -0
  85. package/skills/backend/references/observability.md +190 -0
  86. package/skills/backend/references/resilience.md +201 -0
  87. package/skills/backend/references/security.md +186 -0
  88. package/skills/backend-architect/SKILL.md +119 -0
  89. package/skills/code-reviewer/SKILL.md +143 -0
  90. package/skills/coding-standards/SKILL.md +60 -0
  91. package/skills/coding-standards/references/clean-code.md +258 -0
  92. package/skills/coding-standards/references/code-review.md +192 -0
  93. package/skills/coding-standards/references/commits-and-prs.md +226 -0
  94. package/skills/coding-standards/references/error-strategy.md +193 -0
  95. package/skills/coding-standards/references/naming.md +185 -0
  96. package/skills/coding-standards/references/tdd.md +171 -0
  97. package/skills/database/SKILL.md +53 -0
  98. package/skills/database/references/indexing.md +190 -0
  99. package/skills/database/references/migrations.md +199 -0
  100. package/skills/database/references/nosql.md +185 -0
  101. package/skills/database/references/queries.md +295 -0
  102. package/skills/database/references/scaling.md +203 -0
  103. package/skills/database/references/schema.md +191 -0
  104. package/skills/database-optimizer/SKILL.md +168 -0
  105. package/skills/debugging-workflow/SKILL.md +244 -0
  106. package/skills/devops/SKILL.md +55 -0
  107. package/skills/devops/references/ci-cd.md +204 -0
  108. package/skills/devops/references/containers.md +272 -0
  109. package/skills/devops/references/deploy.md +201 -0
  110. package/skills/devops/references/iac.md +252 -0
  111. package/skills/devops/references/observability.md +228 -0
  112. package/skills/devops/references/secrets.md +178 -0
  113. package/skills/devops-automator/SKILL.md +164 -0
  114. package/skills/frontend/SKILL.md +52 -0
  115. package/skills/frontend/references/accessibility.md +222 -0
  116. package/skills/frontend/references/components.md +206 -0
  117. package/skills/frontend/references/performance.md +219 -0
  118. package/skills/frontend/references/routing.md +209 -0
  119. package/skills/frontend/references/state.md +190 -0
  120. package/skills/frontend/references/testing.md +216 -0
  121. package/skills/frontend-developer/SKILL.md +115 -0
  122. package/skills/git-workflow/SKILL.md +355 -0
  123. package/skills/golang/SKILL.md +49 -0
  124. package/skills/golang/references/concurrency.md +284 -0
  125. package/skills/golang/references/errors.md +241 -0
  126. package/skills/golang/references/idioms.md +285 -0
  127. package/skills/golang/references/testing.md +238 -0
  128. package/skills/java/SKILL.md +50 -0
  129. package/skills/java/references/concurrency.md +194 -0
  130. package/skills/java/references/idioms.md +283 -0
  131. package/skills/java/references/testing.md +228 -0
  132. package/skills/kotlin/SKILL.md +47 -0
  133. package/skills/kotlin/references/coroutines.md +240 -0
  134. package/skills/kotlin/references/idioms.md +268 -0
  135. package/skills/kotlin/references/testing.md +219 -0
  136. package/skills/mobile/SKILL.md +50 -0
  137. package/skills/mobile/references/architecture.md +204 -0
  138. package/skills/mobile/references/navigation.md +158 -0
  139. package/skills/mobile/references/performance.md +152 -0
  140. package/skills/mobile/references/platform.md +166 -0
  141. package/skills/mobile/references/state-and-data.md +174 -0
  142. package/skills/python/SKILL.md +51 -0
  143. package/skills/python/THIRD_PARTY.md +14 -0
  144. package/skills/python/references/async.md +218 -0
  145. package/skills/python/references/error-handling.md +254 -0
  146. package/skills/python/references/idioms.md +279 -0
  147. package/skills/python/references/packaging.md +233 -0
  148. package/skills/python/references/testing.md +269 -0
  149. package/skills/python/references/typing.md +292 -0
  150. package/skills/qa-tester/SKILL.md +186 -0
  151. package/skills/rust/SKILL.md +50 -0
  152. package/skills/rust/references/async.md +224 -0
  153. package/skills/rust/references/errors.md +240 -0
  154. package/skills/rust/references/ownership.md +263 -0
  155. package/skills/rust/references/testing.md +274 -0
  156. package/skills/rust/references/traits.md +250 -0
  157. package/skills/security-engineer/SKILL.md +157 -0
  158. package/skills/swift/SKILL.md +48 -0
  159. package/skills/swift/references/concurrency.md +280 -0
  160. package/skills/swift/references/idioms.md +334 -0
  161. package/skills/swift/references/testing.md +229 -0
  162. package/skills/typescript/SKILL.md +51 -0
  163. package/skills/typescript/references/async.md +241 -0
  164. package/skills/typescript/references/errors.md +208 -0
  165. package/skills/typescript/references/idioms.md +246 -0
  166. package/skills/typescript/references/testing.md +225 -0
  167. package/skills/typescript/references/tooling.md +208 -0
  168. package/skills/typescript/references/types.md +259 -0
@@ -0,0 +1,263 @@
1
+ # Rust — Ownership
2
+
3
+ Borrowing, lifetimes, move vs. copy, smart pointers. The part the language is organized around.
4
+
5
+ ## The rules
6
+
7
+ 1. Each value has one owner.
8
+ 2. When the owner goes out of scope, the value is dropped.
9
+ 3. You can borrow immutably (any number of `&T`) OR mutably (exactly one `&mut T`), never both at once.
10
+
11
+ Everything else is consequences of these.
12
+
13
+ ## Move vs. copy
14
+
15
+ ```rust
16
+ let s = String::from("hi");
17
+ let t = s; // move: s is no longer valid
18
+ // println!("{s}") // ❌ error
19
+
20
+ let n = 5;
21
+ let m = n; // copy: n is still valid (i32 is Copy)
22
+ println!("{n} {m}"); // ✅
23
+ ```
24
+
25
+ `Copy` types (primitives, `&T`, arrays of `Copy`) are copied implicitly. Everything else is moved. Add `#[derive(Clone, Copy)]` to your own small value types.
26
+
27
+ ## Borrowing
28
+
29
+ Prefer borrows over ownership in parameters. The caller chooses.
30
+
31
+ ```rust
32
+ fn length(s: &str) -> usize { s.len() }
33
+
34
+ let owned = String::from("hi");
35
+ length(&owned); // caller keeps the String
36
+ length("hello"); // works with a &'static str too
37
+ ```
38
+
39
+ Rule: **accept `&str` / `&[T]` / `&T`, not `String` / `Vec<T>` / `T`**, unless you need ownership (e.g., storing it).
40
+
41
+ ## The borrow checker — common errors and fixes
42
+
43
+ ### "cannot borrow X as mutable because it is also borrowed as immutable"
44
+
45
+ ```rust
46
+ // ❌
47
+ let mut v = vec![1, 2, 3];
48
+ let first = &v[0]; // immutable borrow
49
+ v.push(4); // mutable borrow while &first alive
50
+ println!("{first}"); // error
51
+
52
+ // ✅ shorten the immutable borrow
53
+ let first = v[0]; // copy out (if Copy)
54
+ v.push(4);
55
+ println!("{first}");
56
+
57
+ // ✅ scope it
58
+ {
59
+ let first = &v[0];
60
+ println!("{first}");
61
+ } // borrow ends here
62
+ v.push(4);
63
+ ```
64
+
65
+ ### "use of moved value"
66
+
67
+ ```rust
68
+ // ❌
69
+ let s = String::from("hi");
70
+ takes(s);
71
+ println!("{s}"); // already moved
72
+
73
+ // ✅ pass by reference
74
+ takes(&s);
75
+
76
+ // ✅ clone if you really need two owners
77
+ takes(s.clone());
78
+ ```
79
+
80
+ ## Lifetimes — usually inferred
81
+
82
+ Rust figures out most lifetimes. You only annotate when the compiler can't:
83
+
84
+ ```rust
85
+ fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
86
+ if x.len() > y.len() { x } else { y }
87
+ }
88
+ ```
89
+
90
+ The `'a` says "the returned reference is valid for the shorter of x's and y's lifetimes".
91
+
92
+ In structs that hold references, you must annotate:
93
+
94
+ ```rust
95
+ struct Parser<'src> {
96
+ input: &'src str,
97
+ }
98
+ ```
99
+
100
+ Rule of thumb: if you're fighting lifetimes, you might want owned data instead of references. Sometimes a `String` is fine.
101
+
102
+ ## `'static` lifetime
103
+
104
+ `'static` means "lives for the whole program". Comes up with:
105
+ - String literals (`&'static str`)
106
+ - Bounded trait objects in async / threading (`Send + Sync + 'static`)
107
+ - Global `Mutex<T>` via `OnceLock` / `LazyLock`
108
+
109
+ A function bound `T: 'static` does NOT mean "must be owned forever" — it means "contains no non-static references". An owned `String` is `'static` because it borrows nothing.
110
+
111
+ ## Smart pointers
112
+
113
+ | Type | Use |
114
+ |---|---|
115
+ | `Box<T>` | Heap allocate; needed for recursive types, trait objects |
116
+ | `Rc<T>` | Shared ownership, single-threaded; refcount |
117
+ | `Arc<T>` | Shared ownership, thread-safe |
118
+ | `RefCell<T>` | Interior mutability, single-threaded, checked at runtime |
119
+ | `Mutex<T>` / `RwLock<T>` | Interior mutability, thread-safe |
120
+ | `Cow<'a, T>` | Borrow OR own; copy-on-write |
121
+
122
+ Rule: **start with `&T` / `&mut T`. Reach for smart pointers only when simple borrowing can't model what you need.**
123
+
124
+ ## Interior mutability
125
+
126
+ When a value is shared (`&T`) but a method needs to mutate it internally (caches, ref counts), use `RefCell` (single-threaded) or `Mutex` (threaded).
127
+
128
+ ```rust
129
+ use std::cell::RefCell;
130
+
131
+ struct Cache {
132
+ map: RefCell<HashMap<String, String>>,
133
+ }
134
+
135
+ impl Cache {
136
+ fn get(&self, k: &str) -> Option<String> {
137
+ self.map.borrow().get(k).cloned()
138
+ }
139
+ fn put(&self, k: String, v: String) {
140
+ self.map.borrow_mut().insert(k, v);
141
+ }
142
+ }
143
+ ```
144
+
145
+ `.borrow_mut()` while another `.borrow()` is live panics at runtime. It trades compile-time for runtime checks — a last resort.
146
+
147
+ ## `Rc<RefCell<T>>` — the single-threaded shared-mutable
148
+
149
+ Legal, common in tree / graph data structures, single-threaded only.
150
+
151
+ ```rust
152
+ use std::{cell::RefCell, rc::Rc};
153
+
154
+ type Node = Rc<RefCell<NodeData>>;
155
+ ```
156
+
157
+ If two threads may touch it → `Arc<Mutex<T>>`. If you find yourself writing `Rc<RefCell<...>>` often, reconsider — sometimes ownership flows would be cleaner with a different data layout (`Vec<Node>` + indexes).
158
+
159
+ ## `Cow` — borrow or own, at runtime
160
+
161
+ ```rust
162
+ use std::borrow::Cow;
163
+
164
+ fn normalize(s: &str) -> Cow<'_, str> {
165
+ if s.chars().any(|c| c.is_uppercase()) {
166
+ Cow::Owned(s.to_lowercase())
167
+ } else {
168
+ Cow::Borrowed(s)
169
+ }
170
+ }
171
+ ```
172
+
173
+ Great for "usually don't need to clone, sometimes do" paths. Callers get a `&str` from `.as_ref()`.
174
+
175
+ ## `Drop` — RAII
176
+
177
+ Values implement `Drop` to release resources (file handles, mutex guards, DB connections).
178
+
179
+ ```rust
180
+ impl Drop for TempFile {
181
+ fn drop(&mut self) {
182
+ std::fs::remove_file(&self.path).ok();
183
+ }
184
+ }
185
+ ```
186
+
187
+ Runs when the value goes out of scope. Don't `mem::forget` something with a `Drop` unless you really know.
188
+
189
+ Guards (`MutexGuard`, `RefMut`) are `Drop`-based — they release the lock when dropped. Keep them scoped tightly:
190
+
191
+ ```rust
192
+ {
193
+ let mut g = mu.lock().unwrap();
194
+ g.push(x);
195
+ } // lock released HERE
196
+ call_may_block(); // no lock held
197
+ ```
198
+
199
+ ## `unsafe` — the escape hatch
200
+
201
+ Only where the borrow checker can't prove safety (FFI, raw pointers, some `Send`/`Sync` work). Every `unsafe` block needs a `// SAFETY:` comment stating the invariants you've manually verified.
202
+
203
+ ```rust
204
+ // SAFETY: `ptr` is not null (checked above) and `len` is within the allocation.
205
+ let slice = unsafe { std::slice::from_raw_parts(ptr, len) };
206
+ ```
207
+
208
+ If you can do it safely, do it safely. `unsafe` is not a performance knob.
209
+
210
+ ## Common patterns
211
+
212
+ ### Newtype for domain meaning
213
+
214
+ ```rust
215
+ pub struct UserId(pub String);
216
+ pub struct Email(pub String);
217
+
218
+ fn find_user(id: UserId) -> Option<User> { ... }
219
+
220
+ // fn call takes UserId, not String → hard to mix with other string IDs
221
+ ```
222
+
223
+ ### Builder
224
+
225
+ ```rust
226
+ let req = Request::builder()
227
+ .url("https://x.com")
228
+ .timeout(Duration::from_secs(5))
229
+ .build()?;
230
+ ```
231
+
232
+ For types with many optional fields or construction invariants.
233
+
234
+ ### Typestate — encode state in the type
235
+
236
+ ```rust
237
+ struct Draft;
238
+ struct Published;
239
+
240
+ struct Post<State> { body: String, _s: PhantomData<State> }
241
+
242
+ impl Post<Draft> {
243
+ fn publish(self) -> Post<Published> { ... }
244
+ }
245
+
246
+ impl Post<Published> {
247
+ fn url(&self) -> String { ... }
248
+ // no publish() here — you can't double-publish
249
+ }
250
+ ```
251
+
252
+ ## Anti-patterns
253
+
254
+ | Anti-pattern | Fix |
255
+ |---|---|
256
+ | `.clone()` reflex to silence the borrow checker | Understand what ownership you need; clones should be deliberate |
257
+ | `Rc<RefCell<T>>` everywhere | Revisit data layout; many problems want a `Vec<T>` with indexes |
258
+ | Accepting `String` / `Vec<T>` when `&str` / `&[T]` works | Over-constrains callers |
259
+ | Returning references from local functions ("dangling reference") | Return owned data, or rework the lifetime story |
260
+ | `mut` everywhere | Minimize mutation; Rust rewards immutable-by-default |
261
+ | Implementing `Clone` by default | Only for types callers will reasonably clone |
262
+ | Allocating in a hot loop (`format!`, `vec![...]`) | Preallocate; `write!` into a reused buffer |
263
+ | Leaking a `MutexGuard` into an async `.await` | Guard is not `Send`; use `tokio::sync::Mutex` or drop the guard before awaiting |
@@ -0,0 +1,274 @@
1
+ # Rust — Testing
2
+
3
+ `#[test]`, integration tests, property testing, snapshots. For general TDD, see `coding-standards/references/tdd.md`.
4
+
5
+ ## Unit tests — alongside the code
6
+
7
+ ```rust
8
+ // src/math.rs
9
+ pub fn add(a: i32, b: i32) -> i32 { a + b }
10
+
11
+ #[cfg(test)]
12
+ mod tests {
13
+ use super::*;
14
+
15
+ #[test]
16
+ fn adds_positives() {
17
+ assert_eq!(add(2, 3), 5);
18
+ }
19
+
20
+ #[test]
21
+ #[should_panic(expected = "overflow")]
22
+ fn panics_on_overflow() {
23
+ add(i32::MAX, 1);
24
+ }
25
+ }
26
+ ```
27
+
28
+ Run with `cargo test`. The `#[cfg(test)]` block is compiled only for tests — the test code doesn't ship in release builds.
29
+
30
+ ## Integration tests — `tests/` directory
31
+
32
+ ```
33
+ mycrate/
34
+ ├── src/
35
+ │ └── lib.rs
36
+ └── tests/
37
+ └── api.rs # one file = one crate, black-box tests
38
+ ```
39
+
40
+ ```rust
41
+ // tests/api.rs
42
+ use mycrate::load;
43
+
44
+ #[test]
45
+ fn loads_from_file() {
46
+ let cfg = load("tests/fixtures/sample.yaml").unwrap();
47
+ assert_eq!(cfg.name, "sample");
48
+ }
49
+ ```
50
+
51
+ Integration tests use only the public API. If you need internal access, that's a unit test.
52
+
53
+ ## Assertions
54
+
55
+ ```rust
56
+ assert!(cond);
57
+ assert_eq!(a, b);
58
+ assert_ne!(a, b);
59
+
60
+ // With custom message
61
+ assert_eq!(got, want, "parsed value mismatch for input {input:?}");
62
+ ```
63
+
64
+ Failures show a nice diff. Stick with `assert_eq!` over manual `if got != want { panic! }`.
65
+
66
+ ## Parameterized tests — table style
67
+
68
+ ```rust
69
+ #[test]
70
+ fn add_cases() {
71
+ for (a, b, want) in [(1, 2, 3), (0, 0, 0), (-1, 1, 0)] {
72
+ assert_eq!(add(a, b), want, "add({a}, {b})");
73
+ }
74
+ }
75
+ ```
76
+
77
+ Or use `rstest` for per-case test names:
78
+
79
+ ```rust
80
+ use rstest::rstest;
81
+
82
+ #[rstest]
83
+ #[case(1, 2, 3)]
84
+ #[case(0, 0, 0)]
85
+ #[case(-1, 1, 0)]
86
+ fn add(#[case] a: i32, #[case] b: i32, #[case] want: i32) {
87
+ assert_eq!(crate::add(a, b), want);
88
+ }
89
+ ```
90
+
91
+ `cargo test add::case_1` runs one case.
92
+
93
+ ## Async tests
94
+
95
+ With tokio:
96
+
97
+ ```rust
98
+ #[tokio::test]
99
+ async fn fetches() {
100
+ let body = fetch("http://...").await.unwrap();
101
+ assert!(body.contains("hi"));
102
+ }
103
+
104
+ #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
105
+ async fn multi_thread() { ... }
106
+ ```
107
+
108
+ Other runtimes have their own macros (`#[async_std::test]`).
109
+
110
+ ## Property tests — `proptest` / `quickcheck`
111
+
112
+ Generates random inputs; shrinks failing cases to a minimal counter-example.
113
+
114
+ ```rust
115
+ use proptest::prelude::*;
116
+
117
+ proptest! {
118
+ #[test]
119
+ fn sort_is_idempotent(v in any::<Vec<i32>>()) {
120
+ let mut s = v.clone();
121
+ s.sort();
122
+ let mut s2 = s.clone();
123
+ s2.sort();
124
+ prop_assert_eq!(s, s2);
125
+ }
126
+
127
+ #[test]
128
+ fn roundtrip(s in "\\PC*") { // arbitrary printable UTF-8
129
+ let encoded = encode(&s);
130
+ prop_assert_eq!(decode(&encoded), s);
131
+ }
132
+ }
133
+ ```
134
+
135
+ Use for: parsers, serializers, invariant checks, anything with a large input space.
136
+
137
+ ## Snapshot tests — `insta`
138
+
139
+ ```rust
140
+ #[test]
141
+ fn render() {
142
+ insta::assert_snapshot!(render(&input));
143
+ insta::assert_yaml_snapshot!(serialize(&input));
144
+ }
145
+ ```
146
+
147
+ Run `cargo insta review` to approve / reject. Commit the snapshots under `src/snapshots/`. Review every diff deliberately.
148
+
149
+ Great for CLI output, parser ASTs, generated config. Weak for flaky fields (timestamps, UUIDs) — redact or inject deterministic values.
150
+
151
+ ## Test doubles
152
+
153
+ Fakes > mocks in Rust too.
154
+
155
+ ```rust
156
+ // Production
157
+ pub trait UserRepo {
158
+ fn find(&self, id: &str) -> Option<User>;
159
+ }
160
+
161
+ // In tests
162
+ struct FakeRepo { users: HashMap<String, User> }
163
+
164
+ impl UserRepo for FakeRepo {
165
+ fn find(&self, id: &str) -> Option<User> {
166
+ self.users.get(id).cloned()
167
+ }
168
+ }
169
+ ```
170
+
171
+ For mocks, `mockall`:
172
+
173
+ ```rust
174
+ use mockall::automock;
175
+
176
+ #[automock]
177
+ trait UserRepo {
178
+ fn find(&self, id: &str) -> Option<User>;
179
+ }
180
+
181
+ let mut mock = MockUserRepo::new();
182
+ mock.expect_find()
183
+ .with(eq("u1"))
184
+ .returning(|_| Some(User::sample()));
185
+ ```
186
+
187
+ Reach for `mockall` when the trait has complex call expectations. Otherwise, fakes are clearer.
188
+
189
+ ## Test filtering
190
+
191
+ ```bash
192
+ cargo test # all tests
193
+ cargo test math # name contains 'math'
194
+ cargo test --lib # unit tests only
195
+ cargo test --test api # one integration file
196
+ cargo test -- --nocapture # show println! output
197
+ cargo test -- --test-threads=1 # serial execution (rare)
198
+ ```
199
+
200
+ Add `--quiet` for cleaner CI output.
201
+
202
+ ## Golden files — `testdata/` + `env var`
203
+
204
+ ```rust
205
+ const UPDATE: bool = option_env!("UPDATE_GOLDEN").is_some();
206
+
207
+ #[test]
208
+ fn render_matches_golden() {
209
+ let got = render(sample());
210
+ let path = "testdata/rendered.txt";
211
+ if UPDATE {
212
+ std::fs::write(path, &got).unwrap();
213
+ }
214
+ let want = std::fs::read_to_string(path).unwrap();
215
+ assert_eq!(got, want);
216
+ }
217
+ ```
218
+
219
+ Run `UPDATE_GOLDEN=1 cargo test` to regenerate. `insta` does this better in most cases.
220
+
221
+ ## Doc tests — executable examples in docs
222
+
223
+ ```rust
224
+ /// Adds two numbers.
225
+ ///
226
+ /// ```
227
+ /// use mycrate::add;
228
+ /// assert_eq!(add(2, 3), 5);
229
+ /// ```
230
+ pub fn add(a: i32, b: i32) -> i32 { a + b }
231
+ ```
232
+
233
+ Run with `cargo test --doc`. Keeps public examples honest — if you change the API, the doc test breaks.
234
+
235
+ ## Benchmarks
236
+
237
+ Stable Rust: `cargo bench` with `criterion`.
238
+
239
+ ```rust
240
+ // benches/my_bench.rs
241
+ use criterion::{criterion_group, criterion_main, Criterion};
242
+
243
+ fn bench_parse(c: &mut Criterion) {
244
+ let input = "...";
245
+ c.bench_function("parse", |b| b.iter(|| parse(input)));
246
+ }
247
+
248
+ criterion_group!(benches, bench_parse);
249
+ criterion_main!(benches);
250
+ ```
251
+
252
+ Criterion runs statistical comparisons against previous runs, flags regressions.
253
+
254
+ ## `cargo test` + `cargo llvm-cov` for coverage
255
+
256
+ ```
257
+ cargo install cargo-llvm-cov
258
+ cargo llvm-cov --html
259
+ ```
260
+
261
+ See `coding-standards/references/tdd.md` for target numbers.
262
+
263
+ ## Anti-patterns
264
+
265
+ | Anti-pattern | Fix |
266
+ |---|---|
267
+ | `unwrap()` in non-test code copied into tests | Use `?` in `fn test() -> Result<(), Err>` if you want |
268
+ | `std::thread::sleep` in async tests | `tokio::time::sleep` (or pause time for determinism) |
269
+ | Tests that depend on execution order | Each test is independent; don't share globals |
270
+ | Hitting real external services in unit tests | Use fakes / `wiremock` / `httpmock` |
271
+ | Comparing `Debug` strings for equality | Use `assert_eq!` on typed values |
272
+ | Snapshot of time-dependent output without redaction | Inject a clock |
273
+ | Ignoring `#[should_panic]` without `expected = "..."` | Any panic passes; you want the specific one |
274
+ | Test file names that don't match the module under test | `foo.rs` → `foo/tests.rs` or `tests/foo.rs` |