@polderlabs/bizar 2.3.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 (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +364 -0
  3. package/cli/audit.mjs +144 -0
  4. package/cli/banner.mjs +41 -0
  5. package/cli/bin.mjs +186 -0
  6. package/cli/copy.mjs +508 -0
  7. package/cli/export.mjs +87 -0
  8. package/cli/init.mjs +147 -0
  9. package/cli/install.mjs +390 -0
  10. package/cli/plan-templates.mjs +523 -0
  11. package/cli/plan.mjs +2087 -0
  12. package/cli/prompts.mjs +163 -0
  13. package/cli/update.mjs +273 -0
  14. package/cli/utils.mjs +153 -0
  15. package/config/AGENTS.md +282 -0
  16. package/config/agents/baldr.md +148 -0
  17. package/config/agents/forseti.md +112 -0
  18. package/config/agents/frigg.md +101 -0
  19. package/config/agents/heimdall.md +157 -0
  20. package/config/agents/hermod.md +144 -0
  21. package/config/agents/mimir.md +115 -0
  22. package/config/agents/odin.md +309 -0
  23. package/config/agents/quick.md +78 -0
  24. package/config/agents/semble-search.md +44 -0
  25. package/config/agents/thor.md +97 -0
  26. package/config/agents/tyr.md +96 -0
  27. package/config/agents/vidarr.md +100 -0
  28. package/config/agents/vor.md +140 -0
  29. package/config/commands/audit.md +1 -0
  30. package/config/commands/explain.md +1 -0
  31. package/config/commands/init.md +1 -0
  32. package/config/commands/learn.md +1 -0
  33. package/config/commands/pr-review.md +1 -0
  34. package/config/commands/tailscale-serve.md +96 -0
  35. package/config/hooks/README.md +29 -0
  36. package/config/hooks/post-tool-use.md +16 -0
  37. package/config/hooks/pre-tool-use.md +16 -0
  38. package/config/opencode.json +52 -0
  39. package/config/opencode.json.template +52 -0
  40. package/config/rules/general.md +8 -0
  41. package/config/rules/git.md +11 -0
  42. package/config/rules/javascript.md +10 -0
  43. package/config/rules/python.md +10 -0
  44. package/config/rules/testing.md +10 -0
  45. package/config/skills/bizar/README.md +9 -0
  46. package/config/skills/bizar/SKILL.md +187 -0
  47. package/config/skills/cpp-coding-standards/README.md +28 -0
  48. package/config/skills/cpp-coding-standards/SKILL.md +634 -0
  49. package/config/skills/cpp-coding-standards/agents/openai.yaml +4 -0
  50. package/config/skills/cpp-coding-standards/references/concurrency.md +320 -0
  51. package/config/skills/cpp-coding-standards/references/error-handling.md +229 -0
  52. package/config/skills/cpp-coding-standards/references/memory-safety.md +216 -0
  53. package/config/skills/cpp-coding-standards/references/modern-idioms.md +282 -0
  54. package/config/skills/cpp-coding-standards/references/review-checklist.md +96 -0
  55. package/config/skills/cpp-testing/README.md +28 -0
  56. package/config/skills/cpp-testing/SKILL.md +304 -0
  57. package/config/skills/cpp-testing/agents/openai.yaml +4 -0
  58. package/config/skills/cpp-testing/references/coverage.md +370 -0
  59. package/config/skills/cpp-testing/references/framework-compare.md +175 -0
  60. package/config/skills/cpp-testing/references/host-test-for-embedded.md +499 -0
  61. package/config/skills/cpp-testing/references/mocking.md +364 -0
  62. package/config/skills/cpp-testing/references/tdd-workflow.md +308 -0
  63. package/config/skills/embedded-esp-idf/README.md +41 -0
  64. package/config/skills/embedded-esp-idf/SKILL.md +439 -0
  65. package/config/skills/embedded-esp-idf/agents/openai.yaml +4 -0
  66. package/config/skills/embedded-esp-idf/references/freertos-patterns.md +214 -0
  67. package/config/skills/embedded-esp-idf/references/host-tests.md +164 -0
  68. package/config/skills/embedded-esp-idf/references/idf-py-commands.md +157 -0
  69. package/config/skills/embedded-esp-idf/references/kconfig.md +159 -0
  70. package/config/skills/embedded-esp-idf/references/logging-discipline.md +118 -0
  71. package/config/skills/embedded-esp-idf/references/memory-and-iram.md +137 -0
  72. package/config/skills/embedded-esp-idf/references/nvs.md +121 -0
  73. package/config/skills/embedded-esp-idf/references/packed-structs.md +192 -0
  74. package/config/skills/embedded-esp-idf/scripts/idf_env.sh +47 -0
  75. package/config/skills/embedded-esp-idf/scripts/size_check.sh +77 -0
  76. package/config/skills/self-improvement/SKILL.md +64 -0
  77. package/package.json +47 -0
  78. package/templates/plan/htmx.min.js +1 -0
  79. package/templates/plan/library/bug-investigation.mdx +79 -0
  80. package/templates/plan/library/decision-record.mdx +71 -0
  81. package/templates/plan/library/feature-design.mdx +92 -0
  82. package/templates/plan/meta.json.template +8 -0
  83. package/templates/plan/plan.canvas.template +1711 -0
  84. package/templates/plan/plan.html.template +937 -0
  85. package/templates/plan/plan.mdx.template +46 -0
@@ -0,0 +1,304 @@
1
+ ---
2
+ name: cpp-testing
3
+ description: Use this skill when writing or improving C++ tests, choosing a test framework, designing host-side tests for embedded firmware, or running a test-gate / TDD workflow. Triggers on tasks involving GoogleTest, Catch2, doctest, mocking, coverage, or host-test binaries for firmware modules.
4
+ ---
5
+
6
+ # C++ Testing Skill
7
+
8
+ ## Overview
9
+
10
+ This skill provides guidance for writing effective C++ tests across the full testing pyramid: unit, integration, host-side, and on-device. It covers framework selection, test layering, host-test patterns for embedded firmware, TDD workflow, coverage targets, mocking strategies, and common anti-patterns.
11
+
12
+ Key use cases:
13
+ - Writing new tests for a C++ module
14
+ - Choosing between GoogleTest, Catch2, and doctest
15
+ - Setting up a host-side test binary that links firmware C++ code without ESP-IDF or FreeRTOS
16
+ - Running a test-gate to verify 80% coverage before merging
17
+ - Applying TDD to firmware policy/payload modules
18
+
19
+ ## Test Framework Selection
20
+
21
+ | Framework | Best For | Strengths | Weaknesses |
22
+ |-----------|----------|-----------|------------|
23
+ | **GoogleTest** | Large projects, parametric tests, fixtures | Rich feature set, wide adoption, `TEST_P`, `TEST_F`, death tests | Verbose, heavy |
24
+ | **Catch2** | Quick to write, expressive | Single-header, BDD-style, very low boilerplate | Slower compile, less `gtest` ecosystem |
25
+ | **doctest** | Header-only, compile-time speed | Fast compile, lightweight, C++17 | Less ecosystem, newer |
26
+
27
+ **Recommendation:** Default to GoogleTest for embedded/firmware projects — its `TEST_P`/`TEST_F` and death tests are well-suited to policy modules. Use Catch2 when you want minimal friction for small utilities. Use doctest when compile speed is critical.
28
+
29
+ See `references/framework-compare.md` for code samples.
30
+
31
+ ## Test Layering
32
+
33
+ ```
34
+ ┌─────────────────────────────────────┐
35
+ │ Host-Side Integration Tests │ ← Binary links firmware .cpp, tests policy/payload
36
+ ├─────────────────────────────────────┤
37
+ │ On-Device Integration Tests │ ← Runs on target; tests HW / interrupts
38
+ ├─────────────────────────────────────┤
39
+ │ Unit Tests (isolated) │ ← Pure C++, mocked deps, fast
40
+ └─────────────────────────────────────┘
41
+ ```
42
+
43
+ - **Unit tests:** Test a single class/function in isolation. Link a `.cpp` directly into the test binary with mocked dependencies.
44
+ - **Host-side integration tests:** Compile a `test_*.cpp` binary that links one or more firmware modules from `main/` or `components/`. No ESP-IDF, no FreeRTOS. Use for policy, payload, and datacollector modules.
45
+ - **On-device tests:** Run on the actual hardware. Require `idf.py test` and device flashing.
46
+
47
+ See `references/host-test-for-embedded.md` for the CMake setup pattern.
48
+
49
+ ## Host-Side Test Pattern for Embedded Firmware
50
+
51
+ The goal: build a standalone Linux binary that links firmware C++ code and runs assertions — no ESP-IDF, no FreeRTOS.
52
+
53
+ **CMake pattern (minimal):**
54
+
55
+ ```cmake
56
+ # test/host/CMakeLists.txt
57
+ cmake_minimum_required(VERSION 3.16)
58
+ project(host_feature_flags_test)
59
+
60
+ add_executable(test_feature_flags
61
+ ${CMAKE_CURRENT_SOURCE_DIR}/test_feature_flags.cpp
62
+ ${FIRMWARE_ROOT}/main/policy/feature_flags.cpp
63
+ )
64
+ target_compile_features(test_feature_flags PRIVATE cxx_std_17)
65
+ target_link_libraries(test_feature_flags PRIVATE gtest gmock pthread)
66
+ ```
67
+
68
+ **Test source pattern:**
69
+
70
+ ```cpp
71
+ // test/host/test_feature_flags.cpp
72
+ #include <gtest/gtest.h>
73
+ #include "feature_flags.h" // firmware header — no ESP-IDF, no FreeRTOS
74
+
75
+ // Mock any firmware-freeRTOS deps via abstract interface or std::function injection
76
+ namespace mock {
77
+ std::function<bool(const char*)> is_active_override;
78
+ }
79
+
80
+ bool is_feature_active(const char* name) {
81
+ if (mock::is_active_override) return mock::is_active_override(name);
82
+ return feature_flags_impl(name); // call real impl
83
+ }
84
+
85
+ TEST(FeatureFlags, returns_true_for_known_feature) {
86
+ mock::is_active_override = [](const char* n) { return strcmp(n, "debug") == 0; };
87
+ EXPECT_TRUE(is_feature_active("debug"));
88
+ }
89
+ ```
90
+
91
+ See `references/host-test-for-embedded.md` for full examples testing `feature_flags.cpp`, `resp_fallback.cpp`, and `datacollector.cpp`.
92
+
93
+ ## TDD Workflow for C++
94
+
95
+ 1. **Red** — Write a failing test first. Compile and run; confirm it fails for the right reason.
96
+ 2. **Green** — Write the minimum production code to make the test pass. No optimization yet.
97
+ 3. **Refactor** — Clean up production and test code. Re-run tests to confirm they still pass.
98
+ 4. **Repeat** — Add the next test case.
99
+
100
+ **Tips for C++ TDD:**
101
+ - Keep tests compilable at all times (even if currently failing).
102
+ - Use `ASSERT_*` when continued execution after failure is meaningless.
103
+ - Write one logical assertion per test (multiple `EXPECT_*` is fine).
104
+ - Name tests `Subject_Under_Test_Behavior_Expected`. Example: `RespFallback_returns_original_when_fallback_unavailable`.
105
+
106
+ See `references/tdd-workflow.md` for a step-by-step walkthrough.
107
+
108
+ ## Coverage
109
+
110
+ **80% line coverage** is the default gate. For a test-gate to pass:
111
+ - At least 80% of all `.cpp` files in `main/` and `components/` must be covered.
112
+ - **Exclude:** mock files (`*_mock.cpp`, `*_mock.h`), generated code, third-party sources.
113
+ - Coverage is measured with `gcov` / `lcov`. Run: `cmake --build build -- -j && lcov --capture --directory . --output coverage.info --exclude '*/mocks/*' --exclude '*/build/*'`
114
+
115
+ **What to cover:**
116
+ - All public class methods (including error paths)
117
+ - All `if/else` and `switch` branches
118
+ - Edge cases: empty input, null pointers, boundary values, error returns
119
+ - Negative tests: verify the code fails gracefully for bad inputs
120
+
121
+ See `references/coverage.md` for `gcov`/`lcov` setup and exclusion patterns.
122
+
123
+ ## Mocking Strategies
124
+
125
+ ### 1. Abstract Interface (Dependency Inversion)
126
+
127
+ Define a pure virtual interface and inject a test double:
128
+
129
+ ```cpp
130
+ // interfaces.h
131
+ struct IFeatureStorage {
132
+ virtual bool read(const char* key, std::string& out) = 0;
133
+ virtual ~IFeatureStorage() = default;
134
+ };
135
+
136
+ // production code uses IFeatureStorage*
137
+ class FeatureFlags {
138
+ public:
139
+ explicit FeatureFlags(IFeatureStorage* storage) : storage_(storage) {}
140
+ bool is_active(const char* name);
141
+ private:
142
+ IFeatureStorage* storage_;
143
+ };
144
+
145
+ // test double
146
+ struct FakeStorage : IFeatureStorage {
147
+ bool read(const char* key, std::string& out) override {
148
+ if (strcmp(key, "debug") == 0) { out = "true"; return true; }
149
+ return false;
150
+ }
151
+ };
152
+
153
+ TEST(FeatureFlags, uses_injected_storage) {
154
+ FakeStorage fake;
155
+ FeatureFlags ff(&fake);
156
+ EXPECT_TRUE(ff.is_active("debug"));
157
+ }
158
+ ```
159
+
160
+ ### 2. Link-Time Seam
161
+
162
+ Compile the module under test with a fake implementation at link time. Replace the real `*.cpp` with a test stub. Useful when you cannot modify the source or inject dependencies:
163
+
164
+ ```cmake
165
+ # Link test stub instead of production implementation
166
+ target_sources(test_feature_flags PRIVATE fake_feature_flags.cpp)
167
+ ```
168
+
169
+ ### 3. `std::function` Injection
170
+
171
+ Pass behavior as `std::function` for runtime flexibility:
172
+
173
+ ```cpp
174
+ class RespFallback {
175
+ public:
176
+ using response_check_t = std::function<bool(const Response&)>;
177
+ explicit RespFallback(response_check_t checker) : checker_(std::move(checker)) {}
178
+ // ...
179
+ };
180
+ ```
181
+
182
+ This is the lightest option — no interface hierarchy needed.
183
+
184
+ See `references/mocking.md` for detailed patterns and tradeoffs.
185
+
186
+ ## Fixtures and TEST_F / TEST_P
187
+
188
+ ### TEST_F (Fixture-based)
189
+
190
+ Use `TEST_F` when multiple tests share setup/teardown logic:
191
+
192
+ ```cpp
193
+ class DatacollectorTest : public ::testing::Test {
194
+ protected:
195
+ void SetUp() override { collector_ = std::make_unique<Datacollector>(); }
196
+ void TearDown() override { collector_.reset(); }
197
+ std::unique_ptr<Datacollector> collector_;
198
+ };
199
+
200
+ TEST_F(DatacollectorTest, appends_reading) {
201
+ collector_->append(42.0);
202
+ EXPECT_DOUBLE_EQ(collector_->latest(), 42.0);
203
+ }
204
+ ```
205
+
206
+ ### TEST_P (Parametric)
207
+
208
+ Use `TEST_P` when the same test logic applies to multiple parameter sets:
209
+
210
+ ```cpp
211
+ class RespFallbackParamTest : public ::testing::TestWithParam<const char*> {};
212
+
213
+ TEST_P(RespFallbackParamTest, handles_named_response) {
214
+ const char* name = GetParam();
215
+ RespFallback rf;
216
+ EXPECT_NO_THROW(rf.process(name));
217
+ }
218
+
219
+ INSTANTIATE_TEST_SUITE_P(NamedResponses, RespFallbackParamTest,
220
+ ::testing::Values("keepalive", "chunked", "gzip"));
221
+ ```
222
+
223
+ ## Assertion Choice: EXPECT_* vs ASSERT_*
224
+
225
+ | Macro | Behavior on failure | Use when... |
226
+ |-------|---------------------|-------------|
227
+ | `EXPECT_*` | Continues test execution | Primary assertions; multiple related checks |
228
+ | `ASSERT_*` | Aborts immediately | Precondition failure; continuing is meaningless |
229
+
230
+ **Rule:** Default to `EXPECT_*`. Use `ASSERT_*` when failure would make subsequent assertions invalid (e.g., null pointer dereference, file open failure).
231
+
232
+ ## Test Naming Convention
233
+
234
+ Follow the pattern: `Subject_Under_Test_Behavior_Expected`
235
+
236
+ ```
237
+ RespFallback_returns_original_when_fallback_unavailable
238
+ FeatureFlags_is_active_returns_true_for_enabled_feature
239
+ Datacollector_latest_throws_when_collection_empty
240
+ ```
241
+
242
+ Avoid: `test1`, `TestFeatureFlags`, `test_is_active` — they don't communicate intent.
243
+
244
+ ## Common Anti-Patterns
245
+
246
+ 1. **Testing private methods** — Test the public interface only. Private method changes break tests unnecessarily.
247
+ 2. **Overspecifying** — Don't assert on exact ordering unless that's the contract. Prefer behavioral assertions.
248
+ 3. **Missing negative tests** — Always test the error/edge path, not just the happy path.
249
+ 4. **Global state leakage** — Each test must be independent. Reset shared state in `SetUp()`.
250
+ 5. **Mocking too much** — If you're mocking everything, you're not testing the real code. Prefer integration tests for module interactions.
251
+ 6. **Flaky tests** — Avoid timing, threading races, and filesystem dependencies in unit tests.
252
+
253
+ ## Do / Don't
254
+
255
+ ### Do
256
+
257
+ ```cpp
258
+ // DO: Use descriptive test names
259
+ TEST(FeatureFlags, is_active_returns_false_for_unknown_feature) {
260
+ EXPECT_FALSE(flags.is_active("nonexistent"));
261
+ }
262
+
263
+ // DO: Use ASSERT_* for preconditions
264
+ TEST(Datacollector, latest_throws_when_empty) {
265
+ ASSERT_FALSE(collector->has_readings()); // guard before calling latest()
266
+ EXPECT_THROW(collector->latest(), std::runtime_error);
267
+ }
268
+
269
+ // DO: Test through the public interface
270
+ TEST(RespFallback, returns_original_when_fallback_missing) {
271
+ RespFallback rf;
272
+ Response out = rf.process(Response{"original"});
273
+ EXPECT_EQ(out.payload, "original");
274
+ }
275
+ ```
276
+
277
+ ### Don't
278
+
279
+ ```cpp
280
+ // DON'T: Test private methods — test the public API instead
281
+ TEST(FeatureFlags, DISABLED_test_internal_state) { /* ... */ }
282
+
283
+ // DON'T: Overspecify — don't assert on internal state
284
+ TEST(FeatureFlags, is_active_caches_result) { // fragile, tests implementation
285
+ flags.is_active("debug");
286
+ EXPECT_EQ(flags.cache_size(), 1); // too specific
287
+ }
288
+
289
+ // DON'T: Forget negative tests
290
+ TEST(FeatureFlags, is_active_handles_nullptr) {
291
+ // Always test error paths
292
+ EXPECT_FALSE(flags.is_active(nullptr));
293
+ }
294
+ ```
295
+
296
+ ## References
297
+
298
+ Detailed guides on specific topics:
299
+
300
+ - **`references/framework-compare.md`** — GoogleTest vs Catch2 vs doctest with code samples
301
+ - **`references/host-test-for-embedded.md`** — CMake host-test setup, testing policy modules without ESP-IDF
302
+ - **`references/mocking.md`** — Abstract interfaces, link-time seams, `std::function` injection
303
+ - **`references/tdd-workflow.md`** — Red-green-refactor walkthrough in C++
304
+ - **`references/coverage.md`** — gcov/lcov setup, 80% gate, what to exclude
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "C++ Testing"
3
+ short_description: "C++ unit, integration, and host test patterns"
4
+ default_prompt: "Use $cpp-testing to design or improve C++ test coverage, host tests, and TDD workflows."
@@ -0,0 +1,370 @@
1
+ # Coverage for C++ Tests
2
+
3
+ ## Overview
4
+
5
+ Coverage measures which lines of production code are executed by your tests. The 80% line coverage gate means at least 80% of non-excluded lines must be covered before merging. This reference covers measurement tools, interpretation, exclusion patterns, and enforcement.
6
+
7
+ ---
8
+
9
+ ## Tools
10
+
11
+ | Tool | Description | Notes |
12
+ |------|-------------|-------|
13
+ | **gcov** | GCC's coverage tool — comes with `gcc` | Works with any CMake project compiled with `-fprofile-arcs -ftest-coverage` |
14
+ | **lcov** | Front-end for gcov — generates HTML/JSON reports | `apt install lcov` |
15
+ | **gcovr** | Alternative — generates Cobertura XML for CI | `pip install gcovr` |
16
+
17
+ ---
18
+
19
+ ## Setup
20
+
21
+ ### CMake Integration
22
+
23
+ ```cmake
24
+ # test/host/CMakeLists.txt
25
+ option(COVERAGE "Enable coverage reporting" OFF)
26
+
27
+ if(COVERAGE)
28
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fprofile-arcs -ftest-coverage")
29
+ set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fprofile-arcs -ftest-coverage")
30
+ endif()
31
+
32
+ # link gtest normally
33
+ target_link_libraries(test_policy PRIVATE gtest gmock pthread)
34
+
35
+ # on Linux, link gcov explicitly
36
+ if(COVERAGE)
37
+ target_link_libraries(test_policy PRIVATE gcov)
38
+ endif()
39
+ ```
40
+
41
+ ### Build and Capture
42
+
43
+ ```bash
44
+ cd test/host && mkdir -p build && cd build
45
+ cmake .. -DCOVERAGE=ON -DCMAKE_BUILD_TYPE=Debug
46
+ cmake --build . -j$(nproc)
47
+ ./test_policy # run tests — gcov writes .gcda/.gcno files
48
+ lcov --capture --directory . --output coverage.info \
49
+ --exclude '*/test_*' \
50
+ --exclude '*/stubs/*' \
51
+ --exclude '*/build/*' \
52
+ --exclude '*/googletest/*' \
53
+ --exclude '*/.cache/*'
54
+ ```
55
+
56
+ ### View HTML Report
57
+
58
+ ```bash
59
+ genhtml coverage.info --output-directory coverage_html
60
+ # Open coverage_html/index.html in browser
61
+ ```
62
+
63
+ ### Check Only Source Files (not test code)
64
+
65
+ ```bash
66
+ # Filter to only main/ and components/ sources
67
+ lcov --extract coverage.info '*/main/*' --output filtered.info
68
+ lcov --list filtered.info
69
+ ```
70
+
71
+ ---
72
+
73
+ ## What 80% Coverage Means
74
+
75
+ | Metric | Description | Target |
76
+ |--------|-------------|--------|
77
+ | **Line coverage** | Percentage of source lines executed | ≥ 80% |
78
+ | **Branch coverage** | Percentage of branches (if/else, switch) taken | Not explicitly gated, but inspect for untested branches |
79
+ | **Function coverage** | Percentage of functions called | Should be 100% for public APIs |
80
+
81
+ **80% line coverage does NOT mean:**
82
+ - Every function is tested
83
+ - Every branch is exercised
84
+ - The code is bug-free
85
+
86
+ It means 80% of **executable lines** were executed at least once. You still need:
87
+ - Branch coverage for `if/else` and `switch`
88
+ - Negative tests for error paths
89
+ - Boundary value tests
90
+
91
+ ---
92
+
93
+ ## Files to Exclude
94
+
95
+ Exclude these from coverage reports:
96
+
97
+ ```
98
+ */test_* — test source files (test_*.cpp)
99
+ */stubs/* — test stubs (freertos_stubs.cpp, etc.)
100
+ */build/* — CMake build directory
101
+ */mocks/* — mock implementations
102
+ */googletest/* — GoogleTest source
103
+ */.cache/* — any cache directories
104
+ */generated/* — auto-generated code
105
+ */third_party/* — third-party dependencies
106
+ ```
107
+
108
+ ### lcov Exclude Pattern
109
+
110
+ ```bash
111
+ lcov --capture --directory . \
112
+ --output coverage.info \
113
+ --exclude '*/test_*' \
114
+ --exclude '*/stubs/*' \
115
+ --exclude '*/build/*' \
116
+ --exclude '*/googletest/*' \
117
+ --exclude '*/third_party/*'
118
+ ```
119
+
120
+ ---
121
+
122
+ ## CI Integration: Enforcing the 80% Gate
123
+
124
+ ### GitHub Actions
125
+
126
+ ```yaml
127
+ - name: Run host tests with coverage
128
+ run: |
129
+ cd test/host
130
+ mkdir -p build && cd build
131
+ cmake .. -DCOVERAGE=ON -DCMAKE_BUILD_TYPE=Debug
132
+ cmake --build . -j$(nproc)
133
+ ./test_policy || { cat test_output.log; exit 1; }
134
+
135
+ - name: Capture coverage
136
+ run: |
137
+ lcov --capture --directory test/host/build \
138
+ --output coverage.info \
139
+ --exclude '*/test_*' --exclude '*/stubs/*' --exclude '*/build/*' \
140
+ --exclude '*/googletest/*'
141
+ lcov --extract coverage.info '*/main/*' --output src_coverage.info
142
+ lcov --list src_coverage.info
143
+
144
+ - name: Check 80% gate
145
+ run: |
146
+ total=$(lcov --list src_coverage.info | grep -E "lines\.\.\." | awk '{print $2}' | tr -d '%')
147
+ echo "Coverage: $total%"
148
+ if (( $(echo "$total < 80" | bc -l) )); then
149
+ echo "ERROR: Coverage $total% < 80% gate"
150
+ lcov --list src_coverage.info
151
+ exit 1
152
+ fi
153
+ ```
154
+
155
+ ### GitLab CI
156
+
157
+ ```yaml
158
+ coverage-test:
159
+ script:
160
+ - mkdir -p build && cd build
161
+ - cmake .. -DCOVERAGE=ON -DCMAKE_BUILD_TYPE=Debug
162
+ - cmake --build . -j$(nproc)
163
+ - ./test_policy
164
+ - lcov --capture --directory . --output coverage.info \
165
+ --exclude '*/test_*' --exclude '*/stubs/*' --exclude '*/build/*'
166
+ - lcov --extract coverage.info '*/main/*' --output src_coverage.info
167
+ - lcov --list src_coverage.info
168
+ coverage: /lines\.\.\.: (\d+\.\d+)%/
169
+ ```
170
+
171
+ ---
172
+
173
+ ## Interpreting Coverage Reports
174
+
175
+ ### What "covered" means
176
+
177
+ A line is covered if **any test** executes it at least once.
178
+
179
+ ```cpp
180
+ bool is_valid(int x) {
181
+ if (x > 0) // line 2 — covered if x=1 tested
182
+ return true; // line 3 — covered if x=1 tested
183
+ return false; // line 5 — covered if x=-1 tested
184
+ }
185
+ ```
186
+
187
+ ### What "not covered" means
188
+
189
+ ```bash
190
+ $ lcov --list filtered.info
191
+ Overall coverage rate:
192
+ lines......: 73.5% (217 / 295)
193
+ branches...: 58.3% (42 / 72)
194
+ ```
195
+
196
+ - **lines 73.5%** — below 80%, gate fails
197
+ - **branches 58.3%** — some `if` branches untested; add negative tests
198
+
199
+ ### Finding Uncovered Lines
200
+
201
+ ```bash
202
+ genhtml coverage.info --output-directory html --show-details
203
+ # Open html/*/*.cpp.gcov.html — uncovered lines are highlighted red
204
+ ```
205
+
206
+ Or use `gcovr`:
207
+
208
+ ```bash
209
+ gcovr --filter main/ --xml-pretty > coverage.xml
210
+ # coverage.xml can be imported into GitHub PR checks
211
+ ```
212
+
213
+ ---
214
+
215
+ ## Coverage Gaps: How to Find and Fix
216
+
217
+ ### 1. Uncovered Functions
218
+
219
+ ```bash
220
+ lcov --list filtered.info | grep -E "function.*0\.0"
221
+ ```
222
+
223
+ **Fix:** Write a test that calls the function.
224
+
225
+ ### 2. Uncovered Branches
226
+
227
+ ```bash
228
+ lcov --list filtered.info | grep -E "branch.*not executed"
229
+ ```
230
+
231
+ **Fix:** Add a test for the untaken branch. Example: if `if (ptr == nullptr)` is untested, add:
232
+
233
+ ```cpp
234
+ TEST(Datacollector, append_handles_nullptr) {
235
+ EXPECT_EQ(datacollector_append(nullptr, 42.0), -1);
236
+ }
237
+ ```
238
+
239
+ ### 3. Uncovered Error Paths
240
+
241
+ ```cpp
242
+ // production code
243
+ int datacollector_append(Datacollector* dc, double value) {
244
+ if (dc == nullptr) return -1; // ← often untested
245
+ if (dc->count >= MAX) return -1; // ← sometimes untested
246
+ dc->readings[dc->count++] = value;
247
+ return 0;
248
+ }
249
+ ```
250
+
251
+ **Fix:** Add tests for both error conditions:
252
+
253
+ ```cpp
254
+ TEST(Datacollector, append_returns_error_for_nullptr) {
255
+ EXPECT_EQ(datacollector_append(nullptr, 42.0), -1);
256
+ }
257
+
258
+ TEST(Datacollector, append_returns_error_when_full) {
259
+ Datacollector dc;
260
+ for (int i = 0; i < MAX; ++i) datacollector_append(&dc, 0.0);
261
+ EXPECT_EQ(datacollector_append(&dc, 99.0), -1);
262
+ }
263
+ ```
264
+
265
+ ---
266
+
267
+ ## Coverage Exclusions: When and How
268
+
269
+ ### Exclude Generated Code
270
+
271
+ ```cmake
272
+ # In CMakeLists.txt — mark generated sources
273
+ set_source_files_properties(
274
+ ${CMAKE_CURRENT_SOURCE_DIR}/generated/serde_autogen.cpp
275
+ PROPERTIES HEADER_FILE_ONLY ON
276
+ )
277
+ ```
278
+
279
+ Or via `lcov --remove`:
280
+
281
+ ```bash
282
+ lcov --remove coverage.info '*/generated/*' --output cleaned.info
283
+ ```
284
+
285
+ ### Exclude Test Stubs
286
+
287
+ ```bash
288
+ lcov --remove coverage.info '*/stubs/*' --output cleaned.info
289
+ ```
290
+
291
+ ### Exclude Platform-Specific Code
292
+
293
+ ```cpp
294
+ // coverage: ignore start
295
+ #ifdef ESP_PLATFORM
296
+ // ESP-IDF only
297
+ #endif
298
+ // coverage: ignore end
299
+ ```
300
+
301
+ Note: GCC supports `#pragma GCC coverage_options` but it's fragile. Prefer exclusion at the `lcov` level.
302
+
303
+ ---
304
+
305
+ ## Common Pitfalls
306
+
307
+ ### 1. 100% Coverage ≠ Correct Code
308
+
309
+ ```cpp
310
+ // Tests achieve 100% coverage but don't test correctness
311
+ TEST(Math, add_returns_something) {
312
+ EXPECT_NE(add(2, 2), 0); // passes for add(2,2)=99 — wrong!
313
+ }
314
+ ```
315
+
316
+ **Fix:** Test for **exact expected values**, not just non-zero.
317
+
318
+ ### 2. Over-Excluding Files
319
+
320
+ Excluding `*_test.cpp` is fine. Excluding entire modules because they're "hard to test" is a smell — those modules need redesign.
321
+
322
+ ### 3. Coverage as a Goal
323
+
324
+ Writing tests to increase coverage % (rather than to verify behavior) leads to:
325
+ - Meaningless tests (e.g., `EXPECT_TRUE(true)`)
326
+ - Skipped edge cases
327
+ - False confidence
328
+
329
+ Coverage is a **minimum bar**, not a target. Aim for **behavioral correctness** first.
330
+
331
+ ---
332
+
333
+ ## Quick Reference: Coverage Commands
334
+
335
+ ```bash
336
+ # Full pipeline
337
+ cd test/host && mkdir -p build && cd build
338
+ cmake .. -DCOVERAGE=ON -DCMAKE_BUILD_TYPE=Debug
339
+ cmake --build . -j$(nproc)
340
+ ./test_policy || exit 1
341
+
342
+ # Capture (all files)
343
+ lcov --capture --directory . --output coverage.info \
344
+ --exclude '*/test_*' --exclude '*/stubs/*' --exclude '*/build/*'
345
+
346
+ # Filter to only main/
347
+ lcov --extract coverage.info '*/main/*' --output src_coverage.info
348
+
349
+ # List summary
350
+ lcov --list src_coverage.info
351
+
352
+ # HTML output
353
+ genhtml src_coverage.info --output-directory coverage_html
354
+
355
+ # Enforce 80% gate
356
+ total=$(lcov --list src_coverage.info | grep "lines" | awk '{print $2}' | tr -d '%')
357
+ python3 -c "exit(0 if float('$total') >= 80.0 else 1)" || \
358
+ { echo "FAIL: coverage $total% < 80%"; lcov --list src_coverage.info; exit 1; }
359
+ ```
360
+
361
+ ---
362
+
363
+ ## Summary
364
+
365
+ - **Measure:** Use `gcov` + `lcov` (or `gcovr`) to capture and report coverage
366
+ - **Exclude:** Test files, stubs, mocks, generated code, third-party deps
367
+ - **Gate:** 80% line coverage on `main/` and `components/` (non-ESP-IDF)
368
+ - **Inspect:** Check branch coverage for untested `if/else` paths
369
+ - **Fix:** Add tests for uncovered lines and branches, not just to raise %
370
+ - **CI:** Enforce the gate in CI before merging