@mikrojs/firmware 0.16.0 → 0.17.0-next.20260722220623

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 (37) hide show
  1. package/components/mikrojs/CMakeLists.txt +9 -2
  2. package/components/mikrojs/include/mikrojs_esp32.h +23 -0
  3. package/components/mikrojs/mik_config.cpp +159 -5
  4. package/components/mikrojs/mik_deploy.cpp +54 -35
  5. package/components/mikrojs/mik_main.cpp +66 -5
  6. package/components/mikrojs/mik_nvs_kv.cpp +64 -18
  7. package/components/mikrojs/mik_ota.cpp +1791 -0
  8. package/components/mikrojs/platform_esp32.cpp +59 -0
  9. package/components/mikrojs/test/CMakeLists.txt +1 -0
  10. package/components/mikrojs/test/ble_test.cpp +2 -2
  11. package/components/mikrojs/test/fs_js_test.cpp +1 -1
  12. package/components/mikrojs/test/fs_pub_test.cpp +23 -23
  13. package/components/mikrojs/test/http_server_test.cpp +12 -2
  14. package/components/mikrojs/test/http_test.cpp +2 -2
  15. package/components/mikrojs/test/modules_extended_test.cpp +2 -2
  16. package/components/mikrojs/test/modules_test.cpp +2 -2
  17. package/components/mikrojs/test/nvs_kv_test.cpp +245 -0
  18. package/components/mikrojs/test/ota_host/gunzip_host_test.cpp +118 -0
  19. package/components/mikrojs/test/ota_host/run.sh +174 -0
  20. package/components/mikrojs/test/rtc_test.cpp +1 -1
  21. package/components/mikrojs/test/sleep_test.cpp +1 -1
  22. package/components/mikrojs/test/sntp_test.cpp +1 -1
  23. package/components/mikrojs/test/stdio_test.cpp +1 -1
  24. package/components/mikrojs/test/sys_test.cpp +1 -1
  25. package/components/mikrojs/test/udp_test.cpp +5 -5
  26. package/components/mikrojs/test/wifi_test.cpp +2 -2
  27. package/package.json +3 -3
  28. package/prebuilds/esp32/bootloader/bootloader.bin +0 -0
  29. package/prebuilds/esp32/mikrojs.bin +0 -0
  30. package/prebuilds/esp32c3/bootloader/bootloader.bin +0 -0
  31. package/prebuilds/esp32c3/mikrojs.bin +0 -0
  32. package/prebuilds/esp32c5/bootloader/bootloader.bin +0 -0
  33. package/prebuilds/esp32c5/mikrojs.bin +0 -0
  34. package/prebuilds/esp32c6/bootloader/bootloader.bin +0 -0
  35. package/prebuilds/esp32c6/mikrojs.bin +0 -0
  36. package/prebuilds/esp32s3/bootloader/bootloader.bin +0 -0
  37. package/prebuilds/esp32s3/mikrojs.bin +0 -0
@@ -4,6 +4,8 @@
4
4
  #include <esp_log.h>
5
5
  #include <esp_heap_caps.h>
6
6
  #include <esp_mac.h>
7
+ #include <nanocbor/nanocbor.h>
8
+ #include <nvs.h>
7
9
  #include <esp_random.h>
8
10
  #include <esp_sleep.h>
9
11
  #include <esp_system.h>
@@ -143,6 +145,61 @@ static int esp32_stdin_read(void* buf, size_t len) {
143
145
  /* Base MAC (6 bytes) encoded as Crockford's Base32 (10 lowercase chars).
144
146
  * Lossless: decode the 10 chars to recover the original 6 MAC bytes.
145
147
  * Crockford's Base32 excludes I, L, O, U to avoid visual ambiguity. */
148
+ /* The name pair lives in mik.sys under `name`, stored the way every other
149
+ * mik.sys value is: a CBOR text-string blob, so `mikro/kv/nvs` and CMD_KV_SET
150
+ * both read and write it unchanged. */
151
+ static char esp32_device_name[128] = {0};
152
+
153
+ static const char* esp32_get_device_name(void) {
154
+ nvs_handle_t handle;
155
+ if (nvs_open("mik.sys", NVS_READONLY, &handle) != ESP_OK) return NULL;
156
+
157
+ size_t len = 0;
158
+ if (nvs_get_blob(handle, "name", NULL, &len) != ESP_OK || len == 0 || len > 256) {
159
+ nvs_close(handle);
160
+ return NULL;
161
+ }
162
+ uint8_t buf[256];
163
+ esp_err_t err = nvs_get_blob(handle, "name", buf, &len);
164
+ nvs_close(handle);
165
+ if (err != ESP_OK) return NULL;
166
+
167
+ nanocbor_value_t decoder;
168
+ nanocbor_decoder_init(&decoder, buf, len);
169
+ const uint8_t* str = NULL;
170
+ size_t str_len = 0;
171
+ if (nanocbor_get_tstr(&decoder, &str, &str_len) < 0) return NULL;
172
+ if (str_len == 0 || str_len >= sizeof(esp32_device_name)) return NULL;
173
+
174
+ memcpy(esp32_device_name, str, str_len);
175
+ esp32_device_name[str_len] = '\0';
176
+ return esp32_device_name;
177
+ }
178
+
179
+ static void esp32_set_device_name(const char* value) {
180
+ nvs_handle_t handle;
181
+ if (nvs_open("mik.sys", NVS_READWRITE, &handle) != ESP_OK) return;
182
+
183
+ if (!value || value[0] == '\0') {
184
+ nvs_erase_key(handle, "name");
185
+ nvs_commit(handle);
186
+ nvs_close(handle);
187
+ return;
188
+ }
189
+
190
+ size_t value_len = strlen(value);
191
+ uint8_t buf[256];
192
+ nanocbor_encoder_t enc;
193
+ nanocbor_encoder_init(&enc, buf, sizeof(buf));
194
+ if (nanocbor_put_tstrn(&enc, value, value_len) < 0) {
195
+ nvs_close(handle);
196
+ return;
197
+ }
198
+ nvs_set_blob(handle, "name", buf, nanocbor_encoded_len(&enc));
199
+ nvs_commit(handle);
200
+ nvs_close(handle);
201
+ }
202
+
146
203
  static const char* esp32_get_device_id(void) {
147
204
  static const char cb32[] = "0123456789abcdefghjkmnpqrstvwxyz";
148
205
  static char id[11] = {0}; /* 6 bytes (48 bits) -> 10 x 5-bit chars + NUL */
@@ -275,6 +332,8 @@ static const MIKPlatform esp32_platform = {
275
332
  .stderr_write = esp32_stderr_write,
276
333
  .stdin_read = esp32_stdin_read,
277
334
  .get_device_id = esp32_get_device_id,
335
+ .get_device_name = esp32_get_device_name,
336
+ .set_device_name = esp32_set_device_name,
278
337
  .get_reset_reason = esp32_get_reset_reason,
279
338
  };
280
339
 
@@ -10,6 +10,7 @@ set(_MIK_TEST_SRCS
10
10
  i2s_test.cpp
11
11
  modules_extended_test.cpp
12
12
  modules_test.cpp
13
+ nvs_kv_test.cpp
13
14
  pins_test.cpp
14
15
  pwm_test.cpp
15
16
  repl_protocol_test.cpp
@@ -28,7 +28,7 @@ static void teardown() {
28
28
  /* Ensure the BLE stack is stopped between tests so every test starts
29
29
  * from a clean state. stop() is idempotent. */
30
30
  JSValue ret = MIK_EvalModuleContent(
31
- ctx, "mikrojs/ble-test-teardown",
31
+ ctx, "mikro/ble-test-teardown",
32
32
  "import {Ble} from \"native:mikro/ble\"; new Ble().stop();", 48);
33
33
  if (!JS_IsException(ret)) {
34
34
  JS_FreeValue(ctx, ret);
@@ -43,7 +43,7 @@ static void teardown() {
43
43
  }
44
44
 
45
45
  static JSValue eval_module(const char* code) {
46
- JSValue ret = MIK_EvalModuleContent(ctx, "mikrojs/ble-test", code, strlen(code));
46
+ JSValue ret = MIK_EvalModuleContent(ctx, "mikro/ble-test", code, strlen(code));
47
47
  if (!JS_IsException(ret)) {
48
48
  JS_FreeValue(ctx, ret);
49
49
  mik__execute_jobs(ctx);
@@ -44,7 +44,7 @@ static void fs_js_setup() {
44
44
  static void fs_js_teardown() { MIK_FreeRuntime(mik_rt); }
45
45
 
46
46
  static JSValue fs_eval_module(const char* code) {
47
- JSValue ret = MIK_EvalModuleContent(mik_ctx, "mikrojs/test", code, strlen(code));
47
+ JSValue ret = MIK_EvalModuleContent(mik_ctx, "mikro/test", code, strlen(code));
48
48
  if (!JS_IsException(ret)) {
49
49
  JS_FreeValue(mik_ctx, ret);
50
50
  mik__execute_jobs(mik_ctx);
@@ -25,7 +25,7 @@ static void pub_fs_teardown() {
25
25
  }
26
26
 
27
27
  static JSValue pub_fs_eval(const char* code) {
28
- JSValue ret = MIK_EvalModuleContent(mik_ctx, "mikrojs/test", code, strlen(code));
28
+ JSValue ret = MIK_EvalModuleContent(mik_ctx, "mikro/test", code, strlen(code));
29
29
  if (!JS_IsException(ret)) {
30
30
  JS_FreeValue(mik_ctx, ret);
31
31
  mik__execute_jobs(mik_ctx);
@@ -65,11 +65,11 @@ static int32_t get_global_int32(const char* name) {
65
65
 
66
66
  /* ── readFile ────────────────────────────────────────────────────── */
67
67
 
68
- TEST_CASE("mikrojs/fs readFile returns Uint8Array", "[fs]") {
68
+ TEST_CASE("mikro/fs readFile returns Uint8Array", "[fs]") {
69
69
  pub_fs_setup();
70
70
 
71
71
  JSValue ret = pub_fs_eval(R"(
72
- import { readFile } from "mikrojs/fs";
72
+ import { readFile } from "mikro/fs";
73
73
  const r = readFile("/dummy.txt");
74
74
  globalThis.__ok = r.ok;
75
75
  if (r.ok) globalThis.__content = new TextDecoder().decode(r.value);
@@ -85,11 +85,11 @@ TEST_CASE("mikrojs/fs readFile returns Uint8Array", "[fs]") {
85
85
  free(content);
86
86
  }
87
87
 
88
- TEST_CASE("mikrojs/fs readFile with utf-8 returns string", "[fs]") {
88
+ TEST_CASE("mikro/fs readFile with utf-8 returns string", "[fs]") {
89
89
  pub_fs_setup();
90
90
 
91
91
  JSValue ret = pub_fs_eval(R"(
92
- import { readFile } from "mikrojs/fs";
92
+ import { readFile } from "mikro/fs";
93
93
  const r = readFile("/dummy.txt", "utf-8");
94
94
  globalThis.__ok = r.ok;
95
95
  globalThis.__isString = r.ok && typeof r.value === "string";
@@ -108,11 +108,11 @@ TEST_CASE("mikrojs/fs readFile with utf-8 returns string", "[fs]") {
108
108
  free(content);
109
109
  }
110
110
 
111
- TEST_CASE("mikrojs/fs readFile returns NotFound error for missing file", "[fs]") {
111
+ TEST_CASE("mikro/fs readFile returns NotFound error for missing file", "[fs]") {
112
112
  pub_fs_setup();
113
113
 
114
114
  JSValue ret = pub_fs_eval(R"(
115
- import { readFile } from "mikrojs/fs";
115
+ import { readFile } from "mikro/fs";
116
116
  const r = readFile("/nonexistent.txt");
117
117
  globalThis.__ok = r.ok;
118
118
  if (!r.ok) {
@@ -136,11 +136,11 @@ TEST_CASE("mikrojs/fs readFile returns NotFound error for missing file", "[fs]")
136
136
 
137
137
  /* ── writeFile ───────────────────────────────────────────────────── */
138
138
 
139
- TEST_CASE("mikrojs/fs writeFile and readFile roundtrip", "[fs]") {
139
+ TEST_CASE("mikro/fs writeFile and readFile roundtrip", "[fs]") {
140
140
  pub_fs_setup();
141
141
 
142
142
  JSValue ret = pub_fs_eval(R"(
143
- import { writeFile, readFile } from "mikrojs/fs";
143
+ import { writeFile, readFile } from "mikro/fs";
144
144
  const w = writeFile("/test_pub_write.txt", "hello from pub fs");
145
145
  const r = readFile("/test_pub_write.txt", "utf-8");
146
146
  globalThis.__ok = w.ok && r.ok;
@@ -157,11 +157,11 @@ TEST_CASE("mikrojs/fs writeFile and readFile roundtrip", "[fs]") {
157
157
  free(content);
158
158
  }
159
159
 
160
- TEST_CASE("mikrojs/fs writeFile with Uint8Array", "[fs]") {
160
+ TEST_CASE("mikro/fs writeFile with Uint8Array", "[fs]") {
161
161
  pub_fs_setup();
162
162
 
163
163
  JSValue ret = pub_fs_eval(R"(
164
- import { writeFile, readFile } from "mikrojs/fs";
164
+ import { writeFile, readFile } from "mikro/fs";
165
165
  const data = new TextEncoder().encode("binary data");
166
166
  const w = writeFile("/test_pub_binary.txt", data);
167
167
  const r = readFile("/test_pub_binary.txt", "utf-8");
@@ -181,11 +181,11 @@ TEST_CASE("mikrojs/fs writeFile with Uint8Array", "[fs]") {
181
181
 
182
182
  /* ── stat ────────────────────────────────────────────────────────── */
183
183
 
184
- TEST_CASE("mikrojs/fs stat returns file info", "[fs]") {
184
+ TEST_CASE("mikro/fs stat returns file info", "[fs]") {
185
185
  pub_fs_setup();
186
186
 
187
187
  JSValue ret = pub_fs_eval(R"(
188
- import { stat } from "mikrojs/fs";
188
+ import { stat } from "mikro/fs";
189
189
  const r = stat("/dummy.txt");
190
190
  globalThis.__ok = r.ok;
191
191
  if (r.ok) {
@@ -208,11 +208,11 @@ TEST_CASE("mikrojs/fs stat returns file info", "[fs]") {
208
208
  TEST_ASSERT_FALSE(is_dir);
209
209
  }
210
210
 
211
- TEST_CASE("mikrojs/fs stat returns NotFound for missing path", "[fs]") {
211
+ TEST_CASE("mikro/fs stat returns NotFound for missing path", "[fs]") {
212
212
  pub_fs_setup();
213
213
 
214
214
  JSValue ret = pub_fs_eval(R"(
215
- import { stat } from "mikrojs/fs";
215
+ import { stat } from "mikro/fs";
216
216
  const r = stat("/nonexistent.txt");
217
217
  globalThis.__ok = r.ok;
218
218
  if (!r.ok) globalThis.__name = r.error.name;
@@ -230,11 +230,11 @@ TEST_CASE("mikrojs/fs stat returns NotFound for missing path", "[fs]") {
230
230
 
231
231
  /* ── exists ──────────────────────────────────────────────────────── */
232
232
 
233
- TEST_CASE("mikrojs/fs exists returns true for existing file", "[fs]") {
233
+ TEST_CASE("mikro/fs exists returns true for existing file", "[fs]") {
234
234
  pub_fs_setup();
235
235
 
236
236
  JSValue ret = pub_fs_eval(R"(
237
- import { exists } from "mikrojs/fs";
237
+ import { exists } from "mikro/fs";
238
238
  globalThis.__exists = exists("/dummy.txt");
239
239
  )");
240
240
  bool was_exception = JS_IsException(ret);
@@ -245,11 +245,11 @@ TEST_CASE("mikrojs/fs exists returns true for existing file", "[fs]") {
245
245
  TEST_ASSERT_TRUE(file_exists);
246
246
  }
247
247
 
248
- TEST_CASE("mikrojs/fs exists returns false for missing file", "[fs]") {
248
+ TEST_CASE("mikro/fs exists returns false for missing file", "[fs]") {
249
249
  pub_fs_setup();
250
250
 
251
251
  JSValue ret = pub_fs_eval(R"(
252
- import { exists } from "mikrojs/fs";
252
+ import { exists } from "mikro/fs";
253
253
  globalThis.__exists = exists("/nonexistent.txt");
254
254
  )");
255
255
  bool was_exception = JS_IsException(ret);
@@ -262,11 +262,11 @@ TEST_CASE("mikrojs/fs exists returns false for missing file", "[fs]") {
262
262
 
263
263
  /* ── mkdir / readDir / rmdir ─────────────────────────────────────── */
264
264
 
265
- TEST_CASE("mikrojs/fs mkdir, readDir, rmdir roundtrip", "[fs]") {
265
+ TEST_CASE("mikro/fs mkdir, readDir, rmdir roundtrip", "[fs]") {
266
266
  pub_fs_setup();
267
267
 
268
268
  JSValue ret = pub_fs_eval(R"(
269
- import { mkdir, readDir, writeFile, unlink, rmdir } from "mikrojs/fs";
269
+ import { mkdir, readDir, writeFile, unlink, rmdir } from "mikro/fs";
270
270
  const m = mkdir("/test_pub_dir");
271
271
  const w = writeFile("/test_pub_dir/file.txt", "hi");
272
272
  const entries = readDir("/test_pub_dir");
@@ -288,11 +288,11 @@ TEST_CASE("mikrojs/fs mkdir, readDir, rmdir roundtrip", "[fs]") {
288
288
 
289
289
  /* ── unlink / rename ─────────────────────────────────────────────── */
290
290
 
291
- TEST_CASE("mikrojs/fs unlink and rename", "[fs]") {
291
+ TEST_CASE("mikro/fs unlink and rename", "[fs]") {
292
292
  pub_fs_setup();
293
293
 
294
294
  JSValue ret = pub_fs_eval(R"(
295
- import { writeFile, rename, readFile, unlink } from "mikrojs/fs";
295
+ import { writeFile, rename, readFile, unlink } from "mikro/fs";
296
296
  const w = writeFile("/test_pub_rename_src.txt", "rename me");
297
297
  const mv = rename("/test_pub_rename_src.txt", "/test_pub_rename_dst.txt");
298
298
  const r = readFile("/test_pub_rename_dst.txt", "utf-8");
@@ -33,7 +33,7 @@ static void setup() {
33
33
  static void teardown() { MIK_FreeRuntime(rt); }
34
34
 
35
35
  static JSValue eval_module(const char* code) {
36
- JSValue ret = MIK_EvalModuleContent(ctx, "mikrojs/test", code, strlen(code));
36
+ JSValue ret = MIK_EvalModuleContent(ctx, "mikro/test", code, strlen(code));
37
37
  if (!JS_IsException(ret)) {
38
38
  JS_FreeValue(ctx, ret);
39
39
  mik__execute_jobs(ctx);
@@ -43,13 +43,19 @@ static JSValue eval_module(const char* code) {
43
43
 
44
44
  static void ensure_initialized() {
45
45
  const char* code = "import { nextRequest } from 'native:mikro/http_server';";
46
- JSValue ret = MIK_EvalModuleContent(ctx, "mikrojs/test", code, strlen(code));
46
+ JSValue ret = MIK_EvalModuleContent(ctx, "mikro/test", code, strlen(code));
47
47
  if (!JS_IsException(ret)) {
48
48
  JS_FreeValue(ctx, ret);
49
49
  mik__execute_jobs(ctx);
50
50
  }
51
51
  }
52
52
 
53
+ /* nextRequest() resolves with the closed sentinel unless a server is running.
54
+ * Fake a handle so tests can drive the queues without esp_http_server; clear
55
+ * it before teardown so destroy doesn't httpd_stop the fake. */
56
+ static void fake_server_start() { mik__hs(rt)->server = reinterpret_cast<httpd_handle_t>(1); }
57
+ static void fake_server_stop() { mik__hs(rt)->server = nullptr; }
58
+
53
59
  static void read_global_string(const char* key, char* out, size_t out_len) {
54
60
  JSValue global = JS_GetGlobalObject(ctx);
55
61
  JSValue v = JS_GetPropertyStr(ctx, global, key);
@@ -139,6 +145,7 @@ TEST_CASE("http_server consume with no messages is a no-op", "[httpd]") {
139
145
  TEST_CASE("nextRequest delivers method/url and respond fills the exchange", "[httpd]") {
140
146
  setup();
141
147
  ensure_initialized();
148
+ fake_server_start();
142
149
 
143
150
  MIKHsExchange* ex = push_request("GET", "/hello?x=1");
144
151
 
@@ -168,6 +175,7 @@ TEST_CASE("nextRequest delivers method/url and respond fills the exchange", "[ht
168
175
  size_t headerCount = ex->header_count;
169
176
 
170
177
  free_exchange(ex);
178
+ fake_server_stop();
171
179
  teardown();
172
180
 
173
181
  TEST_ASSERT_EQUAL_STRING("GET", method);
@@ -184,6 +192,7 @@ TEST_CASE("nextRequest delivers method/url and respond fills the exchange", "[ht
184
192
  TEST_CASE("respond tolerates a bodyless response", "[httpd]") {
185
193
  setup();
186
194
  ensure_initialized();
195
+ fake_server_start();
187
196
 
188
197
  MIKHsExchange* ex = push_request("POST", "/save");
189
198
 
@@ -208,6 +217,7 @@ TEST_CASE("respond tolerates a bodyless response", "[httpd]") {
208
217
  bool bodyNull = (ex->body == nullptr);
209
218
 
210
219
  free_exchange(ex);
220
+ fake_server_stop();
211
221
  teardown();
212
222
 
213
223
  TEST_ASSERT_FALSE_MESSAGE(threw, "bodyless respond must not throw");
@@ -43,7 +43,7 @@ static void setup() {
43
43
 
44
44
  static void ensure_http_initialized() {
45
45
  const char* code = "import { request } from 'native:mikro/http';";
46
- JSValue ret = MIK_EvalModuleContent(ctx, "mikrojs/test", code, strlen(code));
46
+ JSValue ret = MIK_EvalModuleContent(ctx, "mikro/test", code, strlen(code));
47
47
  if (!JS_IsException(ret)) {
48
48
  JS_FreeValue(ctx, ret);
49
49
  mik__execute_jobs(ctx);
@@ -53,7 +53,7 @@ static void ensure_http_initialized() {
53
53
  static void teardown() { MIK_FreeRuntime(rt); }
54
54
 
55
55
  static JSValue eval_module(const char* code) {
56
- JSValue ret = MIK_EvalModuleContent(ctx, "mikrojs/test", code, strlen(code));
56
+ JSValue ret = MIK_EvalModuleContent(ctx, "mikro/test", code, strlen(code));
57
57
  if (!JS_IsException(ret)) {
58
58
  JS_FreeValue(ctx, ret);
59
59
  mik__execute_jobs(ctx);
@@ -108,8 +108,8 @@ TEST_CASE("native:* internal import blocked from user code", "[modules]") {
108
108
  TEST_ASSERT_NULL_MESSAGE(result, "native:mikro/sys should be blocked from user code");
109
109
 
110
110
  // But a built-in module should be allowed to import native:mikro/sys
111
- result = mik_module_normalizer(ctx, "mikrojs/sys", "native:mikro/sys", nullptr);
112
- TEST_ASSERT_NOT_NULL_MESSAGE(result, "native:mikro/sys should be allowed from mikrojs/ modules");
111
+ result = mik_module_normalizer(ctx, "mikro/sys", "native:mikro/sys", nullptr);
112
+ TEST_ASSERT_NOT_NULL_MESSAGE(result, "native:mikro/sys should be allowed from mikro/ modules");
113
113
  TEST_ASSERT_EQUAL_STRING("native:mikro/sys", result);
114
114
  js_free(ctx, result);
115
115
 
@@ -17,7 +17,7 @@ TEST_CASE("Bare module names pass through unchanged", "[modules]") {
17
17
  JSRuntime* rt = JS_NewRuntime();
18
18
  JSContext* ctx = JS_NewContext(rt);
19
19
 
20
- assert_normalizes_to(ctx, "main.js", "mikrojs/fs", "mikrojs/fs");
20
+ assert_normalizes_to(ctx, "main.js", "mikro/fs", "mikro/fs");
21
21
  assert_normalizes_to(ctx, "main.js", "lodash", "lodash");
22
22
 
23
23
  JS_FreeContext(ctx);
@@ -112,7 +112,7 @@ TEST_CASE("withUnload() unloads a module after the callback returns", "[modules]
112
112
 
113
113
  /* Exercises the real shipped mikrojs/module withUnload() on device. */
114
114
  const char* code =
115
- "import {withUnload} from 'mikrojs/module';\n"
115
+ "import {withUnload} from 'mikro/module';\n"
116
116
  "globalThis.__first = await withUnload(import('./leaf.js'), (mod) => mod.v);\n"
117
117
  "globalThis.__after = true;\n"
118
118
  "const b = await import('./leaf.js');\n"
@@ -0,0 +1,245 @@
1
+ #include <nvs_flash.h>
2
+
3
+ #include "mikrojs.h"
4
+ #include "private.h"
5
+ #include "quickjs.h"
6
+ #include "unity.h"
7
+
8
+ /* nvs_kv module is self-registered via MIK_REGISTER_MODULE and lazily
9
+ * initialized. These tests cover the mik.kv / mik.sys namespace split on
10
+ * real NVS flash; each test clears both namespaces it touched. */
11
+
12
+ static MIKRuntime* rt;
13
+ static JSContext* ctx;
14
+
15
+ static void setup() {
16
+ /* The bare test runtime has no MIK_Main; init NVS flash once ourselves. */
17
+ static bool nvs_ready = false;
18
+ if (!nvs_ready) {
19
+ esp_err_t err = nvs_flash_init();
20
+ if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
21
+ nvs_flash_erase();
22
+ nvs_flash_init();
23
+ }
24
+ nvs_ready = true;
25
+ }
26
+ rt = MIK_NewRuntime();
27
+ ctx = MIK_GetJSContext(rt);
28
+ }
29
+
30
+ static void teardown() { MIK_FreeRuntime(rt); }
31
+
32
+ static JSValue eval_module(const char* code) {
33
+ /* The name must pass the loader's builtin-importer check ("mikro/" prefix)
34
+ * or native: imports are denied. */
35
+ JSValue ret = MIK_EvalModuleContent(ctx, "mikro/test", code, strlen(code));
36
+ if (!JS_IsException(ret)) {
37
+ JS_FreeValue(ctx, ret);
38
+ mik__execute_jobs(ctx);
39
+ } else {
40
+ JSValue exc = JS_GetException(ctx);
41
+ const char* msg = JS_ToCString(ctx, exc);
42
+ printf("eval exception: %s\n", msg ? msg : "(unprintable)");
43
+ JS_FreeCString(ctx, msg);
44
+ JS_Throw(ctx, exc);
45
+ }
46
+ return ret;
47
+ }
48
+
49
+ /* ── Module exports ───────────────────────────────────────────────── */
50
+
51
+ TEST_CASE("native:mikro/nvs_kv exports sys functions and Result-returning clears", "[kv]") {
52
+ setup();
53
+
54
+ JSValue ret = eval_module(R"(
55
+ import { clear, sysSet, sysGet, sysRemove, sysClear } from "native:mikro/nvs_kv";
56
+ globalThis.__sysSetIsFunc = typeof sysSet === "function";
57
+ globalThis.__sysGetIsFunc = typeof sysGet === "function";
58
+ globalThis.__sysRemoveIsFunc = typeof sysRemove === "function";
59
+ globalThis.__sysClearIsFunc = typeof sysClear === "function";
60
+ globalThis.__clearOk = clear().ok === true;
61
+ globalThis.__sysClearOk = sysClear().ok === true;
62
+ )");
63
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
64
+
65
+ JSValue global = JS_GetGlobalObject(ctx);
66
+
67
+ JSValue v;
68
+ v = JS_GetPropertyStr(ctx, global, "__sysSetIsFunc");
69
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, v), "sysSet should be a function");
70
+ JS_FreeValue(ctx, v);
71
+
72
+ v = JS_GetPropertyStr(ctx, global, "__sysGetIsFunc");
73
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, v), "sysGet should be a function");
74
+ JS_FreeValue(ctx, v);
75
+
76
+ v = JS_GetPropertyStr(ctx, global, "__sysRemoveIsFunc");
77
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, v), "sysRemove should be a function");
78
+ JS_FreeValue(ctx, v);
79
+
80
+ v = JS_GetPropertyStr(ctx, global, "__sysClearIsFunc");
81
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, v), "sysClear should be a function");
82
+ JS_FreeValue(ctx, v);
83
+
84
+ v = JS_GetPropertyStr(ctx, global, "__clearOk");
85
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, v), "clear() should return {ok: true}");
86
+ JS_FreeValue(ctx, v);
87
+
88
+ v = JS_GetPropertyStr(ctx, global, "__sysClearOk");
89
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, v), "sysClear() should return {ok: true}");
90
+ JS_FreeValue(ctx, v);
91
+
92
+ JS_FreeValue(ctx, global);
93
+ teardown();
94
+ }
95
+
96
+ /* ── Namespace isolation ──────────────────────────────────────────── */
97
+
98
+ TEST_CASE("native:mikro/nvs_kv kv and sys namespaces are isolated", "[kv]") {
99
+ setup();
100
+
101
+ JSValue ret = eval_module(R"(
102
+ import { set, get, remove, clear, sysSet, sysGet, sysRemove, sysClear }
103
+ from "native:mikro/nvs_kv";
104
+ clear();
105
+ sysClear();
106
+ set("probe", "app");
107
+ sysSet("probe", "sys");
108
+ globalThis.__kvSees = get("probe") === "app";
109
+ globalThis.__sysSees = sysGet("probe") === "sys";
110
+ remove("probe");
111
+ globalThis.__kvGone = get("probe") === undefined;
112
+ globalThis.__sysSurvivesRemove = sysGet("probe") === "sys";
113
+ sysRemove("probe");
114
+ globalThis.__sysGone = sysGet("probe") === undefined;
115
+ clear();
116
+ sysClear();
117
+ )");
118
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
119
+
120
+ JSValue global = JS_GetGlobalObject(ctx);
121
+
122
+ JSValue v;
123
+ v = JS_GetPropertyStr(ctx, global, "__kvSees");
124
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, v), "kv should read its own value under a shared key");
125
+ JS_FreeValue(ctx, v);
126
+
127
+ v = JS_GetPropertyStr(ctx, global, "__sysSees");
128
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, v), "sys should read its own value under a shared key");
129
+ JS_FreeValue(ctx, v);
130
+
131
+ v = JS_GetPropertyStr(ctx, global, "__kvGone");
132
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, v), "remove should delete the kv entry");
133
+ JS_FreeValue(ctx, v);
134
+
135
+ v = JS_GetPropertyStr(ctx, global, "__sysSurvivesRemove");
136
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, v), "kv remove must not touch the sys entry");
137
+ JS_FreeValue(ctx, v);
138
+
139
+ v = JS_GetPropertyStr(ctx, global, "__sysGone");
140
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, v), "sysRemove should delete the sys entry");
141
+ JS_FreeValue(ctx, v);
142
+
143
+ JS_FreeValue(ctx, global);
144
+ teardown();
145
+ }
146
+
147
+ /* ── Clear scopes ─────────────────────────────────────────────────── */
148
+
149
+ TEST_CASE("native:mikro/nvs_kv clear() leaves sys, sysClear() leaves kv", "[kv]") {
150
+ setup();
151
+
152
+ JSValue ret = eval_module(R"(
153
+ import { set, get, clear, sysSet, sysGet, sysClear } from "native:mikro/nvs_kv";
154
+ clear();
155
+ sysClear();
156
+ set("a", 1);
157
+ sysSet("b", 2);
158
+ clear();
159
+ globalThis.__kvGone = get("a") === undefined;
160
+ globalThis.__sysIntact = sysGet("b") === 2;
161
+ set("a", 1);
162
+ sysClear();
163
+ globalThis.__kvIntact = get("a") === 1;
164
+ globalThis.__sysGone = sysGet("b") === undefined;
165
+ clear();
166
+ )");
167
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
168
+
169
+ JSValue global = JS_GetGlobalObject(ctx);
170
+
171
+ JSValue v;
172
+ v = JS_GetPropertyStr(ctx, global, "__kvGone");
173
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, v), "clear should empty the kv namespace");
174
+ JS_FreeValue(ctx, v);
175
+
176
+ v = JS_GetPropertyStr(ctx, global, "__sysIntact");
177
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, v), "clear must not touch the sys namespace");
178
+ JS_FreeValue(ctx, v);
179
+
180
+ v = JS_GetPropertyStr(ctx, global, "__kvIntact");
181
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, v), "sysClear must not touch the kv namespace");
182
+ JS_FreeValue(ctx, v);
183
+
184
+ v = JS_GetPropertyStr(ctx, global, "__sysGone");
185
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, v), "sysClear should empty the sys namespace");
186
+ JS_FreeValue(ctx, v);
187
+
188
+ JS_FreeValue(ctx, global);
189
+ teardown();
190
+ }
191
+
192
+ /* ── Public API full clear ────────────────────────────────────────── */
193
+
194
+ TEST_CASE("mikro/kv/nvs clear({full: true}) empties both stores", "[kv]") {
195
+ setup();
196
+
197
+ JSValue ret = eval_module(R"(
198
+ import { nvsStorage } from "mikro/kv/nvs";
199
+ import { set, get, clear, sysSet, sysGet, sysClear } from "native:mikro/nvs_kv";
200
+ clear();
201
+ sysClear();
202
+ set("a", 1);
203
+ sysSet("b", 2);
204
+ const partial = nvsStorage.clear();
205
+ globalThis.__partialOk = partial.ok === true;
206
+ globalThis.__kvAfterPartial = get("a") === undefined;
207
+ globalThis.__sysAfterPartial = sysGet("b") === 2;
208
+ set("a", 1);
209
+ const full = nvsStorage.clear({full: true});
210
+ globalThis.__fullOk = full.ok === true;
211
+ globalThis.__kvAfterFull = get("a") === undefined;
212
+ globalThis.__sysAfterFull = sysGet("b") === undefined;
213
+ )");
214
+ TEST_ASSERT_FALSE_MESSAGE(JS_IsException(ret), "Module eval should not throw");
215
+
216
+ JSValue global = JS_GetGlobalObject(ctx);
217
+
218
+ JSValue v;
219
+ v = JS_GetPropertyStr(ctx, global, "__partialOk");
220
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, v), "clear() should return ok");
221
+ JS_FreeValue(ctx, v);
222
+
223
+ v = JS_GetPropertyStr(ctx, global, "__kvAfterPartial");
224
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, v), "clear() should empty app data");
225
+ JS_FreeValue(ctx, v);
226
+
227
+ v = JS_GetPropertyStr(ctx, global, "__sysAfterPartial");
228
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, v), "clear() must leave the system store intact");
229
+ JS_FreeValue(ctx, v);
230
+
231
+ v = JS_GetPropertyStr(ctx, global, "__fullOk");
232
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, v), "clear({full: true}) should return ok");
233
+ JS_FreeValue(ctx, v);
234
+
235
+ v = JS_GetPropertyStr(ctx, global, "__kvAfterFull");
236
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, v), "clear({full: true}) should empty app data");
237
+ JS_FreeValue(ctx, v);
238
+
239
+ v = JS_GetPropertyStr(ctx, global, "__sysAfterFull");
240
+ TEST_ASSERT_TRUE_MESSAGE(JS_ToBool(ctx, v), "clear({full: true}) should empty the system store");
241
+ JS_FreeValue(ctx, v);
242
+
243
+ JS_FreeValue(ctx, global);
244
+ teardown();
245
+ }