mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-09-04 14:45:41 +00:00
fix(notifications): neutralize satellite-local node names in alert bodies (#1640)
* fix(notifications): neutralize satellite-local node names in alert bodies Fleet-aggregated alerts embedded each instance seed name (often Local) while the hub badge already named the remote. Drop identity prefixes and use type-aware local wording so attribution stays on the badge. * fix(docs): correct image-update default check cadence Operator docs still said six-hour polling; the seeded default is two hours in interval mode, and the cadence is configurable or cron-based. * test(notifications): assert hub stamps roster name on neutral remote bodies Cover the fan-in path that attaches hub roster identity while leaving the satellite message body unchanged.
This commit is contained in:
@@ -469,3 +469,106 @@ describe('BlueprintService marker parsing + name-conflict guard', () => {
|
||||
expect(BlueprintService.parseMarker(JSON.stringify({ blueprintId: 'nope', revision: 1 }))).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('BlueprintReconciler drift alert node wording', () => {
|
||||
type ReconcilerWithDrift = {
|
||||
handleDrift: (blueprint: Blueprint, node: Node, reason: string) => Promise<void>;
|
||||
};
|
||||
|
||||
function seedRemoteNode(name: string): number {
|
||||
counter += 1;
|
||||
const db = DatabaseService.getInstance().getDb();
|
||||
const result = db.prepare(
|
||||
`INSERT INTO nodes (name, type, mode, compose_dir, is_default, status, created_at)
|
||||
VALUES (?, 'remote', 'proxy', '/tmp/compose', 0, 'online', ?)`
|
||||
).run(name, Date.now());
|
||||
return result.lastInsertRowid as number;
|
||||
}
|
||||
|
||||
it('suggest-mode local drift uses on this node', async () => {
|
||||
const { NotificationService } = await import('../services/NotificationService');
|
||||
const dispatchSpy = vi.spyOn(NotificationService.getInstance(), 'dispatchAlert').mockResolvedValue(undefined);
|
||||
const nodeId = seedNode();
|
||||
const bp = seedBlueprint({ name: 'drift-local', drift_mode: 'suggest', nodeIds: [nodeId] });
|
||||
const node = DatabaseService.getInstance().getNode(nodeId)!;
|
||||
const reconciler = BlueprintReconciler.getInstance() as unknown as ReconcilerWithDrift;
|
||||
|
||||
await reconciler.handleDrift(bp, node, 'compose changed');
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith(
|
||||
'warning',
|
||||
'blueprint_drift_detected',
|
||||
'Blueprint "drift-local" drifted on this node: compose changed',
|
||||
{ stackName: 'drift-local', actor: 'system:blueprint' },
|
||||
);
|
||||
});
|
||||
|
||||
it('suggest-mode remote drift keeps the authoritative node name', async () => {
|
||||
const { NotificationService } = await import('../services/NotificationService');
|
||||
const dispatchSpy = vi.spyOn(NotificationService.getInstance(), 'dispatchAlert').mockResolvedValue(undefined);
|
||||
const nodeId = seedRemoteNode('sencho-test-02');
|
||||
const bp = seedBlueprint({ name: 'drift-remote', drift_mode: 'suggest', nodeIds: [nodeId] });
|
||||
const node = DatabaseService.getInstance().getNode(nodeId)!;
|
||||
const reconciler = BlueprintReconciler.getInstance() as unknown as ReconcilerWithDrift;
|
||||
|
||||
await reconciler.handleDrift(bp, node, 'compose changed');
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith(
|
||||
'warning',
|
||||
'blueprint_drift_detected',
|
||||
'Blueprint "drift-remote" drifted on node "sencho-test-02": compose changed',
|
||||
{ stackName: 'drift-remote', actor: 'system:blueprint' },
|
||||
);
|
||||
});
|
||||
|
||||
it('stateful marker-loss on local uses on this node', async () => {
|
||||
const { NotificationService } = await import('../services/NotificationService');
|
||||
const dispatchSpy = vi.spyOn(NotificationService.getInstance(), 'dispatchAlert').mockResolvedValue(undefined);
|
||||
vi.spyOn(BlueprintService.getInstance(), 'readMarker').mockResolvedValue(null);
|
||||
const nodeId = seedNode();
|
||||
const bp = seedBlueprint({
|
||||
name: 'marker-local',
|
||||
drift_mode: 'enforce',
|
||||
classification: 'stateful',
|
||||
nodeIds: [nodeId],
|
||||
});
|
||||
const node = DatabaseService.getInstance().getNode(nodeId)!;
|
||||
const reconciler = BlueprintReconciler.getInstance() as unknown as ReconcilerWithDrift;
|
||||
|
||||
await reconciler.handleDrift(bp, node, 'volumes diverged');
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith(
|
||||
'warning',
|
||||
'blueprint_drift_detected',
|
||||
'Blueprint "marker-local" lost its marker on this node; auto-fix declined to avoid stomping unowned data. Reason: volumes diverged',
|
||||
{ stackName: 'marker-local', actor: 'system:blueprint' },
|
||||
);
|
||||
});
|
||||
|
||||
it('enforce correction-failure on local uses on this node', async () => {
|
||||
const { NotificationService } = await import('../services/NotificationService');
|
||||
const dispatchSpy = vi.spyOn(NotificationService.getInstance(), 'dispatchAlert').mockResolvedValue(undefined);
|
||||
vi.spyOn(BlueprintService.getInstance(), 'deployToNode').mockResolvedValue({
|
||||
status: 'failed',
|
||||
error: 'compose up failed',
|
||||
} as Awaited<ReturnType<typeof BlueprintService.prototype.deployToNode>>);
|
||||
const nodeId = seedNode();
|
||||
const bp = seedBlueprint({
|
||||
name: 'fix-local',
|
||||
drift_mode: 'enforce',
|
||||
classification: 'stateless',
|
||||
nodeIds: [nodeId],
|
||||
});
|
||||
const node = DatabaseService.getInstance().getNode(nodeId)!;
|
||||
const reconciler = BlueprintReconciler.getInstance() as unknown as ReconcilerWithDrift;
|
||||
|
||||
await reconciler.handleDrift(bp, node, 'compose changed');
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith(
|
||||
'error',
|
||||
'blueprint_drift_correction_failed',
|
||||
'Auto-fix for "fix-local" on this node failed: compose up failed',
|
||||
{ stackName: 'fix-local', actor: 'system:blueprint' },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user