Files
multi-simadmin/apps/api/src/application/automation/target-resolver.test.ts
T
chick 8406f14469 feat(contracts,automation): add multi-group scopes, SMS macros and operator registry
- Allow scheduled operations to resolve multiple device groups.

- Add reusable SMS time and random-value macros.

- Add China operator PLMN metadata shared by API and UI.
2026-09-07 00:44:33 +08:00

129 lines
4.6 KiB
TypeScript

import Database from 'better-sqlite3';
import { describe, expect, it } from 'vitest';
import { migrateDatabase } from '../../infrastructure/database/migrations.js';
import { resolveOperationTargets, resolveTargets } from './target-resolver.js';
function fixture() {
const db = new Database(':memory:');
db.pragma('foreign_keys = ON');
migrateDatabase(db);
const now = '2026-07-30T00:00:00.000Z';
const insert = db.prepare(
'INSERT INTO instances (id,name,base_url,enabled,config_revision,created_at,updated_at) VALUES (?,?,?,?,?,?,?)',
);
insert.run('a', 'Alpha', 'http://10.0.0.1', 1, 3, now, now);
insert.run('b', 'Beta', 'http://10.0.0.2', 1, 2, now, now);
insert.run('c', 'Disabled', 'http://10.0.0.3', 0, 1, now, now);
const tag = db.prepare('INSERT INTO instance_tags (instance_id,tag,created_at) VALUES (?,?,?)');
tag.run('a', 'lab', now);
tag.run('a', 'east', now);
tag.run('b', 'lab', now);
tag.run('c', 'east', now);
return db;
}
describe('resolveTargets', () => {
it('resolves enabled fixed targets in selector order with current revisions', () => {
const db = fixture();
expect(resolveTargets(db, { mode: 'fixed', instanceIds: ['b', 'c', 'a'] })).toEqual([
{ id: 'b', revision: 2 },
{ id: 'a', revision: 3 },
]);
db.close();
});
it('resolves dynamic all and any tag matches on every call', () => {
const db = fixture();
expect(resolveTargets(db, { mode: 'tags', match: 'all', tags: ['lab', 'east'] })).toEqual([
{ id: 'a', revision: 3 },
]);
expect(resolveTargets(db, { mode: 'tags', match: 'any', tags: ['lab', 'east'] })).toEqual([
{ id: 'a', revision: 3 },
{ id: 'b', revision: 2 },
]);
db.prepare(
'INSERT INTO instances (id,name,base_url,enabled,config_revision,created_at,updated_at) VALUES (?,?,?,?,?,?,?)',
).run(
'd',
'Dynamic',
'http://10.0.0.4',
1,
1,
'2026-07-30T00:00:00.000Z',
'2026-07-30T00:00:00.000Z',
);
db.prepare('INSERT INTO instance_tags (instance_id,tag,created_at) VALUES (?,?,?)').run(
'd',
'east',
'2026-07-30T00:00:00.000Z',
);
expect(
resolveTargets(db, { mode: 'tags', match: 'any', tags: ['east'] }).map((x) => x.id),
).toEqual(['a', 'd']);
db.close();
});
it('resolves the Hub all-devices and group selectors', () => {
const db = fixture();
const now = '2026-07-30T00:00:00.000Z';
db.prepare(
'INSERT INTO device_groups (id,name,description,created_at,updated_at) VALUES (?,?,?,?,?)',
).run('g-lab', '实验室', '', now, now);
db.prepare('UPDATE instances SET group_id = ? WHERE id IN (?,?)').run('g-lab', 'a', 'c');
expect(resolveTargets(db, { mode: 'all' })).toEqual([
{ id: 'a', revision: 3 },
{ id: 'b', revision: 2 },
]);
expect(resolveTargets(db, { mode: 'group', groupId: 'g-lab' })).toEqual([
{ id: 'a', revision: 3 },
]);
expect(resolveTargets(db, { mode: 'groups', groupIds: ['g-lab', 'g-office'] })).toEqual([
{ id: 'a', revision: 3 },
]);
db.close();
});
it('dispatches control-plane backups without resolving device targets', () => {
const db = fixture();
expect(resolveOperationTargets(db, { mode: 'all' }, 'backup-data')).toEqual({
targets: [{ id: 'control-plane', revision: 1 }],
targetSnapshot: [],
unavailableInstanceIds: [],
});
db.close();
});
it('dispatches devices whose capability was never probed', () => {
const db = fixture();
expect(resolveOperationTargets(db, { mode: 'all' }, 'restart-baseband')).toEqual({
targets: [
{ id: 'a', revision: 3 },
{ id: 'b', revision: 2 },
],
targetSnapshot: ['a', 'b'],
unavailableInstanceIds: [],
});
db.close();
});
it('separates targets whose required operation capability is unavailable', () => {
const db = fixture();
const now = '2026-07-30T00:00:00.000Z';
db.prepare(
'INSERT INTO capabilities (instance_id,operation_id,state,observed_at,created_at,updated_at) VALUES (?,?,?,?,?,?)',
).run('a', 'postServiceRestart', 'supported', now, now, now);
db.prepare(
'INSERT INTO capabilities (instance_id,operation_id,state,observed_at,created_at,updated_at) VALUES (?,?,?,?,?,?)',
).run('b', 'postServiceRestart', 'unsupported', now, now, now);
expect(
resolveOperationTargets(db, { mode: 'fixed', instanceIds: ['a', 'b'] }, 'restart-service'),
).toEqual({
targets: [{ id: 'a', revision: 3 }],
targetSnapshot: ['a', 'b'],
unavailableInstanceIds: ['b'],
});
db.close();
});
});