@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,175 @@
1
+ # Framework Compare: GoogleTest vs Catch2 vs doctest
2
+
3
+ ## Overview
4
+
5
+ All three frameworks are C++17-compatible, header-only (or single-header), and support fixtures, parametrics, and mocking. The choice is primarily ergonomic and project-specific.
6
+
7
+ ---
8
+
9
+ ## GoogleTest
10
+
11
+ **Installation:** `apt install libgtest-dev` or `FetchContent` CMake include.
12
+
13
+ **Strengths:**
14
+ - `TEST_F` (fixture), `TEST_P` (parametric), death tests, `ScopedFakeTimer`
15
+ - Widest ecosystem: `gmock` for mocking, `gtest-param-test` for parameterized
16
+ - XML/JSON/HTML output for CI integration
17
+ - Well-understood by embedded/firmware teams
18
+
19
+ **Sample:**
20
+
21
+ ```cpp
22
+ #include <gtest/gtest.h>
23
+
24
+ class StackTest : public ::testing::Test {
25
+ protected:
26
+ void SetUp() override { stack_.clear(); }
27
+ std::vector<int> stack_;
28
+ };
29
+
30
+ TEST_F(StackTest, push_increases_size) {
31
+ stack_.push_back(1);
32
+ EXPECT_EQ(stack_.size(), 1u);
33
+ }
34
+
35
+ TEST_F(StackTest, pop_returns_last_element) {
36
+ stack_.push_back(1);
37
+ stack_.push_back(2);
38
+ EXPECT_EQ(stack_.back(), 2);
39
+ stack_.pop_back();
40
+ EXPECT_EQ(stack_.size(), 1u);
41
+ }
42
+ ```
43
+
44
+ **Parametric example:**
45
+
46
+ ```cpp
47
+ class StringEncodeTest : public ::testing::TestWithParam<const char*> {};
48
+
49
+ TEST_P(StringEncodeTest, encodes_known_formats) {
50
+ const char* fmt = GetParam();
51
+ EXPECT_NO_THROW(encode(fmt));
52
+ }
53
+
54
+ INSTANTIATE_TEST_SUITE_P(KnownFormats, StringEncodeTest,
55
+ ::testing::Values("utf-8", "iso-8859-1", "windows-1252"));
56
+ ```
57
+
58
+ **Best for:** Large codebases, embedded firmware, projects needing `gmock`, parametric tests, or death tests.
59
+
60
+ ---
61
+
62
+ ## Catch2
63
+
64
+ **Installation:** Single header: `curl -L https://github.com/catchorg/Catch2/releases/download/v3.5.4/catch2_with_main.hpp -o include/catch2_with_main.hpp`
65
+
66
+ **Strengths:**
67
+ - BDD-style `SCENARIO` / `GIVEN` / `WHEN` / `THEN` DSL available
68
+ - Minimal boilerplate — no `TEST_F` needed for simple cases
69
+ - Rich assertion syntax: `REQUIRE`, `CHECK`, `REQUIRE_THAT`, `CHECK_THAT`
70
+
71
+ **Sample:**
72
+
73
+ ```cpp
74
+ #define CATCH_CONFIG_MAIN
75
+ #include <catch2/catch_test_macros.hpp>
76
+
77
+ unsigned int factorial(unsigned int n) {
78
+ return n <= 1 ? 1 : n * factorial(n - 1);
79
+ }
80
+
81
+ TEST_CASE("factorial computes correctly", "[factorial]") {
82
+ CHECK(factorial(0) == 1);
83
+ CHECK(factorial(1) == 1);
84
+ CHECK(factorial(4) == 24);
85
+ CHECK(factorial(5) == 120);
86
+ }
87
+ ```
88
+
89
+ **BDD-style:**
90
+
91
+ ```cpp
92
+ SCENARIO("stack behavior") {
93
+ GIVEN("an empty stack") {
94
+ std::vector<int> stack;
95
+ WHEN("an element is pushed") {
96
+ stack.push_back(42);
97
+ THEN("size increases") { CHECK(stack.size() == 1); }
98
+ }
99
+ }
100
+ }
101
+ ```
102
+
103
+ **Best for:** Quick-to-write tests, BDD-style readability, smaller utilities.
104
+
105
+ **Weakness:** Slower compile times than doctest; less ecosystem (no built-in mocking — pair with `trompeloeil` or manual fakes).
106
+
107
+ ---
108
+
109
+ ## doctest
110
+
111
+ **Installation:** Single header: `curl -L https://github.com/doctest/doctest/releases/download/v2.4.11/doctest.h -o include/doctest.h`
112
+
113
+ **Strengths:**
114
+ - Fastest compile time of the three
115
+ - Lightweight — minimal impact on build times
116
+ - `CHECK`/`REQUIRE` similar to Catch2
117
+ - Sub-test support via `SUBTEST`
118
+
119
+ **Sample:**
120
+
121
+ ```cpp
122
+ #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
123
+ #include <doctest/doctest.h>
124
+
125
+ int add(int a, int b) { return a + b; }
126
+
127
+ TEST_CASE("addition works") {
128
+ CHECK(add(2, 3) == 5);
129
+ CHECK(add(-1, 1) == 0);
130
+ REQUIRE(add(0, 0) == 0); // REQUIRE aborts on failure
131
+ }
132
+ ```
133
+
134
+ **Best for:** Compile-time-sensitive projects, small-to-medium codebases, projects that already use many headers.
135
+
136
+ **Weakness:** Smaller ecosystem; mocking requires manual fakes or `trompeloeil`.
137
+
138
+ ---
139
+
140
+ ## Side-by-Side Comparison
141
+
142
+ | Feature | GoogleTest | Catch2 | doctest |
143
+ |---------|-----------|--------|---------|
144
+ | Header-only | No (needs gtest lib) | Yes | Yes |
145
+ | Compile speed | Medium | Slow | Fast |
146
+ | `TEST_F` (fixtures) | Yes | Via `SECTION` | Via `SUBCASE` |
147
+ | `TEST_P` (parametric) | Yes | No | No |
148
+ | Death tests | Yes | No | No |
149
+ | Built-in mocking | gmock | No | No |
150
+ | BDD DSL | No | Yes | No |
151
+ | CI XML output | Yes | Yes | Yes |
152
+ | C++17 required | Yes | Yes | Yes |
153
+ | Ecosystem size | Large | Medium | Small |
154
+
155
+ ---
156
+
157
+ ## Recommendation for Embedded/Firmware Host Tests
158
+
159
+ Default to **GoogleTest** + **gmock**:
160
+ - `TEST_F` / `TEST_P` are well-suited to firmware policy modules
161
+ - `gmock` provides struct-level mocking via `MATCHER` / `ON_CALL`
162
+ - `EXPECT_DEATH` / `EXPECT_DEATH_IF_SUPPORTED` useful for error-injection tests
163
+ - Widely understood — existing firmware teams will recognize it
164
+
165
+ Use **Catch2** when you want BDD-style readability for payload parsing or protocol tests.
166
+
167
+ Use **doctest** when build time is a bottleneck and you only need simple `CHECK` assertions.
168
+
169
+ ---
170
+
171
+ ## Mixing Frameworks
172
+
173
+ It is possible to link multiple frameworks into the same binary, but avoid this — it causes confusion and doubled setup cost. Pick one framework per binary.
174
+
175
+ If you need both BDD-style and parametric tests, use GoogleTest's `TEST_F` with `DESCRIPTION` or use Catch2's `SECTION` mechanism instead of `TEST_P`.
@@ -0,0 +1,499 @@
1
+ # Host-Side Tests for Embedded Firmware
2
+
3
+ ## Goal
4
+
5
+ Build a standalone Linux binary that links and executes C++ firmware code from `main/` or `components/` — with no ESP-IDF, no FreeRTOS, and no `idf.py`. The binary runs on the host (your laptop or CI runner) and exercises the real production code, just with mocked hardware/OS boundaries.
6
+
7
+ This pattern is ideal for:
8
+ - **Policy modules:** `feature_flags.cpp`, `resp_fallback.cpp`, `rate_limiter.cpp`
9
+ - **Payload parsers:** `mqtt_payload.cpp`, `json_parser.cpp`
10
+ - **Data collectors:** `datacollector.cpp`, `telemetry.cpp`
11
+
12
+ ---
13
+
14
+ ## Why Host-Side Tests?
15
+
16
+ | On-Device (`idf.py test`) | Host-Side Binary |
17
+ |---------------------------|------------------|
18
+ | Runs on actual hardware | Runs on Linux/CI |
19
+ | Needs device flashing | No hardware required |
20
+ | Slow iteration | Fast iteration |
21
+ | FreeRTOS available | No FreeRTOS |
22
+ | ESP-IDF available | No ESP-IDF |
23
+ | HW timing real | Deterministic |
24
+
25
+ Host-side tests catch >80% of logic bugs before ever flashing. Use on-device tests only for hardware-specific behavior (interrupts, DMA, sensor drivers).
26
+
27
+ ---
28
+
29
+ ## Project Structure
30
+
31
+ ```
32
+ projects/ams7_esp32/
33
+ ├── main/
34
+ │ ├── policy/
35
+ │ │ ├── feature_flags.cpp
36
+ │ │ ├── feature_flags.h
37
+ │ │ ├── resp_fallback.cpp
38
+ │ │ └── resp_fallback.h
39
+ │ ├── payload/
40
+ │ │ ├── datacollector.cpp
41
+ │ │ └── datacollector.h
42
+ │ └── utils/
43
+ │ └── hex_utils.cpp
44
+ ├── test/ ← your host test directory
45
+ │ └── host/
46
+ │ ├── CMakeLists.txt
47
+ │ ├── test_feature_flags.cpp
48
+ │ ├── test_resp_fallback.cpp
49
+ │ └── test_datacollector.cpp
50
+ └── components/ ← if you have ESP-IDF components
51
+ ```
52
+
53
+ ---
54
+
55
+ ## CMakeLists.txt Pattern
56
+
57
+ ```cmake
58
+ # test/host/CMakeLists.txt
59
+ cmake_minimum_required(VERSION 3.16)
60
+ project(host_policy_tests)
61
+
62
+ set(CMAKE_CXX_STANDARD 17)
63
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
64
+
65
+ # --- GoogleTest ---
66
+ include(FetchContent)
67
+ FetchContent_Declare(
68
+ googletest
69
+ GIT_REPOSITORY https://github.com/google/googletest.git
70
+ GIT_TAG v1.14.0
71
+ )
72
+ set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
73
+ FetchContent_MakeAvailable(googletest)
74
+
75
+ # --- Test executable ---
76
+ add_executable(test_policy
77
+ test_feature_flags.cpp
78
+ test_resp_fallback.cpp
79
+ test_datacollector.cpp
80
+ )
81
+
82
+ # --- Link firmware modules under test ---
83
+ # Point FIRMWARE_ROOT at the project root or adjust as needed
84
+ target_include_directories(test_policy PRIVATE
85
+ ${CMAKE_CURRENT_SOURCE_DIR}/../../main
86
+ ${CMAKE_CURRENT_SOURCE_DIR}/../../main/policy
87
+ ${CMAKE_CURRENT_SOURCE_DIR}/../../main/payload
88
+ )
89
+
90
+ # Link the real implementation files directly (no ESP-IDF)
91
+ target_sources(test_policy PRIVATE
92
+ ${CMAKE_CURRENT_SOURCE_DIR}/../../main/policy/feature_flags.cpp
93
+ ${CMAKE_CURRENT_SOURCE_DIR}/../../main/policy/resp_fallback.cpp
94
+ ${CMAKE_CURRENT_SOURCE_DIR}/../../main/payload/datacollector.cpp
95
+ )
96
+
97
+ target_link_libraries(test_policy PRIVATE
98
+ gtest
99
+ gmock
100
+ pthread
101
+ )
102
+
103
+ # --- Optional: gcov coverage ---
104
+ if(COVERAGE)
105
+ target_compile_options(test_policy PRIVATE -fprofile-arcs -ftest-coverage)
106
+ target_link_libraries(test_policy PRIVATE gcov)
107
+ endif()
108
+ ```
109
+
110
+ **Build and run:**
111
+
112
+ ```bash
113
+ cd test/host
114
+ mkdir build && cd build
115
+ cmake .. -DCOVERAGE=ON
116
+ cmake --build . -j$(nproc)
117
+ ./test_policy
118
+ # or with coverage:
119
+ cmake --build . -j$(nproc) && lcov --capture --directory . --output coverage.info --exclude '*/test_*' --exclude '*/build/*'
120
+ ```
121
+
122
+ ---
123
+
124
+ ## Pattern 1: Testing `feature_flags.cpp`
125
+
126
+ ### Firmware header (minimal)
127
+
128
+ ```cpp
129
+ // main/policy/feature_flags.h
130
+ #pragma once
131
+ #include <stdbool.h>
132
+ #include <stddef.h>
133
+
134
+ #ifdef __cplusplus
135
+ extern "C" {
136
+ #endif
137
+
138
+ bool feature_flags_is_active(const char* name);
139
+ const char* feature_flags_get_version(void);
140
+
141
+ #ifdef __cplusplus
142
+ }
143
+ #endif
144
+ ```
145
+
146
+ ### Firmware implementation (real code)
147
+
148
+ ```cpp
149
+ // main/policy/feature_flags.cpp
150
+ #include "feature_flags.h"
151
+ #include <string.h>
152
+
153
+ static bool debug_flag = false;
154
+ static bool trace_flag = false;
155
+ static bool legacy_mode = false;
156
+
157
+ bool feature_flags_is_active(const char* name) {
158
+ if (name == nullptr) return false;
159
+ if (strcmp(name, "debug") == 0) return debug_flag;
160
+ if (strcmp(name, "trace") == 0) return trace_flag;
161
+ if (strcmp(name, "legacy") == 0) return legacy_mode;
162
+ return false;
163
+ }
164
+
165
+ const char* feature_flags_get_version(void) {
166
+ return "1.2.0";
167
+ }
168
+ ```
169
+
170
+ ### Host test
171
+
172
+ ```cpp
173
+ // test/host/test_feature_flags.cpp
174
+ #include <gtest/gtest.h>
175
+ #include "feature_flags.h"
176
+ #include <string.h>
177
+
178
+ TEST(FeatureFlags, is_active_returns_false_for_nullptr) {
179
+ EXPECT_FALSE(feature_flags_is_active(nullptr));
180
+ }
181
+
182
+ TEST(FeatureFlags, is_active_returns_false_for_unknown_feature) {
183
+ EXPECT_FALSE(feature_flags_is_active("nonexistent"));
184
+ }
185
+
186
+ TEST(FeatureFlags, is_active_returns_true_for_known_enabled_feature) {
187
+ // In real firmware these might be set at boot; for host test
188
+ // we test the implementation directly — no mocking needed here
189
+ // because the flags are compile-time false in this build.
190
+ EXPECT_FALSE(feature_flags_is_active("debug")); // currently disabled
191
+ }
192
+
193
+ TEST(FeatureFlags, get_version_returns_semver_string) {
194
+ const char* ver = feature_flags_get_version();
195
+ ASSERT_NE(nullptr, ver);
196
+ EXPECT_STREQ("1.2.0", ver);
197
+ }
198
+ ```
199
+
200
+ ---
201
+
202
+ ## Pattern 2: Testing `resp_fallback.cpp` with Mocked Response
203
+
204
+ ### Firmware header
205
+
206
+ ```cpp
207
+ // main/policy/resp_fallback.h
208
+ #pragma once
209
+ #include <stddef.h>
210
+
211
+ typedef struct {
212
+ const char* payload;
213
+ size_t payload_len;
214
+ int status_code;
215
+ } Response;
216
+
217
+ typedef enum {
218
+ FALLBACK_OK,
219
+ FALLBACK_MISSING,
220
+ FALLBACK_INVALID
221
+ } FallbackResult;
222
+
223
+ FallbackResult resp_fallback_process(const Response* req, Response* out);
224
+ ```
225
+
226
+ ### Firmware implementation
227
+
228
+ ```cpp
229
+ // main/policy/resp_fallback.cpp
230
+ #include "resp_fallback.h"
231
+ #include <string.h>
232
+
233
+ static const char* s_fallback_payload = "default_response";
234
+ static bool s_fallback_available = true;
235
+
236
+ void resp_fallback_set_fallback_available(bool available) {
237
+ s_fallback_available = available;
238
+ }
239
+
240
+ FallbackResult resp_fallback_process(const Response* req, Response* out) {
241
+ if (req == nullptr || out == nullptr) return FALLBACK_INVALID;
242
+ if (req->status_code >= 200 && req->status_code < 300) {
243
+ // Pass-through for success codes
244
+ out->payload = req->payload;
245
+ out->payload_len = req->payload_len;
246
+ out->status_code = req->status_code;
247
+ return FALLBACK_OK;
248
+ }
249
+ // Error code — try fallback
250
+ if (!s_fallback_available) return FALLBACK_MISSING;
251
+ out->payload = s_fallback_payload;
252
+ out->payload_len = strlen(s_fallback_payload);
253
+ out->status_code = 200;
254
+ return FALLBACK_OK;
255
+ }
256
+ ```
257
+
258
+ ### Host test with `std::function` injection
259
+
260
+ ```cpp
261
+ // test/host/test_resp_fallback.cpp
262
+ #include <gtest/gtest.h>
263
+ #include "resp_fallback.h"
264
+ #include <cstring>
265
+
266
+ class RespFallbackTest : public ::testing::Test {
267
+ protected:
268
+ void SetUp() override {
269
+ resp_fallback_set_fallback_available(true);
270
+ }
271
+ void TearDown() override {
272
+ resp_fallback_set_fallback_available(true);
273
+ }
274
+ };
275
+
276
+ TEST_F(RespFallbackTest, success_codes_pass_through) {
277
+ Response in = { .payload = "ok", .payload_len = 2, .status_code = 200 };
278
+ Response out;
279
+ FallbackResult r = resp_fallback_process(&in, &out);
280
+ EXPECT_EQ(r, FALLBACK_OK);
281
+ EXPECT_EQ(out.status_code, 200);
282
+ EXPECT_STREQ(out.payload, "ok");
283
+ }
284
+
285
+ TEST_F(RespFallbackTest, error_code_returns_fallback) {
286
+ Response in = { .payload = "error", .payload_len = 5, .status_code = 500 };
287
+ Response out;
288
+ FallbackResult r = resp_fallback_process(&in, &out);
289
+ EXPECT_EQ(r, FALLBACK_OK);
290
+ EXPECT_EQ(out.status_code, 200);
291
+ EXPECT_STREQ(out.payload, "default_response");
292
+ }
293
+
294
+ TEST_F(RespFallbackTest, error_code_when_fallback_unavailable_returns_missing) {
295
+ resp_fallback_set_fallback_available(false);
296
+ Response in = { .payload = "error", .payload_len = 5, .status_code = 500 };
297
+ Response out;
298
+ FallbackResult r = resp_fallback_process(&in, &out);
299
+ EXPECT_EQ(r, FALLBACK_MISSING);
300
+ }
301
+
302
+ TEST_F(RespFallbackTest, null_request_returns_invalid) {
303
+ Response out;
304
+ EXPECT_EQ(resp_fallback_process(nullptr, &out), FALLBACK_INVALID);
305
+ }
306
+
307
+ TEST_F(RespFallbackTest, null_output_returns_invalid) {
308
+ Response in = { .payload = "ok", .payload_len = 2, .status_code = 200 };
309
+ EXPECT_EQ(resp_fallback_process(&in, nullptr), FALLBACK_INVALID);
310
+ }
311
+ ```
312
+
313
+ ---
314
+
315
+ ## Pattern 3: Testing `datacollector.cpp`
316
+
317
+ ### Firmware header
318
+
319
+ ```cpp
320
+ // main/payload/datacollector.h
321
+ #pragma once
322
+ #include <stddef.h>
323
+ #include <stdint.h>
324
+
325
+ #define DATACOLLECTOR_MAX_READINGS 64
326
+
327
+ typedef struct {
328
+ double readings[DATACOLLECTOR_MAX_READINGS];
329
+ size_t count;
330
+ } Datacollector;
331
+
332
+ void datacollector_init(Datacollector* dc);
333
+ int datacollector_append(Datacollector* dc, double value);
334
+ double datacollector_latest(const Datacollector* dc);
335
+ size_t datacollector_count(const Datacollector* dc);
336
+ void datacollector_clear(Datacollector* dc);
337
+ ```
338
+
339
+ ### Firmware implementation
340
+
341
+ ```cpp
342
+ // main/payload/datacollector.cpp
343
+ #include "datacollector.h"
344
+ #include <string.h>
345
+
346
+ void datacollector_init(Datacollector* dc) {
347
+ memset(dc, 0, sizeof(*dc));
348
+ }
349
+
350
+ int datacollector_append(Datacollector* dc, double value) {
351
+ if (dc->count >= DATACOLLECTOR_MAX_READINGS) return -1;
352
+ dc->readings[dc->count++] = value;
353
+ return 0;
354
+ }
355
+
356
+ double datacollector_latest(const Datacollector* dc) {
357
+ if (dc->count == 0) return 0.0; // Note: consider throwing instead
358
+ return dc->readings[dc->count - 1];
359
+ }
360
+
361
+ size_t datacollector_count(const Datacollector* dc) {
362
+ return dc->count;
363
+ }
364
+
365
+ void datacollector_clear(Datacollector* dc) {
366
+ memset(dc, 0, sizeof(*dc));
367
+ }
368
+ ```
369
+
370
+ ### Host test
371
+
372
+ ```cpp
373
+ // test/host/test_datacollector.cpp
374
+ #include <gtest/gtest.h>
375
+ #include "datacollector.h"
376
+ #include <cmath>
377
+
378
+ class DatacollectorTest : public ::testing::Test {
379
+ protected:
380
+ void SetUp() override { datacollector_init(&dc_); }
381
+ Datacollector dc_;
382
+ };
383
+
384
+ TEST_F(DatacollectorTest, init_clears_readings) {
385
+ EXPECT_EQ(datacollector_count(&dc_), 0u);
386
+ }
387
+
388
+ TEST_F(DatacollectorTest, append_increments_count) {
389
+ EXPECT_EQ(datacollector_append(&dc_, 1.0), 0);
390
+ EXPECT_EQ(datacollector_count(&dc_), 1u);
391
+ EXPECT_EQ(datacollector_append(&dc_, 2.0), 0);
392
+ EXPECT_EQ(datacollector_count(&dc_), 2u);
393
+ }
394
+
395
+ TEST_F(DatacollectorTest, latest_returns_last_value) {
396
+ datacollector_append(&dc_, 1.0);
397
+ datacollector_append(&dc_, 2.0);
398
+ datacollector_append(&dc_, 3.0);
399
+ EXPECT_DOUBLE_EQ(datacollector_latest(&dc_), 3.0);
400
+ }
401
+
402
+ TEST_F(DatacollectorTest, latest_returns_zero_when_empty) {
403
+ // Current impl returns 0 — this documents current behavior
404
+ EXPECT_DOUBLE_EQ(datacollector_latest(&dc_), 0.0);
405
+ }
406
+
407
+ TEST_F(DatacollectorTest, append_returns_minus_one_when_full) {
408
+ for (int i = 0; i < DATACOLLECTOR_MAX_READINGS; ++i) {
409
+ EXPECT_EQ(datacollector_append(&dc_, static_cast<double>(i)), 0);
410
+ }
411
+ EXPECT_EQ(datacollector_append(&dc_, 999.0), -1); // full
412
+ }
413
+
414
+ TEST_F(DatacollectorTest, clear_resets_count) {
415
+ datacollector_append(&dc_, 1.0);
416
+ datacollector_append(&dc_, 2.0);
417
+ datacollector_clear(&dc_);
418
+ EXPECT_EQ(datacollector_count(&dc_), 0u);
419
+ }
420
+ ```
421
+
422
+ ---
423
+
424
+ ## Handling FreeRTOS Dependencies
425
+
426
+ If a firmware module calls FreeRTOS APIs (e.g., `vTaskDelay`, `xQueueSend`), create a **stub library** that provides no-op or fake implementations:
427
+
428
+ ```cpp
429
+ // test/host/stubs/freertos_stubs.cpp
430
+ #include "freertos/FreeRTOS.h"
431
+ #include "freertos/task.h"
432
+
433
+ extern "C" {
434
+
435
+ void vTaskDelay(const TickType_t xTicksToDelay) {
436
+ (void)xTicksToDelay; // no-op on host
437
+ }
438
+
439
+ BaseType_t xQueueSend(QueueHandle_t, const void*, TickType_t) {
440
+ return pdTRUE; // fake success
441
+ }
442
+
443
+ } // extern "C"
444
+ ```
445
+
446
+ Then in `CMakeLists.txt`:
447
+
448
+ ```cmake
449
+ target_sources(test_policy PRIVATE
450
+ ${CMAKE_CURRENT_SOURCE_DIR}/../../main/policy/feature_flags.cpp
451
+ ${CMAKE_CURRENT_SOURCE_DIR}/stubs/freertos_stubs.cpp # link stubs
452
+ )
453
+ ```
454
+
455
+ Keep stubs minimal. If a module requires extensive FreeRTOS mocking, consider testing it as an integration test on-device instead.
456
+
457
+ ---
458
+
459
+ ## CI Integration
460
+
461
+ ```yaml
462
+ # .github/workflows/host-tests.yml (GitHub Actions example)
463
+ name: Host-Side Tests
464
+
465
+ on: [push, pull_request]
466
+
467
+ jobs:
468
+ host-tests:
469
+ runs-on: ubuntu-latest
470
+ steps:
471
+ - uses: actions/checkout@v4
472
+ - name: Install deps
473
+ run: sudo apt-get update && sudo apt-get install -y cmake g++ lcov
474
+ - name: Build and test
475
+ run: |
476
+ cd test/host
477
+ mkdir -p build && cd build
478
+ cmake .. -DCOVERAGE=ON
479
+ cmake --build . -j$(nproc)
480
+ ./test_policy || exit 1
481
+ lcov --capture --directory . --output coverage.info \
482
+ --exclude '*/test_*' --exclude '*/build/*' --exclude '*/stubs/*'
483
+ lcov --filter coverage.info --output filtered.info \
484
+ --exclude '*/test_*' --exclude '*/stubs/*'
485
+ lcov --list filtered.info
486
+ # 80% gate:
487
+ lcov --filter filtered.info --output-filtered.info \
488
+ | grep -q "lines\.\.\.\." || true
489
+ ```
490
+
491
+ ---
492
+
493
+ ## Key Principles
494
+
495
+ 1. **Link real `.cpp` files** — not headers only. You want to test the compiled implementation.
496
+ 2. **No ESP-IDF includes** in test sources — if a header pulls in `esp_log.h`, create a local stub or use `-D` flags to mock it out.
497
+ 3. **Use `extern "C"`** when linking firmware `.cpp` files that have C linkage.
498
+ 4. **Test behavior, not structure** — assert on return values, state changes, and side effects visible through the public API.
499
+ 5. **Deterministic only** — host tests must be free of timing, threading races, and hardware dependencies.