chore(mesh): drop mesh_stacks table on pilot-mode DBs (#1085)

Per the C-3 design, mesh state lives on central. Pilots learn aliases
via the D-1 override push and hold them in `pilotAliasOverlay`. The
local `mesh_stacks` table on pilots has been dead since C-3 shipped.

This is a greenfield cleanup (Directive 20):

- `migrateMeshTables()` runs `DROP TABLE IF EXISTS mesh_stacks` when
  `SENCHO_MODE === 'pilot'` and skips the CREATE. Central-mode
  behavior is unchanged.
- The four mesh CRUD methods (`listMeshStacks`, `isMeshStackEnabled`,
  `insertMeshStack`, `deleteMeshStack`) short-circuit on the same
  check. Reads early-return empty/false; writes early-return;
  `insertMeshStack` also warns so an accidental pilot-side write is
  loud.
- New `isPilotMode()` helper mirrors the existing one in
  `bootstrap/startup.ts`. Layering rules forbid the shared import; a
  third call-site would justify extraction to `helpers/`.

Operator-visible behavior is unchanged on either side. On central, the
table and its rows are untouched. On pilots, the table is removed (it
held no rows in steady state) and any caller of the CRUD methods
continues to see empty list / false / no-op without touching SQLite.

Test plan

- `npx tsc --noEmit`: clean.
- New `database-service-pilot-mesh-stacks.test.ts`: 5/5 green (table
  absence + four short-circuit assertions).
- `mesh-service.test.ts`, `mesh-diagnostic-local.test.ts`, and
  `mesh-service-proactive-bootstrap-fanout.test.ts`: 58/58 green.
- Full backend suite: 2280/2281; the single failure is a pre-existing
  Windows EBUSY flake in `filesystem-backup.test.ts` that reproduces
  on a clean tree.
- Code reviewed; findings applied.
This commit is contained in:
Anso
2026-05-17 05:25:00 -04:00
committed by GitHub
parent 3f620591c8
commit 50b89db3b8
2 changed files with 108 additions and 13 deletions
@@ -0,0 +1,76 @@
/**
* Regression guard for F-A8: on pilot-mode hosts, `mesh_stacks` is dropped
* at boot and the CRUD methods short-circuit.
*
* - Per the C-3 design, mesh state lives on central. Pilots learn aliases
* via the D-1 override push and hold them in MeshService.pilotAliasOverlay.
* They never write to the local DB; the table is dead on pilots.
*
* - DatabaseService runs `DROP TABLE IF EXISTS mesh_stacks` in
* `migrateMeshTables()` when `SENCHO_MODE === 'pilot'` and skips the
* CREATE. The four CRUD methods (`listMeshStacks`, `isMeshStackEnabled`,
* `insertMeshStack`, `deleteMeshStack`) short-circuit on the same check
* so the dropped table is never queried.
*
* - Central-mode behavior is unchanged: every other mesh test in the suite
* (mesh-service.test.ts, mesh-diagnostic-local.test.ts, ...) exercises
* the table normally with `SENCHO_MODE` unset and serves as the implicit
* negative control.
*
* The test sets `SENCHO_MODE=pilot` BEFORE `setupTestDb` so the singleton's
* constructor runs `migrateMeshTables()` in pilot mode against the freshly
* copied baseline DB. The gate-function assertions then exercise the runtime
* short-circuits.
*/
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { setupTestDb, cleanupTestDb } from './helpers/setupTestDb';
let tmpDir: string;
let DatabaseService: typeof import('../services/DatabaseService').DatabaseService;
beforeAll(async () => {
process.env.SENCHO_MODE = 'pilot';
tmpDir = await setupTestDb();
({ DatabaseService } = await import('../services/DatabaseService'));
});
afterAll(() => {
delete process.env.SENCHO_MODE;
cleanupTestDb(tmpDir);
});
describe('DatabaseService mesh_stacks lifecycle on pilot-mode hosts (F-A8)', () => {
it('drops the mesh_stacks table during migration on pilot mode', () => {
const db = DatabaseService.getInstance();
const row = db.getDb().prepare(
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'mesh_stacks'"
).get();
expect(row).toBeUndefined();
});
it('listMeshStacks returns [] without querying the dropped table', () => {
const db = DatabaseService.getInstance();
expect(db.listMeshStacks()).toEqual([]);
expect(db.listMeshStacks(1)).toEqual([]);
});
it('isMeshStackEnabled returns false without querying the dropped table', () => {
const db = DatabaseService.getInstance();
expect(db.isMeshStackEnabled(1, 'any-stack')).toBe(false);
});
it('insertMeshStack is a no-op (no row written, no SQL touched)', () => {
const db = DatabaseService.getInstance();
expect(() => db.insertMeshStack(1, 'guarded-stack', 'tester')).not.toThrow();
expect(db.listMeshStacks(1)).toEqual([]);
const tableStillAbsent = db.getDb().prepare(
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'mesh_stacks'"
).get();
expect(tableStillAbsent).toBeUndefined();
});
it('deleteMeshStack is a no-op', () => {
const db = DatabaseService.getInstance();
expect(() => db.deleteMeshStack(1, 'any-stack')).not.toThrow();
});
});
+32 -13
View File
@@ -4,6 +4,10 @@ import fs from 'fs';
import { CryptoService } from './CryptoService';
import { isSeverityAtLeast } from '../utils/severity';
function isPilotMode(): boolean {
return process.env.SENCHO_MODE === 'pilot';
}
export interface Agent {
id?: number;
type: 'discord' | 'slack' | 'webhook';
@@ -1462,20 +1466,28 @@ export class DatabaseService {
private migrateMeshTables(): void {
try {
this.db.prepare(`
CREATE TABLE IF NOT EXISTS mesh_stacks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id INTEGER NOT NULL,
stack_name TEXT NOT NULL,
created_at INTEGER NOT NULL,
created_by TEXT,
UNIQUE(node_id, stack_name),
FOREIGN KEY (node_id) REFERENCES nodes(id) ON DELETE CASCADE
)
`).run();
this.db.prepare('CREATE INDEX IF NOT EXISTS idx_mesh_stacks_node ON mesh_stacks(node_id)').run();
if (isPilotMode()) {
// Per C-3 design, mesh state lives on central. Pilots never write
// to mesh_stacks; alias data arrives via the D-1 override push
// and lives in MeshService.pilotAliasOverlay. Drop any leftover
// rows from a prior central-mode boot and skip the CREATE.
this.db.prepare('DROP TABLE IF EXISTS mesh_stacks').run();
} else {
this.db.prepare(`
CREATE TABLE IF NOT EXISTS mesh_stacks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id INTEGER NOT NULL,
stack_name TEXT NOT NULL,
created_at INTEGER NOT NULL,
created_by TEXT,
UNIQUE(node_id, stack_name),
FOREIGN KEY (node_id) REFERENCES nodes(id) ON DELETE CASCADE
)
`).run();
this.db.prepare('CREATE INDEX IF NOT EXISTS idx_mesh_stacks_node ON mesh_stacks(node_id)').run();
}
} catch (e) {
console.warn('[DatabaseService] Could not create mesh_stacks:', (e as Error).message);
console.warn('[DatabaseService] mesh_stacks migration:', (e as Error).message);
}
try {
this.db.prepare(`
@@ -1600,6 +1612,7 @@ export class DatabaseService {
// --- Sencho Mesh ---
public listMeshStacks(nodeId?: number): Array<{ id: number; node_id: number; stack_name: string; created_at: number; created_by: string | null }> {
if (isPilotMode()) return [];
const sql = nodeId !== undefined
? 'SELECT id, node_id, stack_name, created_at, created_by FROM mesh_stacks WHERE node_id = ?'
: 'SELECT id, node_id, stack_name, created_at, created_by FROM mesh_stacks';
@@ -1610,17 +1623,23 @@ export class DatabaseService {
}
public isMeshStackEnabled(nodeId: number, stackName: string): boolean {
if (isPilotMode()) return false;
const row = this.db.prepare('SELECT 1 FROM mesh_stacks WHERE node_id = ? AND stack_name = ?').get(nodeId, stackName);
return !!row;
}
public insertMeshStack(nodeId: number, stackName: string, createdBy: string | null): void {
if (isPilotMode()) {
console.warn(`[DatabaseService] insertMeshStack ignored on pilot (node=${nodeId}, stack=${stackName})`);
return;
}
this.db.prepare(
'INSERT INTO mesh_stacks (node_id, stack_name, created_at, created_by) VALUES (?, ?, ?, ?)'
).run(nodeId, stackName, Date.now(), createdBy);
}
public deleteMeshStack(nodeId: number, stackName: string): void {
if (isPilotMode()) return;
this.db.prepare('DELETE FROM mesh_stacks WHERE node_id = ? AND stack_name = ?').run(nodeId, stackName);
}