Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46d32b3553 | ||
|
|
e20e11e2d5 | ||
|
|
0b20df242c | ||
|
|
a16b6c19d4 |
@@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v0.21.8 — 2026-09-27 07:13
|
||||||
|
|
||||||
|
- Fixed `npm test` exiting 1 with "Errors 2" even though all tests pass (every CI run red since v0.21.2): the two watchdog specs that force a reconnect destroy the service's socket while data is still pending in its receive buffer (the overflow spec floods 300 KB and destroys on the first ~64 KB chunk; the garbage spec destroys right after its last write), so the kernel sends RST instead of FIN and the fake bridge's accepted server-side sockets emit `read ECONNRESET`. Those test-owned sockets had no `'error'` handler, so Node threw it as an uncaught exception that vitest counts as a run-level failure. `startBridge()` now attaches a no-op `'error'` handler to every accepted socket (single registration point, covers all four spec files using the fake bridge). Test-only change — `fake-bridge.ts` is imported by specs only; no runtime diff, no deploy needed.
|
||||||
|
|
||||||
|
## v0.21.7 — 2026-09-27 06:50
|
||||||
|
|
||||||
|
- Fixed double registration of scheduled jobs: `ScheduleModule.forRoot()` was called from both `WeatherModule` and `BatteryAlertModule`, so Nest created two ScheduleExplorers, each scanning all providers — every @Cron/@Interval job (hourly weather broadcast, per-minute battery check) fired twice. Removed the call from `battery.module.ts`; the single registration in `weather.module.ts` remains.
|
||||||
|
- Tests: 174 → 175 — new regression spec (`src/schedule-singleton.spec.ts`) asserts `ScheduleModule.forRoot(` appears exactly once across all `src/**/*.module.ts`.
|
||||||
|
|
||||||
## v0.21.6 — 2026-09-26 10:37
|
## v0.21.6 — 2026-09-26 10:37
|
||||||
|
|
||||||
- New `#help` command (owner request): replies with a short list of all available commands as ONE multi-line message (single packet, not one spaced packet per line) — `#ping`, the `#rss help/add/remove/get/sources/tags` family. Registered in the CommandService constructor next to `#ping`.
|
- New `#help` command (owner request): replies with a short list of all available commands as ONE multi-line message (single packet, not one spaced packet per line) — `#ping`, the `#rss help/add/remove/get/sources/tags` family. Registered in the CommandService constructor next to `#ping`.
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mesh-bot",
|
"name": "mesh-bot",
|
||||||
"version": "0.21.6",
|
"version": "0.21.8",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ScheduleModule } from '@nestjs/schedule';
|
|
||||||
import { MeshModule } from '../mesh/mesh.module.js';
|
import { MeshModule } from '../mesh/mesh.module.js';
|
||||||
import { BatteryAlertService } from './battery.service.js';
|
import { BatteryAlertService } from './battery.service.js';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ScheduleModule.forRoot(), MeshModule],
|
imports: [MeshModule],
|
||||||
providers: [BatteryAlertService],
|
providers: [BatteryAlertService],
|
||||||
exports: [BatteryAlertService],
|
exports: [BatteryAlertService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export function startBridge(
|
|||||||
): Promise<{ port: number; sockets: net.Socket[]; close: () => void }> {
|
): Promise<{ port: number; sockets: net.Socket[]; close: () => void }> {
|
||||||
const sockets: net.Socket[] = [];
|
const sockets: net.Socket[] = [];
|
||||||
const server = net.createServer((socket) => {
|
const server = net.createServer((socket) => {
|
||||||
|
// Test-owned socket: the service destroys its end mid-test with unread data pending (overflow/garbage tests), which makes the kernel send RST and this side emit ECONNRESET. Expected teardown noise — without a handler Node throws it as an uncaught exception and vitest fails the whole run.
|
||||||
|
socket.on('error', () => {});
|
||||||
onSocket?.(socket); // attached at accept time, before any data can arrive
|
onSocket?.(socket); // attached at accept time, before any data can arrive
|
||||||
sockets.push(socket);
|
sockets.push(socket);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { readdirSync, readFileSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
const srcDir = fileURLToPath(new URL('.', import.meta.url));
|
||||||
|
|
||||||
|
function moduleFiles(dir: string): string[] {
|
||||||
|
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) =>
|
||||||
|
entry.isDirectory()
|
||||||
|
? moduleFiles(join(dir, entry.name))
|
||||||
|
: entry.name.endsWith('.module.ts')
|
||||||
|
? [join(dir, entry.name)]
|
||||||
|
: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ScheduleModule.forRoot single registration', () => {
|
||||||
|
// Regression guard (v0.21.7): each forRoot() call creates its own ScheduleExplorer that scans all providers, so a second call made every @Cron/@Interval job fire twice.
|
||||||
|
it('appears exactly once across all src/**/*.module.ts', () => {
|
||||||
|
const hits = moduleFiles(srcDir).flatMap((file) =>
|
||||||
|
(
|
||||||
|
readFileSync(file, 'utf8').match(/ScheduleModule\.forRoot\(/g) ?? []
|
||||||
|
).map(() => file.slice(srcDir.length)),
|
||||||
|
);
|
||||||
|
expect(hits).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user