fix(networking): ignore verified Mesh attachments in drift (#1729)

This commit is contained in:
Anso
2026-07-29 12:55:30 -04:00
committed by GitHub
parent 55644cf87d
commit 3c934066f3
14 changed files with 1206 additions and 50 deletions
@@ -4,14 +4,21 @@
* runtime-vs-Compose drift comparison (system/default/external networks and
* stopped containers are not flagged).
*/
import { describe, it, expect } from 'vitest';
import { describe, it, expect, vi } from 'vitest';
import type { EffectiveModel, EffService } from '../services/preflight/effectiveModel';
import type { DeclaredCompose } from '../helpers/composeDependencyParse';
import type { DependencySnapshot, DependencyContainer, DependencyNetwork } from '../services/DockerController';
import {
fromEffectiveModel, fromDeclaredCompose, compareStackNetworks, runtimeResourceName, parseAccessUrlPorts,
type ManagedNetworkAttachmentPredicate,
} from '../services/network/normalize';
import { assembleStackNetworkFacts } from '../services/network/composeNetworkInspector';
import { assembleStackNetworkFacts, buildStackNetworkFacts } from '../services/network/composeNetworkInspector';
import { buildNodeNetworkingFindings } from '../services/network/networkingFindings';
import type { NetworkingNetworkBase } from '../services/network/networkingTypes';
import DockerController from '../services/DockerController';
import { ComposeService } from '../services/ComposeService';
import { DatabaseService } from '../services/DatabaseService';
import { FileSystemService } from '../services/FileSystemService';
function effSvc(over: Partial<EffService> = {}): EffService {
const hasHealthcheck = over.hasHealthcheck ?? true;
@@ -264,6 +271,187 @@ describe('compareStackNetworks', () => {
expect(drift.foreignNetworkAttachments).toEqual([]);
});
it('removes managed Mesh drift before Networking findings while preserving advanced-driver info', () => {
const meshOnlyModel: EffectiveModel = {
projectName: 'myapp',
services: [],
networks: {},
volumes: {},
};
const snap = snapshot(
[container({ networks: [{ name: 'sencho_mesh', id: 'm', ip: '' }] })],
[depNet({ id: 'm', name: 'sencho_mesh', driver: 'macvlan', composeProject: null, stack: null })],
);
const facts = assembleStackNetworkFacts(
'myapp',
meshOnlyModel,
null,
snap,
(_runtimeContainer, networkName) => networkName === 'sencho_mesh',
);
const baseNetworks: NetworkingNetworkBase[] = [{
id: 'm',
name: 'sencho_mesh',
driver: 'macvlan',
scope: 'local',
isSystem: false,
ingress: false,
composeProject: null,
stack: null,
connectedCount: 1,
isSencho: true,
ownership: 'sencho-managed',
declaredByStacks: [],
declaredExternalByStacks: [],
isExternalDependency: false,
}];
const findings = buildNodeNetworkingFindings(1, snap, [facts], baseNetworks);
expect(facts.drift.runtimeOnlyAttachments).toEqual([]);
expect(facts.drift.foreignNetworkAttachments).toEqual([]);
expect(findings.some(f => f.kind === 'network-undeclared' || f.kind === 'foreign-network-attachment')).toBe(false);
expect(findings).toContainEqual(expect.objectContaining({
kind: 'advanced-driver-caveat',
severity: 'info',
network: 'sencho_mesh',
}));
});
it('keeps Networking facts available and Mesh drift actionable when opt-in authority fails', async () => {
const snap = snapshot(
[container({ networks: [{ name: 'sencho_mesh', id: 'm', ip: '' }] })],
[depNet({ name: 'sencho_mesh', composeProject: null, stack: null })],
);
vi.spyOn(ComposeService, 'getInstance').mockReturnValue({
renderConfig: vi.fn().mockResolvedValue({
rendered: JSON.stringify({ name: 'myapp', services: { web: { image: 'nginx:1.27' } } }),
stderr: '',
code: 0,
timedOut: false,
}),
} as unknown as ComposeService);
vi.spyOn(FileSystemService, 'getInstance').mockReturnValue({
getStacks: vi.fn().mockResolvedValue(['myapp']),
} as unknown as FileSystemService);
vi.spyOn(DockerController, 'getInstance').mockReturnValue({
getDependencySnapshot: vi.fn().mockResolvedValue(snap),
} as unknown as DockerController);
vi.spyOn(DatabaseService, 'getInstance').mockImplementation(() => {
throw new Error('database unavailable');
});
vi.spyOn(console, 'warn').mockImplementation(() => undefined);
try {
const facts = await buildStackNetworkFacts(1, 'myapp');
expect(facts.runtime).toBe('available');
expect(facts.drift.foreignNetworkAttachments).toEqual([{ container: 'web1', network: 'sencho_mesh' }]);
} finally {
vi.restoreAllMocks();
}
});
it('threads opted-in authority through Networking facts and preserves advanced-driver info', async () => {
const snap = snapshot(
[container({ networks: [{ name: 'sencho_mesh', id: 'm', ip: '' }] })],
[depNet({ id: 'm', name: 'sencho_mesh', driver: 'macvlan', composeProject: null, stack: null })],
);
vi.spyOn(ComposeService, 'getInstance').mockReturnValue({
renderConfig: vi.fn().mockResolvedValue({
rendered: JSON.stringify({ name: 'myapp', services: { web: { image: 'nginx:1.27' } } }),
stderr: '',
code: 0,
timedOut: false,
}),
} as unknown as ComposeService);
vi.spyOn(FileSystemService, 'getInstance').mockReturnValue({
getStacks: vi.fn().mockResolvedValue(['myapp']),
} as unknown as FileSystemService);
vi.spyOn(DockerController, 'getInstance').mockReturnValue({
getDependencySnapshot: vi.fn().mockResolvedValue(snap),
} as unknown as DockerController);
vi.spyOn(DatabaseService, 'getInstance').mockReturnValue({
isMeshStackEnabled: vi.fn().mockReturnValue(true),
getStackExposureIntents: vi.fn().mockReturnValue([]),
getStackDossier: vi.fn().mockReturnValue(null),
} as unknown as DatabaseService);
try {
const facts = await buildStackNetworkFacts(1, 'myapp');
const baseNetworks: NetworkingNetworkBase[] = [{
id: 'm', name: 'sencho_mesh', driver: 'macvlan', scope: 'local', isSystem: false,
ingress: false, composeProject: null, stack: null, connectedCount: 1, isSencho: true,
ownership: 'sencho-managed', declaredByStacks: [], declaredExternalByStacks: [],
isExternalDependency: false,
}];
const findings = buildNodeNetworkingFindings(1, snap, [facts], baseNetworks);
expect(facts.drift.runtimeOnlyAttachments).toEqual([]);
expect(facts.drift.foreignNetworkAttachments).toEqual([]);
expect(findings.filter(f => f.kind === 'network-undeclared' || f.kind === 'foreign-network-attachment')).toEqual([]);
expect(findings).toContainEqual(expect.objectContaining({
kind: 'advanced-driver-caveat',
severity: 'info',
network: 'sencho_mesh',
}));
} finally {
vi.restoreAllMocks();
}
});
it('ignores a verified Sencho Mesh attachment for the Sencho container', () => {
const snap = snapshot(
[container({ id: 'sencho-id', name: 'sencho', service: 'sencho', networks: [{ name: 'sencho_mesh', id: 'm', ip: '' }] })],
[depNet({ name: 'sencho_mesh', composeProject: null, stack: null })],
);
const managed: ManagedNetworkAttachmentPredicate = (runtimeContainer, networkName) =>
runtimeContainer.id === 'sencho-id' && networkName === 'sencho_mesh';
const drift = compareStackNetworks(declared, snap, 'myapp', managed);
expect(drift.runtimeOnlyAttachments).toEqual([]);
expect(drift.foreignNetworkAttachments).toEqual([]);
});
it('ignores a verified Mesh attachment for an opted-in application stack', () => {
const snap = snapshot(
[container({ networks: [{ name: 'sencho_mesh', id: 'm', ip: '' }] })],
[depNet({ name: 'sencho_mesh', composeProject: null, stack: null })],
);
const managed: ManagedNetworkAttachmentPredicate = (_runtimeContainer, networkName) =>
networkName === 'sencho_mesh';
const drift = compareStackNetworks(declared, snap, 'myapp', managed);
expect(drift.runtimeOnlyAttachments).toEqual([]);
expect(drift.foreignNetworkAttachments).toEqual([]);
});
it('keeps an unverified manual Mesh attachment actionable', () => {
const snap = snapshot(
[container({ networks: [{ name: 'sencho_mesh', id: 'm', ip: '' }] })],
[depNet({ name: 'sencho_mesh', composeProject: null, stack: null })],
);
const drift = compareStackNetworks(declared, snap, 'myapp', () => false);
expect(drift.runtimeOnlyAttachments).toEqual([]);
expect(drift.foreignNetworkAttachments).toEqual([{ container: 'web1', network: 'sencho_mesh' }]);
});
it('keeps sencho_extra actionable even when the stack is Mesh-managed', () => {
const snap = snapshot(
[container({ networks: [{ name: 'sencho_extra', id: 'e', ip: '' }] })],
[depNet({ name: 'sencho_extra', composeProject: null, stack: null })],
);
const drift = compareStackNetworks(declared, snap, 'myapp', () => true);
expect(drift.runtimeOnlyAttachments).toEqual([]);
expect(drift.foreignNetworkAttachments).toEqual([{ container: 'web1', network: 'sencho_extra' }]);
});
it('does not flag attachments from stopped containers', () => {
const snap = snapshot(
[container({ state: 'exited', networks: [{ name: 'myapp_extra', id: 'b', ip: '' }] })],
@@ -18,6 +18,7 @@ import { declaredFromEffectiveModel } from '../helpers/effectiveToDeclaredCompos
import type { DeclaredCompose, DeclaredService, DeclaredPort } from '../helpers/composeDependencyParse';
import type { EffectiveModel, EffService } from '../services/preflight/effectiveModel';
import { fromDeclaredCompose, fromEffectiveModel } from '../services/network/normalize';
import { DatabaseService } from '../services/DatabaseService';
// ── builders ────────────────────────────────────────────────────────────
@@ -580,6 +581,32 @@ describe('assembleStackDrift - network drift', () => {
expect(report.findings.filter(f => f.kind.startsWith('network-'))).toEqual([]);
expect(report.status).toBe('in-sync');
});
it('reports in-sync when a verified Mesh attachment is the only runtime difference', () => {
const report = assembleStackDrift({
stack: 'app',
declared: declared([service({ name: 'web' })]),
containers: [container({ id: 'c1', service: 'web', networks: [{ name: 'sencho_mesh', id: 'm', ip: '' }] })],
networks: [depNet('sencho_mesh', { composeProject: null, stack: null })],
managedNetworkAttachment: (_runtimeContainer, networkName) => networkName === 'sencho_mesh',
});
expect(report.status).toBe('in-sync');
expect(report.findings.filter(f => f.kind === 'network-undeclared')).toEqual([]);
});
it('keeps an unverified manual Mesh attachment drifted', () => {
const report = assembleStackDrift({
stack: 'app',
declared: declared([service({ name: 'web' })]),
containers: [container({ id: 'c1', service: 'web', networks: [{ name: 'sencho_mesh', id: 'm', ip: '' }] })],
networks: [depNet('sencho_mesh', { composeProject: null, stack: null })],
managedNetworkAttachment: () => false,
});
expect(report.status).toBe('drifted');
expect(report.findings.filter(f => f.kind === 'network-undeclared')).toHaveLength(1);
});
});
// ── declaredFromEffectiveModel ─────────────────────────────────────────────
@@ -815,4 +842,46 @@ describe('buildStackDriftReport - boundaries', () => {
expect(findingKinds(report)).toContain('network-undeclared');
vi.restoreAllMocks();
});
it('keeps the report available and Mesh drift actionable when opt-in authority fails', async () => {
const snapshot: DependencySnapshot = {
containers: [container({ id: 'c1', service: 'web', stack: 'app', image: 'nginx:1.25', networks: [{ name: 'sencho_mesh', id: 'm', ip: '' }] })],
networks: [depNet('sencho_mesh', { composeProject: null, stack: null })],
volumes: [],
};
stubDockerRender({ name: 'app', services: { web: { image: 'nginx:1.25' } } });
stubFsAndSnapshot(snapshot);
vi.spyOn(DatabaseService, 'getInstance').mockImplementation(() => {
throw new Error('database unavailable');
});
vi.spyOn(console, 'warn').mockImplementation(() => undefined);
const report = await buildStackDriftReport(0, 'app');
expect(report.status).toBe('drifted');
expect(report.findings).toContainEqual(expect.objectContaining({
kind: 'network-undeclared',
actual: 'sencho_mesh',
}));
vi.restoreAllMocks();
});
it('reports in-sync through the public builder when DB authority opts the stack into Mesh', async () => {
const snapshot: DependencySnapshot = {
containers: [container({ id: 'c1', service: 'web', stack: 'app', image: 'nginx:1.25', networks: [{ name: 'sencho_mesh', id: 'm', ip: '' }] })],
networks: [depNet('sencho_mesh', { composeProject: null, stack: null })],
volumes: [],
};
stubDockerRender({ name: 'app', services: { web: { image: 'nginx:1.25' } } });
stubFsAndSnapshot(snapshot);
vi.spyOn(DatabaseService, 'getInstance').mockReturnValue({
isMeshStackEnabled: vi.fn().mockReturnValue(true),
} as unknown as DatabaseService);
const report = await buildStackDriftReport(0, 'app');
expect(report.status).toBe('in-sync');
expect(report.findings.filter(f => f.kind === 'network-undeclared')).toEqual([]);
vi.restoreAllMocks();
});
});
@@ -0,0 +1,124 @@
import fs from 'fs/promises';
import os from 'os';
import path from 'path';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { DatabaseService } from '../services/DatabaseService';
import SelfIdentityService from '../services/SelfIdentityService';
import type { DependencyContainer } from '../services/DockerController';
import { resolveManagedMeshAttachment } from '../services/network/managedMeshAttachment';
const originalDataDir = process.env.DATA_DIR;
const originalMode = process.env.SENCHO_MODE;
let tempDir: string | null = null;
function runtimeContainer(overrides: Partial<DependencyContainer> = {}): DependencyContainer {
return {
id: 'app-id',
name: 'app-web-1',
service: 'web',
composeProject: 'app',
stack: 'app',
state: 'running',
exitCode: null,
image: 'nginx:latest',
networks: [],
volumes: [],
ports: [],
...overrides,
};
}
function stubAuthorities(meshStackEnabled: boolean, ownContainers: string[] = []): void {
vi.spyOn(DatabaseService, 'getInstance').mockReturnValue({
isMeshStackEnabled: vi.fn().mockReturnValue(meshStackEnabled),
} as unknown as DatabaseService);
vi.spyOn(SelfIdentityService, 'getInstance').mockReturnValue({
isOwnContainer: vi.fn((idOrName: string) => ownContainers.includes(idOrName)),
} as unknown as SelfIdentityService);
}
afterEach(async () => {
vi.restoreAllMocks();
if (originalDataDir === undefined) delete process.env.DATA_DIR;
else process.env.DATA_DIR = originalDataDir;
if (originalMode === undefined) delete process.env.SENCHO_MODE;
else process.env.SENCHO_MODE = originalMode;
if (tempDir) await fs.rm(tempDir, { recursive: true, force: true });
tempDir = null;
});
describe('resolveManagedMeshAttachment', () => {
it('authorizes the canonical Mesh attachment for a centrally opted-in stack', async () => {
process.env.SENCHO_MODE = 'server';
stubAuthorities(true);
const isManaged = await resolveManagedMeshAttachment(1, 'app');
expect(isManaged(runtimeContainer(), 'sencho_mesh')).toBe(true);
expect(isManaged(runtimeContainer(), 'sencho_extra')).toBe(false);
});
it('authorizes the canonical Mesh attachment for the actual Sencho container', async () => {
process.env.SENCHO_MODE = 'server';
stubAuthorities(false, ['sencho-id']);
const isManaged = await resolveManagedMeshAttachment(1, 'sencho');
expect(isManaged(runtimeContainer({ id: 'sencho-id', name: 'sencho' }), 'sencho_mesh')).toBe(true);
expect(isManaged(runtimeContainer(), 'sencho_mesh')).toBe(false);
});
it('keeps a manual Mesh attachment actionable for an opted-out stack', async () => {
process.env.SENCHO_MODE = 'server';
stubAuthorities(false);
const isManaged = await resolveManagedMeshAttachment(1, 'app');
expect(isManaged(runtimeContainer(), 'sencho_mesh')).toBe(false);
});
it('uses Pilot override presence as the authoritative opt-in representation', async () => {
process.env.SENCHO_MODE = 'pilot';
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'sencho-mesh-drift-'));
process.env.DATA_DIR = tempDir;
const overrideDir = path.join(tempDir, 'mesh', 'overrides', '7');
await fs.mkdir(overrideDir, { recursive: true });
await fs.writeFile(path.join(overrideDir, 'app.override.yml'), 'services: {}\n');
stubAuthorities(false);
const isManaged = await resolveManagedMeshAttachment(7, 'app');
expect(isManaged(runtimeContainer(), 'sencho_mesh')).toBe(true);
});
it('does not let stale server override presence supersede opted-out DB state', async () => {
process.env.SENCHO_MODE = 'server';
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'sencho-mesh-drift-'));
process.env.DATA_DIR = tempDir;
const overrideDir = path.join(tempDir, 'mesh', 'overrides', '1');
await fs.mkdir(overrideDir, { recursive: true });
await fs.writeFile(path.join(overrideDir, 'app.override.yml'), 'services: {}\n');
stubAuthorities(false);
const isManaged = await resolveManagedMeshAttachment(1, 'app');
expect(isManaged(runtimeContainer(), 'sencho_mesh')).toBe(false);
});
it('fails closed when Mesh opt-in state cannot be read', async () => {
process.env.SENCHO_MODE = 'server';
vi.spyOn(DatabaseService, 'getInstance').mockImplementation(() => {
throw new Error('database unavailable');
});
vi.spyOn(SelfIdentityService, 'getInstance').mockReturnValue({
isOwnContainer: vi.fn().mockReturnValue(false),
} as unknown as SelfIdentityService);
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
const isManaged = await resolveManagedMeshAttachment(1, 'app');
expect(isManaged(runtimeContainer(), 'sencho_mesh')).toBe(false);
expect(warn).toHaveBeenCalledWith(
'[NetworkDrift] Could not verify Mesh opt-in state for %s:',
'app',
'database unavailable',
);
});
});
@@ -96,7 +96,7 @@ describe('MeshService.removeOverrideFromNode (remote dispatch)', () => {
db.deleteNode(remoteNodeId);
});
it('swallows network errors from the remote so the disable cascade can continue', async () => {
it('rejects network errors so callers can preserve authoritative opt-in state', async () => {
const svc = MeshService.getInstance();
const db = DatabaseService.getInstance();
const remoteNodeId = db.addNode({
@@ -117,11 +117,37 @@ describe('MeshService.removeOverrideFromNode (remote dispatch)', () => {
vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('ECONNREFUSED'));
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { /* silence */ });
// Must not throw: the remote being offline is a tolerable condition;
// the cascade upstream uses Promise.allSettled and continues.
await expect(svc.removeOverrideFromNode(remoteNodeId, 'sample-stack')).resolves.toBeUndefined();
await expect(svc.removeOverrideFromNode(remoteNodeId, 'sample-stack')).rejects.toThrow('ECONNREFUSED');
expect(warnSpy).toHaveBeenCalled();
db.deleteNode(remoteNodeId);
});
it('rejects a non-success response from a busy remote target', async () => {
const svc = MeshService.getInstance();
const db = DatabaseService.getInstance();
const remoteNodeId = db.addNode({
name: 'remove-override-busy-test',
type: 'remote',
mode: 'proxy',
compose_dir: '/tmp',
is_default: false,
api_url: 'https://remote.example.com:1852',
api_token: 'remote-tok',
});
vi.spyOn(NodeRegistry.getInstance(), 'getProxyTarget').mockReturnValue({
apiUrl: 'https://remote.example.com:1852',
apiToken: 'remote-tok',
});
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('another operation is already in progress', { status: 500 }),
);
vi.spyOn(console, 'warn').mockImplementation(() => { /* silence */ });
await expect(svc.removeOverrideFromNode(remoteNodeId, 'sample-stack'))
.rejects.toThrow('HTTP 500');
db.deleteNode(remoteNodeId);
});
});
+382
View File
@@ -1,6 +1,7 @@
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import { EventEmitter } from 'events';
import fsSync from 'fs';
import fs from 'fs/promises';
import path from 'path';
import { setupTestDb, cleanupTestDb } from './helpers/setupTestDb';
import { getSenchoIpFromSubnet, MeshError, type MeshTarget, type MeshTcpStreamLike } from '../services/MeshService';
@@ -20,6 +21,7 @@ afterAll(() => {
});
beforeEach(() => {
process.env.SENCHO_MODE = 'server';
const db = DatabaseService.getInstance().getDb();
db.prepare('DELETE FROM mesh_stacks').run();
db.prepare('DELETE FROM nodes WHERE is_default = 0').run();
@@ -106,6 +108,84 @@ describe('MeshService.optInStack', () => {
expect(db.isMeshStackEnabled(localNodeId, 'api')).toBe(false);
});
it('restores opt-in authority when target override removal is rejected', async () => {
const svc = MeshService.getInstance();
const db = DatabaseService.getInstance();
const localNodeId = db.getNodes()[0].id;
db.insertMeshStack(localNodeId, 'busy-stack', 'setup');
vi.spyOn(svc, 'removeOverrideFromNode').mockRejectedValue(
new Error('HTTP 500: another operation is already in progress'),
);
await expect(svc.optOutStack(localNodeId, 'busy-stack', 'tester'))
.rejects.toThrow('another operation is already in progress');
expect(db.isMeshStackEnabled(localNodeId, 'busy-stack')).toBe(true);
});
it('serializes concurrent opt-out requests for the same node', async () => {
const svc = MeshService.getInstance();
const db = DatabaseService.getInstance();
const localNodeId = db.getNodes()[0].id;
db.insertMeshStack(localNodeId, 'queued-stack', 'setup');
let releaseRemoval!: () => void;
const removalPending = new Promise<void>((resolve) => {
releaseRemoval = resolve;
});
const removeSpy = vi.spyOn(svc, 'removeOverrideFromNode').mockReturnValue(removalPending);
vi.spyOn(svc as unknown as { regenerateOverridesAcrossFleet: () => Promise<void> }, 'regenerateOverridesAcrossFleet')
.mockResolvedValue(undefined);
vi.spyOn(svc as unknown as { cascadeRecomposeAcrossFleet: () => void }, 'cascadeRecomposeAcrossFleet')
.mockImplementation(() => { /* noop */ });
vi.spyOn(svc, 'triggerRedeploy').mockImplementation(() => { /* noop */ });
const first = svc.optOutStack(localNodeId, 'queued-stack', 'tester');
await vi.waitFor(() => expect(removeSpy).toHaveBeenCalledTimes(1));
const second = svc.optOutStack(localNodeId, 'queued-stack', 'tester');
await Promise.resolve();
expect(removeSpy).toHaveBeenCalledTimes(1);
releaseRemoval();
await Promise.all([first, second]);
expect(removeSpy).toHaveBeenCalledTimes(1);
expect(db.isMeshStackEnabled(localNodeId, 'queued-stack')).toBe(false);
});
it('serializes different Mesh mutations per node without blocking another node', async () => {
const svc = MeshService.getInstance();
const db = DatabaseService.getInstance();
const localNodeId = db.getNodes()[0].id;
const remoteNodeId = db.addNode({
name: 'independent-node', type: 'remote', mode: 'pilot_agent',
compose_dir: '/tmp', is_default: false, api_url: '', api_token: '',
});
db.setNodeMeshEnabled(localNodeId, true);
db.insertMeshStack(localNodeId, 'held-stack', 'setup');
let releaseRemoval!: () => void;
const removalPending = new Promise<void>((resolve) => {
releaseRemoval = resolve;
});
vi.spyOn(svc, 'removeOverrideFromNode').mockReturnValue(removalPending);
vi.spyOn(svc as unknown as { regenerateOverridesAcrossFleet: () => Promise<void> }, 'regenerateOverridesAcrossFleet')
.mockResolvedValue(undefined);
vi.spyOn(svc as unknown as { cascadeRecomposeAcrossFleet: () => void }, 'cascadeRecomposeAcrossFleet')
.mockImplementation(() => { /* noop */ });
vi.spyOn(svc, 'triggerRedeploy').mockImplementation(() => { /* noop */ });
const optOut = svc.optOutStack(localNodeId, 'held-stack', 'tester');
await vi.waitFor(() => expect(svc.removeOverrideFromNode).toHaveBeenCalledTimes(1));
const disable = svc.disableForNode(localNodeId, 'tester');
await svc.enableForNode(remoteNodeId);
expect(db.getNodeMeshEnabled(localNodeId)).toBe(true);
expect(db.getNodeMeshEnabled(remoteNodeId)).toBe(true);
releaseRemoval();
await Promise.all([optOut, disable]);
expect(db.getNodeMeshEnabled(localNodeId)).toBe(false);
db.deleteNode(remoteNodeId);
});
it('rejects an invalid stack name (path traversal attempt)', async () => {
const svc = MeshService.getInstance();
const db = DatabaseService.getInstance();
@@ -427,6 +507,44 @@ describe('MeshService.disableForNode', () => {
db.deleteNode(remoteNodeId);
});
it('keeps failed remote stacks authoritative when node disable is incomplete', async () => {
const svc = MeshService.getInstance();
const db = DatabaseService.getInstance();
const remoteNodeId = db.addNode({
name: 'remote-partial-disable', type: 'remote', mode: 'pilot_agent',
compose_dir: '/tmp', is_default: false, api_url: '', api_token: '',
});
db.setNodeMeshEnabled(remoteNodeId, true);
db.insertMeshStack(remoteNodeId, 'removed-stack', 'setup');
db.insertMeshStack(remoteNodeId, 'busy-stack', 'setup');
vi.spyOn(svc, 'removeOverrideFromNode').mockImplementation(async (_nodeId, stackName) => {
if (stackName === 'busy-stack') throw new Error('target busy');
});
vi.spyOn(
svc as unknown as { regenerateOverridesAcrossFleet: () => Promise<void> },
'regenerateOverridesAcrossFleet',
).mockResolvedValue(undefined);
vi.spyOn(
svc as unknown as { cascadeRecomposeAcrossFleet: () => void },
'cascadeRecomposeAcrossFleet',
).mockImplementation(() => { /* noop */ });
const redeployed: string[] = [];
vi.spyOn(svc, 'triggerRedeploy').mockImplementation((_nodeId, stackName) => {
redeployed.push(stackName);
});
vi.spyOn(svc as unknown as { refreshAliasCache: () => Promise<void> }, 'refreshAliasCache')
.mockRejectedValue(new Error('refresh failed'));
vi.spyOn(console, 'warn').mockImplementation(() => { /* silence */ });
await expect(svc.disableForNode(remoteNodeId, 'tester')).rejects.toThrow('busy-stack');
expect(db.getNodeMeshEnabled(remoteNodeId)).toBe(true);
expect(db.isMeshStackEnabled(remoteNodeId, 'removed-stack')).toBe(false);
expect(db.isMeshStackEnabled(remoteNodeId, 'busy-stack')).toBe(true);
expect(redeployed).toContain('removed-stack');
db.deleteNode(remoteNodeId);
});
it('defaults the actor when none is supplied so legacy callers still log a non-empty actor', async () => {
const svc = MeshService.getInstance();
const db = DatabaseService.getInstance();
@@ -673,6 +791,62 @@ describe('MeshService.optInStack rollback', () => {
.rejects.toThrow(/simulated remote pilot offline/);
expect(db.isMeshStackEnabled(localNodeId, 'api')).toBe(false);
});
it('retains remote authority when the push outcome is unknown', async () => {
const svc = MeshService.getInstance();
const db = DatabaseService.getInstance();
const remoteNodeId = db.addNode({
name: 'ambiguous-push', type: 'remote', mode: 'pilot_agent',
compose_dir: '/tmp', is_default: false, api_url: '', api_token: '',
});
vi.spyOn(svc as unknown as { inspectStackServices: (n: number, s: string) => Promise<unknown> }, 'inspectStackServices')
.mockResolvedValue([{ service: 'web', ports: [8080] }]);
vi.spyOn(svc, 'pushOverrideToNode').mockRejectedValue(new Error('connection reset'));
await expect(svc.optInStack(remoteNodeId, 'ambiguous-stack', 'tester'))
.rejects.toThrow('connection reset');
expect(db.isMeshStackEnabled(remoteNodeId, 'ambiguous-stack')).toBe(true);
db.deleteNode(remoteNodeId);
});
it('treats a remote gateway error as an ambiguous push outcome', async () => {
const svc = MeshService.getInstance();
const db = DatabaseService.getInstance();
const remoteNodeId = db.addNode({
name: 'gateway-error-push', type: 'remote', mode: 'pilot_agent',
compose_dir: '/tmp', is_default: false, api_url: '', api_token: '',
});
vi.spyOn(svc as unknown as { inspectStackServices: (n: number, s: string) => Promise<unknown> }, 'inspectStackServices')
.mockResolvedValue([{ service: 'web', ports: [8080] }]);
vi.spyOn(svc as unknown as { proxyFetch: () => Promise<Response> }, 'proxyFetch')
.mockResolvedValue(new Response('gateway timeout', { status: 502 }));
await expect(svc.optInStack(remoteNodeId, 'gateway-error-stack', 'tester'))
.rejects.toThrow('HTTP 502');
expect(db.isMeshStackEnabled(remoteNodeId, 'gateway-error-stack')).toBe(true);
db.deleteNode(remoteNodeId);
});
it('rolls back remote authority when the target explicitly rejects the push', async () => {
const svc = MeshService.getInstance();
const db = DatabaseService.getInstance();
const remoteNodeId = db.addNode({
name: 'rejected-push', type: 'remote', mode: 'pilot_agent',
compose_dir: '/tmp', is_default: false, api_url: '', api_token: '',
});
vi.spyOn(svc as unknown as { inspectStackServices: (n: number, s: string) => Promise<unknown> }, 'inspectStackServices')
.mockResolvedValue([{ service: 'web', ports: [8080] }]);
vi.spyOn(svc, 'pushOverrideToNode')
.mockRejectedValue(new MeshError('push_failed', 'target rejected override'));
await expect(svc.optInStack(remoteNodeId, 'rejected-stack', 'tester'))
.rejects.toThrow('target rejected override');
expect(db.isMeshStackEnabled(remoteNodeId, 'rejected-stack')).toBe(false);
db.deleteNode(remoteNodeId);
});
});
describe('MeshService.optInStack guard rails (network setup)', () => {
@@ -984,6 +1158,7 @@ describe('MeshService.ensureStackOverride (BUG-1 fix)', () => {
const db = DatabaseService.getInstance();
const localNodeId = db.getNodes()[0].id;
process.env.SENCHO_MODE = 'pilot';
// Simulate the pilot scenario: no mesh_stacks row (isMeshStackEnabled → false),
// but the override file already exists on disk, pushed by central via D-1.
const dataDir = process.env.DATA_DIR as string;
@@ -1008,6 +1183,184 @@ describe('MeshService.ensureStackOverride (BUG-1 fix)', () => {
// Cleanup.
fsSync.unlinkSync(overrideFile);
process.env.SENCHO_MODE = 'server';
});
it('persists and removes proxy-target opt-in state with a pushed local override', async () => {
const svc = MeshService.getInstance();
const db = DatabaseService.getInstance();
const localNodeId = db.getNodes()[0].id;
vi.spyOn(svc, 'getDeclaredStackServiceNames').mockResolvedValue(['web']);
const file = await svc.applyLocalOverride('proxy-stack', []);
expect(file).not.toBeNull();
const yaml = fsSync.readFileSync(file as string, 'utf8');
expect(yaml).toContain('web:');
expect(yaml).toContain('sencho_mesh');
expect(fsSync.readdirSync(path.dirname(file as string)).some((name) => name.includes('proxy-stack') && name.endsWith('.tmp'))).toBe(false);
expect(db.isMeshStackEnabled(localNodeId, 'proxy-stack')).toBe(true);
await svc.removeLocalOverride('proxy-stack');
expect(db.isMeshStackEnabled(localNodeId, 'proxy-stack')).toBe(false);
});
it('does not write a proxy override when DB authority cannot be recorded', async () => {
const svc = MeshService.getInstance();
const db = DatabaseService.getInstance();
const localNodeId = db.getNodes()[0].id;
vi.spyOn(svc, 'getDeclaredStackServiceNames').mockResolvedValue(['web']);
vi.spyOn(db, 'insertMeshStack').mockImplementation(() => {
throw new Error('database unavailable');
});
await expect(svc.applyLocalOverride('db-failure', [])).rejects.toThrow('database unavailable');
const overrideFile = path.join(
process.env.DATA_DIR as string,
'mesh',
'overrides',
String(localNodeId),
'db-failure.override.yml',
);
expect(fsSync.existsSync(overrideFile)).toBe(false);
});
it('restores an existing override when DB authority cannot be recorded', async () => {
const svc = MeshService.getInstance();
const db = DatabaseService.getInstance();
const localNodeId = db.getNodes()[0].id;
const overrideDir = path.join(process.env.DATA_DIR as string, 'mesh', 'overrides', String(localNodeId));
fsSync.mkdirSync(overrideDir, { recursive: true });
const overrideFile = path.join(overrideDir, 'db-replacement-failure.override.yml');
const originalYaml = 'services:\n prior:\n networks:\n - sencho_mesh\n';
fsSync.writeFileSync(overrideFile, originalYaml, 'utf8');
vi.spyOn(svc, 'getDeclaredStackServiceNames').mockResolvedValue(['web']);
vi.spyOn(db, 'insertMeshStack').mockImplementation(() => {
throw new Error('database unavailable');
});
await expect(svc.applyLocalOverride('db-replacement-failure', [])).rejects.toThrow('database unavailable');
expect(fsSync.readFileSync(overrideFile, 'utf8')).toBe(originalYaml);
expect(db.isMeshStackEnabled(localNodeId, 'db-replacement-failure')).toBe(false);
expect(fsSync.readdirSync(overrideDir).some((name) => name.includes('db-replacement-failure') && name.endsWith('.tmp'))).toBe(false);
});
it('does not publish DB authority or a final file when atomic override publication fails', async () => {
const svc = MeshService.getInstance();
const db = DatabaseService.getInstance();
const localNodeId = db.getNodes()[0].id;
vi.spyOn(svc, 'getDeclaredStackServiceNames').mockResolvedValue(['web']);
vi.spyOn(fs, 'rename').mockRejectedValue(new Error('rename failed'));
await expect(svc.applyLocalOverride('write-failure', [])).rejects.toThrow('rename failed');
const overrideDir = path.join(process.env.DATA_DIR as string, 'mesh', 'overrides', String(localNodeId));
expect(db.isMeshStackEnabled(localNodeId, 'write-failure')).toBe(false);
expect(fsSync.existsSync(path.join(overrideDir, 'write-failure.override.yml'))).toBe(false);
expect(fsSync.readdirSync(overrideDir).some((name) => name.includes('write-failure'))).toBe(false);
});
it('preserves the prior override and authority when atomic replacement fails', async () => {
const svc = MeshService.getInstance();
const db = DatabaseService.getInstance();
const localNodeId = db.getNodes()[0].id;
const overrideDir = path.join(process.env.DATA_DIR as string, 'mesh', 'overrides', String(localNodeId));
fsSync.mkdirSync(overrideDir, { recursive: true });
const overrideFile = path.join(overrideDir, 'replacement-failure.override.yml');
const originalYaml = 'services:\n prior:\n networks:\n - sencho_mesh\n';
fsSync.writeFileSync(overrideFile, originalYaml, 'utf8');
db.insertMeshStack(localNodeId, 'replacement-failure', 'tester');
vi.spyOn(svc, 'getDeclaredStackServiceNames').mockResolvedValue(['web']);
vi.spyOn(fs, 'rename').mockRejectedValue(new Error('rename failed'));
await expect(svc.applyLocalOverride('replacement-failure', [])).rejects.toThrow('rename failed');
expect(fsSync.readFileSync(overrideFile, 'utf8')).toBe(originalYaml);
expect(db.isMeshStackEnabled(localNodeId, 'replacement-failure')).toBe(true);
});
it('prevents overlapping override mutations for the same stack', async () => {
const svc = MeshService.getInstance();
const db = DatabaseService.getInstance();
const localNodeId = db.getNodes()[0].id;
let releaseServices!: (services: string[]) => void;
const servicesPending = new Promise<string[]>((resolve) => {
releaseServices = resolve;
});
vi.spyOn(svc, 'getDeclaredStackServiceNames').mockReturnValue(servicesPending);
const first = svc.applyLocalOverride('concurrent-stack', []);
await vi.waitFor(() => expect(svc.getDeclaredStackServiceNames).toHaveBeenCalledTimes(1));
await expect(svc.applyLocalOverride('concurrent-stack', [])).rejects.toThrow('another operation');
releaseServices(['web']);
await first;
expect(db.isMeshStackEnabled(localNodeId, 'concurrent-stack')).toBe(true);
});
it('prevents removal from overlapping an in-flight override apply', async () => {
const svc = MeshService.getInstance();
const db = DatabaseService.getInstance();
const localNodeId = db.getNodes()[0].id;
let releaseServices!: (services: string[]) => void;
const servicesPending = new Promise<string[]>((resolve) => {
releaseServices = resolve;
});
vi.spyOn(svc, 'getDeclaredStackServiceNames').mockReturnValue(servicesPending);
const apply = svc.applyLocalOverride('apply-remove-stack', []);
await vi.waitFor(() => expect(svc.getDeclaredStackServiceNames).toHaveBeenCalledTimes(1));
await expect(svc.removeLocalOverride('apply-remove-stack')).rejects.toThrow('another operation');
releaseServices(['web']);
const file = await apply;
expect(file).not.toBeNull();
expect(fsSync.existsSync(file as string)).toBe(true);
expect(db.isMeshStackEnabled(localNodeId, 'apply-remove-stack')).toBe(true);
});
it('does not report committed removal as failed when alias refresh fails', async () => {
const svc = MeshService.getInstance();
const db = DatabaseService.getInstance();
const localNodeId = db.getNodes()[0].id;
const overrideDir = path.join(process.env.DATA_DIR as string, 'mesh', 'overrides', String(localNodeId));
fsSync.mkdirSync(overrideDir, { recursive: true });
const overrideFile = path.join(overrideDir, 'refresh-failure.override.yml');
fsSync.writeFileSync(overrideFile, 'services: {}\n', 'utf8');
db.insertMeshStack(localNodeId, 'refresh-failure', 'setup');
(svc as unknown as { pilotAliasOverlay: Map<string, unknown> }).pilotAliasOverlay.set('refresh-failure', []);
vi.spyOn(svc as unknown as { refreshAliasCache: () => Promise<void> }, 'refreshAliasCache')
.mockRejectedValue(new Error('refresh failed'));
vi.spyOn(console, 'warn').mockImplementation(() => { /* silence */ });
await expect(svc.removeLocalOverride('refresh-failure')).resolves.toBeUndefined();
expect(fsSync.existsSync(overrideFile)).toBe(false);
expect(db.isMeshStackEnabled(localNodeId, 'refresh-failure')).toBe(false);
});
it('does not report committed apply as failed when alias refresh fails', async () => {
const svc = MeshService.getInstance();
const db = DatabaseService.getInstance();
const localNodeId = db.getNodes()[0].id;
vi.spyOn(svc, 'getDeclaredStackServiceNames').mockResolvedValue(['web']);
vi.spyOn(svc as unknown as { refreshAliasCache: () => Promise<void> }, 'refreshAliasCache')
.mockRejectedValue(new Error('refresh failed'));
vi.spyOn(console, 'warn').mockImplementation(() => { /* silence */ });
const file = await svc.applyLocalOverride('apply-refresh-failure', [], [{
host: 'web.example', nodeId: localNodeId, nodeName: 'local', stackName: 'apply-refresh-failure',
serviceName: 'web', port: 8080,
}]);
expect(file).not.toBeNull();
expect(fsSync.existsSync(file as string)).toBe(true);
expect(db.isMeshStackEnabled(localNodeId, 'apply-refresh-failure')).toBe(true);
});
it('returns null for pilot nodes when no pushed override file exists', async () => {
@@ -1015,9 +1368,38 @@ describe('MeshService.ensureStackOverride (BUG-1 fix)', () => {
const db = DatabaseService.getInstance();
const localNodeId = db.getNodes()[0].id;
process.env.SENCHO_MODE = 'pilot';
// No mesh_stacks row, no file on disk.
const result = await svc.ensureStackOverride(localNodeId, 'no-such-stack');
expect(result).toBeNull();
process.env.SENCHO_MODE = 'server';
});
it('does not use stale override presence as authority on a server', async () => {
const svc = MeshService.getInstance();
const db = DatabaseService.getInstance();
const localNodeId = db.getNodes()[0].id;
const overrideDir = path.join(process.env.DATA_DIR as string, 'mesh', 'overrides', String(localNodeId));
fsSync.mkdirSync(overrideDir, { recursive: true });
const overrideFile = path.join(overrideDir, 'stale-server.override.yml');
fsSync.writeFileSync(overrideFile, 'services: {}\n', 'utf8');
const result = await svc.ensureStackOverride(localNodeId, 'stale-server');
expect(result).toBeNull();
fsSync.unlinkSync(overrideFile);
});
it('restores proxy-target DB authority when override removal fails', async () => {
const svc = MeshService.getInstance();
const db = DatabaseService.getInstance();
const localNodeId = db.getNodes()[0].id;
db.insertMeshStack(localNodeId, 'unlink-failure', 'tester');
vi.spyOn(fs, 'unlink').mockRejectedValue(Object.assign(new Error('permission denied'), { code: 'EACCES' }));
await expect(svc.removeLocalOverride('unlink-failure')).rejects.toThrow('permission denied');
expect(db.isMeshStackEnabled(localNodeId, 'unlink-failure')).toBe(true);
});
});
@@ -45,6 +45,7 @@ describe('networking summary', () => {
afterEach(() => {
vi.restoreAllMocks();
DatabaseService.getInstance().deleteStackExposureIntents(1, STACK);
DatabaseService.getInstance().deleteMeshStack(1, STACK);
fs.rmSync(stackDir, { recursive: true, force: true });
});
@@ -72,6 +73,41 @@ describe('networking summary', () => {
expect(res.body.networkDrift.stacks).toContain(STACK);
});
it('does not count an opted-in Mesh attachment as network drift', async () => {
DatabaseService.getInstance().insertMeshStack(1, STACK, 'tester');
vi.spyOn(DockerController, 'getInstance').mockReturnValue({
getDependencySnapshot: vi.fn().mockResolvedValue({
containers: [{ id: 'c1', name: 'web1', service: 'web', composeProject: STACK, stack: STACK, state: 'running', exitCode: null, image: 'nginx', networks: [{ name: 'sencho_mesh', id: 'm', ip: '' }], volumes: [], ports: [] }],
networks: [
{ id: 'm', name: 'sencho_mesh', driver: 'bridge', scope: 'local', isSystem: false, composeProject: null, stack: null },
],
volumes: [],
}),
} as unknown as DockerController);
const res = await request(app).get('/api/networking/summary').set('Authorization', authHeader);
expect(res.status).toBe(200);
expect(res.body.networkDrift).toEqual({ count: 0, stacks: [] });
});
it('counts an opted-out manual Mesh attachment as network drift', async () => {
vi.spyOn(DockerController, 'getInstance').mockReturnValue({
getDependencySnapshot: vi.fn().mockResolvedValue({
containers: [{ id: 'c1', name: 'web1', service: 'web', composeProject: STACK, stack: STACK, state: 'running', exitCode: null, image: 'nginx', networks: [{ name: 'sencho_mesh', id: 'm', ip: '' }], volumes: [], ports: [] }],
networks: [
{ id: 'm', name: 'sencho_mesh', driver: 'bridge', scope: 'local', isSystem: false, composeProject: null, stack: null },
],
volumes: [],
}),
} as unknown as DockerController);
const res = await request(app).get('/api/networking/summary').set('Authorization', authHeader);
expect(res.status).toBe(200);
expect(res.body.networkDrift).toEqual({ count: 1, stacks: [STACK] });
});
it('still reports declared signals when the snapshot is unavailable (drift skipped)', async () => {
vi.spyOn(DockerController, 'getInstance').mockReturnValue({
getDependencySnapshot: vi.fn().mockRejectedValue(new Error('docker down')),