@dimina-kit/devkit 0.1.2-dev.20260616102751 → 0.1.2-dev.20260624040247

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.
@@ -159,21 +159,20 @@ describe('compile-worker-entry — wireCompileWorkerProcess (orphan-safety-net s
159
159
  });
160
160
  });
161
161
  /**
162
- * CODEX-REVIEW REGRESSION (m9) — the fork-mode self-wiring gate currently
163
- * uses `process.argv[1].includes('compile-worker-entry')`, a SUBSTRING match.
164
- * Any unrelated script whose path merely contains that fragment (run with an
165
- * IPC channel, e.g. itself a fork) would import this module and silently
166
- * self-wire: a disconnect→exit(0) handler and a message handler hijacking the
167
- * host's IPC traffic. The gate must engage ONLY when argv[1] is THIS module
168
- * (same path as import.meta.url) pinned behaviorally below via a fresh
169
- * module evaluation under a manipulated argv[1].
162
+ * The fork-mode self-wiring gate must NOT use a SUBSTRING match on
163
+ * `process.argv[1]`: any unrelated script whose path merely contains
164
+ * `compile-worker-entry` (run with an IPC channel, e.g. itself a fork) would
165
+ * import this module and silently self-wire a disconnect→exit(0) handler and
166
+ * a message handler hijacking the host's IPC traffic. The gate must engage
167
+ * ONLY when argv[1] is THIS module (same path as import.meta.url) — pinned
168
+ * behaviorally below via a fresh module evaluation under a manipulated argv[1].
170
169
  *
171
170
  * Observation seam: self-wiring is `process.on('message'|'disconnect', …)` on
172
171
  * the REAL process — the tests diff the process listener sets around the
173
172
  * import and remove any added listeners again in cleanup (so a failing run
174
173
  * can never leak a disconnect→exit(0) handler into the vitest worker).
175
174
  */
176
- describe('compile-worker-entry — fork-mode self-wiring gate (codex m9)', () => {
175
+ describe('compile-worker-entry — fork-mode self-wiring gate', () => {
177
176
  function snapshotListeners() {
178
177
  return {
179
178
  message: process.listeners('message'),
@@ -198,7 +197,7 @@ describe('compile-worker-entry — fork-mode self-wiring gate (codex m9)', () =>
198
197
  vi.resetModules();
199
198
  await import('./compile-worker-entry.js');
200
199
  }
201
- it('m9: an argv[1] LOOKALIKE path (substring match) must NOT self-wire the worker IPC handlers', async () => {
200
+ it('an argv[1] LOOKALIKE path (substring match) must NOT self-wire the worker IPC handlers', async () => {
202
201
  const originalArgv1 = process.argv[1] ?? '';
203
202
  const originalSend = process.send;
204
203
  const before = snapshotListeners();
@@ -222,7 +221,7 @@ describe('compile-worker-entry — fork-mode self-wiring gate (codex m9)', () =>
222
221
  vi.resetModules();
223
222
  }
224
223
  });
225
- it('m9 positive control: when argv[1] IS this entry module, the fork-mode self-wiring engages', async () => {
224
+ it('positive control: when argv[1] IS this entry module, the fork-mode self-wiring engages', async () => {
226
225
  const originalArgv1 = process.argv[1] ?? '';
227
226
  const originalSend = process.send;
228
227
  const before = snapshotListeners();
@@ -15,13 +15,13 @@ import { writeUntilPredicate } from './watch-rebuild.testutil.js';
15
15
  * > 0) re-writes the source file until the rebuild's first log line appears.
16
16
  */
17
17
  /**
18
- * LEAK-PROOFING WAVE (项目关闭时保证编译子进程同步关闭) REAL-PROCESS
19
- * integration contract. Nothing in this file mocks `child_process`: every
20
- * test forks the actual `compile-worker-entry` (and, for the `openProject`
21
- * tests, runs the actual `@dimina/compiler`) and asserts on REAL process
22
- * death via `process.kill(pid, 0)` (ESRCH ⇒ the PID is gone).
18
+ * REAL-PROCESS integration contract: a closed project must take its compile
19
+ * worker down with it. Nothing in this file mocks `child_process`: every test
20
+ * forks the actual `compile-worker-entry` (and, for the `openProject` tests,
21
+ * runs the actual `@dimina/compiler`) and asserts on REAL process death via
22
+ * `process.kill(pid, 0)` (ESRCH ⇒ the PID is gone).
23
23
  *
24
- * Why these tests exist on top of the existing mocked-fork suite
24
+ * Why these tests exist on top of the mocked-fork suite
25
25
  * (`compile-worker.test.ts` pins `child.kill` was CALLED): a kill() spy
26
26
  * proves intent, not death. The two leak classes this file closes:
27
27
  *
@@ -38,12 +38,11 @@ import { writeUntilPredicate } from './watch-rebuild.testutil.js';
38
38
  * actually dead (not merely "kill was invoked"), including when a build
39
39
  * is in flight at close time.
40
40
  *
41
- * PID discovery (decision recorded for the implementer): NO new public API
42
- * is pinned for exposing the worker PID. The worker is discovered externally
43
- * via a `ps -axo pid=,ppid=,command=` scan for children of THIS test process
44
- * whose command line contains `compile-worker-entry`. That keeps the
45
- * contract purely behavioral if the implementation later wants to expose
46
- * `worker.pid`, nothing here constrains it either way.
41
+ * PID discovery: NO public API is pinned for exposing the worker PID. The
42
+ * worker is discovered externally via a `ps -axo pid=,ppid=,command=` scan for
43
+ * children of THIS test process whose command line contains
44
+ * `compile-worker-entry`. That keeps the contract purely behavioral — exposing
45
+ * `worker.pid` later is unconstrained either way.
47
46
  *
48
47
  * CI stability: all death checks are bounded polls (8s deadline, 100ms
49
48
  * interval), and every discovered/forked PID is SIGKILLed in cleanup so a
@@ -256,13 +255,12 @@ describe('② session.close() — the worker PID is ACTUALLY dead afterwards (re
256
255
  + 'a busy worker that survives close is the classic "compile still running after project closed" leak').toBe(true);
257
256
  }, 90_000);
258
257
  /**
259
- * CODEX-REVIEW REGRESSION (M3): the tests above tolerate an 8s post-close
260
- * death poll. The actual contract is stronger — `await session.close()`
261
- * must RETURN only after the worker already exited (close() awaits the
262
- * child 'exit'), so the await itself is the death guarantee and no caller
263
- * ever needs a grace poll.
258
+ * The tests above tolerate an 8s post-close death poll. The actual contract
259
+ * is stronger — `await session.close()` must RETURN only after the worker
260
+ * already exited (close() awaits the child 'exit'), so the await itself is
261
+ * the death guarantee and no caller ever needs a grace poll.
264
262
  */
265
- it("M3: await session.close() returns only AFTER the worker PID is already dead — the await IS the guarantee, no grace poll", async () => {
263
+ it("await session.close() returns only AFTER the worker PID is already dead — the await IS the guarantee, no grace poll", async () => {
266
264
  const root = makeFixture();
267
265
  const session = await devkit.openProject({
268
266
  projectPath: root,
@@ -11,8 +11,7 @@ import { writeUntilPredicate, writeUntilSettled } from './watch-rebuild.testutil
11
11
  * below depend on a REAL chokidar inotify watch (fork is mocked, chokidar is
12
12
  * not). Under CI load (concurrent real dmcc compile in
13
13
  * open-project-compile-log.test.ts pegging CPU) a single fs.writeFileSync's
14
- * inotify event can be dropped, hanging `await rebuilt`/`vi.waitFor` forever
15
- * (PR #44 30s timeout at the "fork exactly once across … 2 rebuilds" test).
14
+ * inotify event can be dropped, hanging `await rebuilt`/`vi.waitFor` forever.
16
15
  * Count-agnostic / `>=` waiters are wrapped in writeUntilSettled /
17
16
  * writeUntilPredicate, which RE-WRITE the source file (micro-varied content →
18
17
  * fresh inotify event) until the rebuild lands; the rebuild scheduler
@@ -21,16 +20,12 @@ import { writeUntilPredicate, writeUntilSettled } from './watch-rebuild.testutil
21
20
  * them (see its inline note).
22
21
  */
23
22
  /**
24
- * FORK-ARCHITECTURE WAVE (dmcc 编译子进程化) TDD contract for the PARENT
25
- * side: `openProject` orchestrating a long-lived forked compile worker
26
- * (NOT yet implemented).
23
+ * Contract for the PARENT side: `openProject` orchestrating a long-lived
24
+ * forked compile worker.
27
25
  *
28
- * ⚠️ ARCHITECTURE-DECISION CHANGE (user-approved, 2026-06-12):
29
- * Compilation moves from the in-process `require('@dimina/compiler')` call
30
- * (src/index.ts:104-105 with its host-global `process.chdir`) into a forked
31
- * long-lived child process. The tee-style `withCapturedStdio` contract from
32
- * ROUND 2 was deleted (see compile-log.test.ts header); THIS file pins its
33
- * replacement. Explicit architecture correction, not goalpost-moving.
26
+ * Compilation runs in a forked long-lived child process (which chdirs in its
27
+ * own process) rather than the in-process `require('@dimina/compiler')` call
28
+ * with its host-global `process.chdir`.
34
29
  *
35
30
  * Parent-side contract pinned here (child_process.fork is mocked; the fake
36
31
  * child is an EventEmitter with PassThrough stdout/stderr + send/kill spies):
@@ -476,10 +471,10 @@ describe('openProject — fork-based compile worker orchestration', () => {
476
471
  }, 45_000);
477
472
  });
478
473
  /**
479
- * LEAK-PROOFING WAVE (项目关闭时保证编译子进程同步关闭) close-time guards on
480
- * the exported `createCompileWorker` directly (same mocked fork as above).
481
- * Real process death is pinned by `compile-worker-leak.test.ts`; these pin
482
- * the parent-side STATE MACHINE around close:
474
+ * Close-time guards on the exported `createCompileWorker` directly (same
475
+ * mocked fork as above). Real process death is pinned by
476
+ * `compile-worker-leak.test.ts`; these pin the parent-side STATE MACHINE
477
+ * around close:
483
478
  * - close with a build in flight kills immediately and SETTLES the pending
484
479
  * promise (a hung promise wedges the rebuild scheduler — and a scheduler
485
480
  * wedged at close time is itself a teardown leak),
@@ -546,23 +541,22 @@ describe('createCompileWorker — close-time leak guards (direct use)', () => {
546
541
  }, 45_000);
547
542
  });
548
543
  /**
549
- * CODEX-REVIEW REGRESSION WAVE (fix/editor-hot-reload-and-simulator-leftovers)
550
- * failing regression tests for review findings M1 / M2 / M3 / m7. Same
551
- * mocked-fork harness as above; each test names the finding it pins.
544
+ * Regression guards for worker error replies, fork errors, close semantics,
545
+ * and the trailing line. Same mocked-fork harness as above.
552
546
  *
553
- * M1 a `{ type:'result', error }` reply is currently resolved as a SUCCESS
554
- * with appInfo:null — it must reject the in-flight build (message passed
555
- * through) and surface via onBuildError on the rebuild path.
556
- * M2 a child 'error' event (spawn/IPC failure) is swallowed; Node does NOT
557
- * guarantee an accompanying 'exit', so the in-flight build hangs forever
558
- * and the dead child is never replaced.
559
- * M3 close() kills and returns immediately it must return a promise that
560
- * resolves only after the child actually exited, and must settle the
561
- * in-flight build itself (not rely on the child cooperating with exit).
562
- * m7 a final line without a trailing newline is buffered forever it must
563
- * be flushed to onLog exactly once when the stream ends.
547
+ * - a `{ type:'result', error }` reply must NOT resolve as a SUCCESS with
548
+ * appInfo:null — it must reject the in-flight build (message passed
549
+ * through) and surface via onBuildError on the rebuild path.
550
+ * - a child 'error' event (spawn/IPC failure) must not be swallowed; Node
551
+ * does NOT guarantee an accompanying 'exit', so the in-flight build would
552
+ * hang forever and the dead child would never be replaced.
553
+ * - close() must return a promise that resolves only after the child actually
554
+ * exited, and must settle the in-flight build itself (not rely on the child
555
+ * cooperating with exit).
556
+ * - a final line without a trailing newline must be flushed to onLog exactly
557
+ * once when the stream ends (not buffered forever).
564
558
  */
565
- describe('codex review regressions — worker error replies, fork errors, close semantics, trailing line (M1/M2/M3/m7)', () => {
559
+ describe('worker error replies, fork errors, close semantics, trailing line', () => {
566
560
  const REQUEST = {
567
561
  projectPath: '/tmp/p',
568
562
  outputDir: '/tmp/out',
@@ -583,7 +577,7 @@ describe('codex review regressions — worker error replies, fork errors, close
583
577
  return child;
584
578
  });
585
579
  }
586
- it('M1: build() rejects (error message passed through) when the worker reply carries an error — not a silent appInfo:null success', async () => {
580
+ it('build() rejects (error message passed through) when the worker reply carries an error — not a silent appInfo:null success', async () => {
587
581
  useSilentChildren();
588
582
  const worker = devkit.createCompileWorker({});
589
583
  const pending = worker.build(REQUEST);
@@ -600,7 +594,7 @@ describe('codex review regressions — worker error replies, fork errors, close
600
594
  });
601
595
  await settled;
602
596
  }, 45_000);
603
- it('M1: a rebuild whose worker reply carries an error settles through onBuildError — NOT through onRebuild as a fake hot-reload success', async () => {
597
+ it('a rebuild whose worker reply carries an error settles through onBuildError — NOT through onRebuild as a fake hot-reload success', async () => {
604
598
  const root = makeFixture();
605
599
  const buildErrors = [];
606
600
  const rebuilds = [];
@@ -632,7 +626,7 @@ describe('codex review regressions — worker error replies, fork errors, close
632
626
  expect(String(buildErrors[0].message), 'the worker-reported error message must pass through to onBuildError (the devtools panel renders it)').toMatch(/esbuild exploded/);
633
627
  expect(rebuilds, 'the failed rebuild must NOT fire onRebuild — onRebuild triggers the hot-reload toast').toHaveLength(0);
634
628
  }, 45_000);
635
- it("M2: a fork 'error' event with NO accompanying 'exit' settles the in-flight build (bounded) and the next build re-forks a fresh worker", async () => {
629
+ it("a fork 'error' event with NO accompanying 'exit' settles the in-flight build (bounded) and the next build re-forks a fresh worker", async () => {
636
630
  useSilentChildren();
637
631
  const worker = devkit.createCompileWorker({});
638
632
  const pending = worker.build(REQUEST);
@@ -657,7 +651,7 @@ describe('codex review regressions — worker error replies, fork errors, close
657
651
  fresh.respondToBuild('/tmp/p');
658
652
  await expect(second).resolves.toEqual(expect.objectContaining({ appId: WORKER_APP.appId }));
659
653
  }, 45_000);
660
- it("M2: 'error' followed by a LATE 'exit' is idempotent — the stale exit must not settle the NEXT build on the fresh worker", async () => {
654
+ it("'error' followed by a LATE 'exit' is idempotent — the stale exit must not settle the NEXT build on the fresh worker", async () => {
661
655
  useSilentChildren();
662
656
  const worker = devkit.createCompileWorker({});
663
657
  const pending = worker.build(REQUEST);
@@ -666,7 +660,7 @@ describe('codex review regressions — worker error replies, fork errors, close
666
660
  }, { timeout: 5000 });
667
661
  const first = children[0];
668
662
  first.emit('error', new Error('spawn failure'));
669
- expect(await raceSettle(pending, 2000), "the 'error' event alone must settle the in-flight build (see the companion M2 test)").toBe('rejected');
663
+ expect(await raceSettle(pending, 2000), "the 'error' event alone must settle the in-flight build").toBe('rejected');
670
664
  // Start the recovery build, THEN let the dead child's 'exit' fire late
671
665
  // (the real-world double-fire). The stale exit must not reject the new
672
666
  // in-flight build that belongs to the fresh worker.
@@ -683,7 +677,7 @@ describe('codex review regressions — worker error replies, fork errors, close
683
677
  fresh.respondToBuild('/tmp/p');
684
678
  await expect(second, "the dead child's late 'exit' must be a no-op for the new generation — error+exit double-fire settles ONE build, once").resolves.toEqual(expect.objectContaining({ appId: WORKER_APP.appId }));
685
679
  }, 45_000);
686
- it("M3: close() returns a promise that resolves only AFTER the child actually exited — kill-and-return is not a close", async () => {
680
+ it("close() returns a promise that resolves only AFTER the child actually exited — kill-and-return is not a close", async () => {
687
681
  const worker = devkit.createCompileWorker({});
688
682
  await worker.build(REQUEST);
689
683
  const child = children[0];
@@ -702,7 +696,7 @@ describe('codex review regressions — worker error replies, fork errors, close
702
696
  child.emit('exit', null, 'SIGTERM');
703
697
  await closeResult;
704
698
  }, 45_000);
705
- it('M3: close() itself rejects the in-flight build — even when the child never emits exit', async () => {
699
+ it('close() itself rejects the in-flight build — even when the child never emits exit', async () => {
706
700
  useSilentChildren();
707
701
  const worker = devkit.createCompileWorker({});
708
702
  const pending = worker.build(REQUEST);
@@ -717,18 +711,17 @@ describe('codex review regressions — worker error replies, fork errors, close
717
711
  + 'come leaves the rebuild scheduler hanging at teardown').toBe('rejected');
718
712
  }, 45_000);
719
713
  /**
720
- * CODEX RE-REVIEW M3 NOT-RESOLVED follow-up. `settleDeath`'s
721
- * `removeAllListeners()` also strips the `once('exit', resolve)` that
722
- * close() registered on the SAME child: when a child dies through the
723
- * 'error' path while a close() is in flight (kill sent, exit not yet
724
- * emitted), the closePromise loses its only resolver and hangs forever.
725
- * Contract: a child 'error' during an in-flight close is a death signal
726
- * (Node does NOT guarantee an 'exit' after 'error' — see M2) — the close
714
+ * `settleDeath`'s `removeAllListeners()` must not strip the
715
+ * `once('exit', resolve)` that close() registered on the SAME child: when a
716
+ * child dies through the 'error' path while a close() is in flight (kill
717
+ * sent, exit not yet emitted), the closePromise must not lose its only
718
+ * resolver and hang forever. A child 'error' during an in-flight close is a
719
+ * death signal (Node does NOT guarantee an 'exit' after 'error') the close
727
720
  * promise must still resolve, whether a late 'exit' follows or never comes.
728
- * The normal-exit-resolves case is already pinned above ("M3: close()
729
- * returns a promise that resolves only AFTER the child actually exited").
721
+ * The normal-exit-resolves case is pinned above ("close() returns a promise
722
+ * that resolves only AFTER the child actually exited").
730
723
  */
731
- it("M3 follow-up: a child 'error' during an in-flight close() must not strip the close resolver — closePromise resolves even though only a LATE 'exit' follows", async () => {
724
+ it("a child 'error' during an in-flight close() must not strip the close resolver — closePromise resolves even though only a LATE 'exit' follows", async () => {
732
725
  const worker = devkit.createCompileWorker({});
733
726
  await worker.build(REQUEST);
734
727
  const child = children[0];
@@ -746,21 +739,21 @@ describe('codex review regressions — worker error replies, fork errors, close
746
739
  expect(await raceSettle(closeResult, 1500), "settleDeath's removeAllListeners must not strip close()'s exit resolver — a child that dies via "
747
740
  + "'error' mid-close leaves closePromise hanging forever, wedging every awaiter of session.close()").toBe('resolved');
748
741
  }, 45_000);
749
- it("M3 follow-up: a child 'error' during an in-flight close() with NO 'exit' ever resolves the close too — 'error' is a death signal, not a wait-longer signal", async () => {
742
+ it("a child 'error' during an in-flight close() with NO 'exit' ever resolves the close too — 'error' is a death signal, not a wait-longer signal", async () => {
750
743
  const worker = devkit.createCompileWorker({});
751
744
  await worker.build(REQUEST);
752
745
  const child = children[0];
753
746
  child.manualExit = true;
754
747
  const closeResult = worker.close();
755
748
  expect(child.kill).toHaveBeenCalled();
756
- // Node does NOT guarantee an 'exit' after 'error' (the exact premise M2
757
- // pinned for builds): the lone 'error' must settle the close as well.
749
+ // Node does NOT guarantee an 'exit' after 'error': the lone 'error' must
750
+ // settle the close as well.
758
751
  child.emit('error', new Error('spawn EAGAIN'));
759
752
  expect(await raceSettle(closeResult, 1500), "a lone child 'error' during close() must resolve the close promise — settleDeath already treats "
760
753
  + "'error' as death everywhere else (clears the child, rejects builds); close() waiting for an 'exit' "
761
754
  + 'that Node never guarantees hangs teardown forever').toBe('resolved');
762
755
  }, 45_000);
763
- it('m7: a final line WITHOUT a trailing newline is flushed to onLog exactly once when the stream ends', async () => {
756
+ it('a final line WITHOUT a trailing newline is flushed to onLog exactly once when the stream ends', async () => {
764
757
  const root = makeFixture();
765
758
  const entries = [];
766
759
  const logOpts = { onLog: (entry) => entries.push(entry) };
package/dist/index.js CHANGED
@@ -68,7 +68,7 @@ export async function openProject(opts) {
68
68
  // When the compiler is racing (e.g. opening a project that was just
69
69
  // closed elsewhere in the same Electron process) `build()` can return
70
70
  // null even though the manifest on disk is perfectly readable. The
71
- // pre-existing fallback to `{ appId: 'unknown' }` was load-bearing for
71
+ // fallback to `{ appId: 'unknown' }` is load-bearing for
72
72
  // any consumer that calls `wx.setStorageSync` — the dimina runtime
73
73
  // stores values under `${appId}_${key}` and the devtools storage panel
74
74
  // also derives its IPC prefix from `appInfo.appId`. A stale `unknown`
@@ -6,17 +6,16 @@ import { PassThrough } from 'node:stream';
6
6
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
7
7
  import * as devkit from './index.js';
8
8
  /**
9
- * CODEX-REVIEW REGRESSION WAVE (fix/editor-hot-reload-and-simulator-leftovers)
10
- * failing regression tests for review findings M4 / M6: resource cleanup
11
- * when `openProject` fails AFTER the compile worker was already forked.
9
+ * Resource cleanup when `openProject` fails AFTER the compile worker was
10
+ * already forked.
12
11
  *
13
- * M4 the worker is created before the dev server starts; a server-start
14
- * failure propagates out of openProject but never kills the worker — a
15
- * leaked compiler process per failed open.
16
- * M6 `createProjectWatcher().ready` only resolves on chokidar's 'ready';
17
- * a watcher 'error' (EMFILE, permission loss, …) before ready leaves
18
- * `await watcher?.ready` and therefore openProject — hung forever,
19
- * leaking the worker AND the already-listening dev server.
12
+ * - the worker is created before the dev server starts; a server-start
13
+ * failure that propagates out of openProject must still kill the worker
14
+ * (otherwise a leaked compiler process per failed open).
15
+ * - `createProjectWatcher().ready` only resolves on chokidar's 'ready'; a
16
+ * watcher 'error' (EMFILE, permission loss, …) before ready must REJECT
17
+ * `ready` — otherwise `await watcher?.ready` (and openProject) hangs
18
+ * forever, leaking the worker AND the already-listening dev server.
20
19
  *
21
20
  * Harness: same fake-fork pattern as compile-worker.test.ts, plus this file
22
21
  * mocks the fe dev-server module and chokidar so the failure point of each
@@ -125,8 +124,8 @@ function makeFixture() {
125
124
  function sleep(ms) {
126
125
  return new Promise(resolve => setTimeout(resolve, ms));
127
126
  }
128
- describe('openProject failure cleanup — no leaked compile worker (codex M4/M6)', () => {
129
- it('M4: a dev-server start failure AFTER the worker was forked rejects openProject AND kills the worker', async () => {
127
+ describe('openProject failure cleanup — no leaked compile worker', () => {
128
+ it('a dev-server start failure AFTER the worker was forked rejects openProject AND kills the worker', async () => {
130
129
  const root = makeFixture();
131
130
  mocks.feStart.mockReset();
132
131
  mocks.feStart.mockRejectedValue(new Error('listen EADDRINUSE: address already in use'));
@@ -139,7 +138,7 @@ describe('openProject failure cleanup — no leaked compile worker (codex M4/M6)
139
138
  expect(children[0].kill, 'a server-start failure must kill the already-forked compile worker — today the error propagates but the '
140
139
  + 'worker (a whole compiler process) is leaked on every failed open').toHaveBeenCalled();
141
140
  }, 15_000);
142
- it("M6: a watcher 'error' before 'ready' rejects openProject within a bounded time and kills the worker (no eternal hang)", async () => {
141
+ it("a watcher 'error' before 'ready' rejects openProject within a bounded time and kills the worker (no eternal hang)", async () => {
143
142
  const root = makeFixture();
144
143
  mocks.watch.mockReset();
145
144
  mocks.watch.mockImplementation(() => {
@@ -65,7 +65,7 @@ describe('openProject onLog — first compile (integration, real dmcc)', () => {
65
65
  ...logOpts,
66
66
  });
67
67
  openSessions.push(session);
68
- expect(entries.length, 'openProject must wire opts.onLog through withCapturedStdio around the first build() — no lines were captured').toBeGreaterThan(0);
68
+ expect(entries.length, 'openProject must wire opts.onLog around the first build() — no lines were captured').toBeGreaterThan(0);
69
69
  const texts = entries.map(entry => entry.text);
70
70
  expect(texts.some(text => /esbuild 转换失败/.test(text)), 'the `[logic] esbuild 转换失败 <file>` line is the highest-value error line and must be kept').toBe(true);
71
71
  expect(texts.some(text => /^✖ /.test(text)), '`✖ …[FAILED: …]` listr failure lines must be kept').toBe(true);
@@ -124,7 +124,7 @@ describe('openProject onLog — watcher rebuild (integration, real dmcc)', () =>
124
124
  // Count-agnostic (entries.length > 0): re-write until the rebuild lands so
125
125
  // a dropped inotify event under CI load can't hang `await rebuilt`.
126
126
  await writeUntilSettled(rebuilt, path.join(root, 'pages', 'index', 'index.js'), attempt => `Page({ data: { msg: "updated-${attempt}" } })\n`);
127
- expect(entries.length, 'the rebuild() build() call must ALSO be wrapped with withCapturedStdio — no rebuild lines reached onLog').toBeGreaterThan(0);
127
+ expect(entries.length, 'the rebuild() build() call must ALSO deliver to onLog — no rebuild lines reached onLog').toBeGreaterThan(0);
128
128
  const texts = entries.map(entry => entry.text);
129
129
  expect(texts.some(text => /^✔ /.test(text)), 'rebuild stage-completion lines must be kept').toBe(true);
130
130
  expectNoNoise(texts);
@@ -120,10 +120,10 @@ describe('createRebuildScheduler — saves during an in-flight build must not be
120
120
  expect(run).toHaveBeenCalledTimes(2);
121
121
  });
122
122
  });
123
- // ─── ROUND 2 — `run` throwing SYNCHRONOUSLY (not rejecting) ──────────────────
123
+ // ─── `run` throwing SYNCHRONOUSLY (not rejecting) ──────────────────
124
124
  //
125
- // Regression tests for the wedge found in review: `start()` sets
126
- // `running = true` and then calls `run()` bare. If `run` throws synchronously
125
+ // Regression tests for a wedge: `start()` sets `running = true` and then calls
126
+ // `run()` bare. If `run` throws synchronously
127
127
  // (e.g. a config/IO error raised before the build's first await — no promise
128
128
  // ever exists), the `.catch().then()` settle chain is never attached, so:
129
129
  // - the exception escapes through `schedule()` to the watcher callsite,
@@ -135,7 +135,7 @@ describe('createRebuildScheduler — saves during an in-flight build must not be
135
135
  // drops a pending dirty flag", with failures being `run`'s responsibility to
136
136
  // report) must hold regardless of HOW the run fails: rejection and synchronous
137
137
  // throw are the same event to the scheduler.
138
- describe('ROUND 2 — createRebuildScheduler: run() throwing synchronously must not wedge the scheduler', () => {
138
+ describe('createRebuildScheduler: run() throwing synchronously must not wedge the scheduler', () => {
139
139
  /** A `run` that throws synchronously on the i-th call(s), resolves otherwise. */
140
140
  function makeSyncThrowingRun(throwOnCalls) {
141
141
  const run = vi.fn(() => {
@@ -1 +1 @@
1
- {"version":3,"file":"watch-rebuild.testutil.d.ts","sourceRoot":"","sources":["../src/watch-rebuild.testutil.ts"],"names":[],"mappings":"AA+BA;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CACtC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,EACzB,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GACrC,OAAO,CAAC,IAAI,CAAC,CAoBf;AAED;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,CACxC,SAAS,EAAE,MAAM,OAAO,EACxB,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GACrC,OAAO,CAAC,IAAI,CAAC,CAYf"}
1
+ {"version":3,"file":"watch-rebuild.testutil.d.ts","sourceRoot":"","sources":["../src/watch-rebuild.testutil.ts"],"names":[],"mappings":"AA8BA;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CACtC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,EACzB,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GACrC,OAAO,CAAC,IAAI,CAAC,CAoBf;AAED;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,CACxC,SAAS,EAAE,MAAM,OAAO,EACxB,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GACrC,OAAO,CAAC,IAAI,CAAC,CAYf"}
@@ -10,8 +10,7 @@ import fs from 'node:fs';
10
10
  * watch. A single `fs.writeFileSync` produces a single inotify `change` event,
11
11
  * and under CI load (Linux inotify + a concurrent REAL dmcc compile in
12
12
  * open-project-compile-log.test.ts pegging the CPU) chokidar can DROP that lone
13
- * event — the rebuild never fires and the test waits forever (PR #44's 30s
14
- * timeout at compile-worker.test.ts:297).
13
+ * event — the rebuild never fires and the test waits forever until timeout.
15
14
  *
16
15
  * The fix is NOT a longer timeout (the event is lost, not slow): re-issue the
17
16
  * filesystem write — with micro-varied content so size+mtime change and a fresh
@@ -20,8 +19,8 @@ import fs from 'node:fs';
20
19
  * write into exactly ONE trailing build, so re-writing is safe for the
21
20
  * count-agnostic / `>=` assertions these helpers are applied to.
22
21
  *
23
- * ⚠️ This is flake hardening, NOT goalpost-moving: no assertion is touched.
24
- * Only apply these helpers where the assertion is count-agnostic or `>=`
22
+ * This is flake hardening only: no assertion is touched. Only apply these
23
+ * helpers where the assertion is count-agnostic or `>=`
25
24
  * (re-triggering an extra coalesced rebuild cannot change the outcome). Tests
26
25
  * that pin an EXACT rebuild/build-send count must not use them.
27
26
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dimina-kit/devkit",
3
- "version": "0.1.2-dev.20260616102751",
3
+ "version": "0.1.2-dev.20260624040247",
4
4
  "description": "Development toolkit for Dimina mini-apps with H5 container preview",
5
5
  "keywords": [
6
6
  "dimina",
@@ -49,6 +49,7 @@
49
49
  ],
50
50
  "devDependencies": {
51
51
  "@types/node": "^22.15.3",
52
+ "@vitest/coverage-v8": "^4.1.4",
52
53
  "eslint": "^10.2.1",
53
54
  "typescript": "5.9.2",
54
55
  "vitest": "^4.1.4",
@@ -71,6 +72,7 @@
71
72
  "check-types": "tsc --noEmit",
72
73
  "lint": "eslint . --max-warnings 0",
73
74
  "test": "vitest run",
74
- "test:dev": "vitest"
75
+ "test:dev": "vitest",
76
+ "test:coverage": "vitest run --coverage"
75
77
  }
76
78
  }