- 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.
129 lines
4.6 KiB
TypeScript
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();
|
|
});
|
|
});
|