feat(api): complete phase 2.4 control plane
This commit is contained in:
@@ -64,9 +64,12 @@ describe('database migrations', () => {
|
||||
'capabilities',
|
||||
'instance_tags',
|
||||
'instances',
|
||||
'job_attempts',
|
||||
'job_items',
|
||||
'jobs',
|
||||
'operation_preparations',
|
||||
'schema_migrations',
|
||||
'secret_cleanup_tasks',
|
||||
'secret_references',
|
||||
'status_snapshots',
|
||||
]);
|
||||
@@ -91,7 +94,18 @@ describe('database migrations', () => {
|
||||
expect(foreignKeys).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ from: 'job_id', on_delete: 'CASCADE', table: 'jobs' }),
|
||||
expect.objectContaining({ from: 'instance_id', on_delete: 'RESTRICT', table: 'instances' }),
|
||||
]),
|
||||
);
|
||||
expect(foreignKeys).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ from: 'instance_id', table: 'instances' }),
|
||||
]),
|
||||
);
|
||||
const jobColumns = database.pragma('table_info(jobs)') as Array<{ name: string }>;
|
||||
expect(jobColumns).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ name: 'root_job_id' }),
|
||||
expect.objectContaining({ name: 'retry_of_job_id' }),
|
||||
]),
|
||||
);
|
||||
const indexes = database
|
||||
@@ -107,6 +121,103 @@ describe('database migrations', () => {
|
||||
'idx_status_snapshots_instance_observed_at',
|
||||
]),
|
||||
);
|
||||
const now = '2026-07-16T00:00:00.000Z';
|
||||
database
|
||||
.prepare(
|
||||
'INSERT INTO instances (id,name,base_url,auth_mode,enabled,config_revision,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?)',
|
||||
)
|
||||
.run('cleanup-owner', 'Cleanup owner', 'http://10.0.0.9', 'password', 1, 1, now, now);
|
||||
database
|
||||
.prepare(
|
||||
'INSERT INTO secret_references (id,instance_id,purpose,provider,external_reference,created_at,updated_at) VALUES (?,?,?,?,?,?,?)',
|
||||
)
|
||||
.run(
|
||||
'ref-one',
|
||||
'cleanup-owner',
|
||||
'instance-password',
|
||||
'macos-keychain',
|
||||
'opaque-one',
|
||||
now,
|
||||
now,
|
||||
);
|
||||
expect(() =>
|
||||
database
|
||||
.prepare(
|
||||
'INSERT INTO secret_references (id,instance_id,purpose,provider,external_reference,created_at,updated_at) VALUES (?,?,?,?,?,?,?)',
|
||||
)
|
||||
.run(
|
||||
'ref-two',
|
||||
'cleanup-owner',
|
||||
'instance-password',
|
||||
'macos-keychain',
|
||||
'opaque-two',
|
||||
now,
|
||||
now,
|
||||
),
|
||||
).toThrow(/unique/i);
|
||||
database
|
||||
.prepare(
|
||||
'INSERT INTO secret_cleanup_tasks (reference,instance_id,purpose,provider,queued_at,updated_at) VALUES (?,?,?,?,?,?)',
|
||||
)
|
||||
.run('opaque-one', 'cleanup-owner', 'instance-password', 'macos-keychain', now, now);
|
||||
database.prepare('DELETE FROM instances WHERE id=?').run('cleanup-owner');
|
||||
expect(database.prepare('SELECT reference FROM secret_cleanup_tasks').all()).toEqual([
|
||||
{ reference: 'opaque-one' },
|
||||
]);
|
||||
database.close();
|
||||
});
|
||||
|
||||
it('upgrades duplicate v1 secret references by retaining one canonical row and queuing the rest', async () => {
|
||||
const directory = await temporaryDirectory();
|
||||
const database = openDatabase(join(directory, 'app.sqlite'));
|
||||
migrateDatabase(database, [MIGRATIONS[0]!]);
|
||||
const older = '2026-07-15T00:00:00.000Z';
|
||||
const newer = '2026-07-16T00:00:00.000Z';
|
||||
database
|
||||
.prepare(
|
||||
'INSERT INTO instances (id,name,base_url,auth_mode,enabled,config_revision,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?)',
|
||||
)
|
||||
.run('owner', 'Owner', 'http://10.0.0.8', 'password', 1, 1, older, newer);
|
||||
const reference = (slot: string) =>
|
||||
`keychain://multi-simadmin/${Buffer.from(JSON.stringify(['owner', 'instance-password', slot]), 'utf8').toString('base64url')}`;
|
||||
const insert = database.prepare(
|
||||
'INSERT INTO secret_references (id,instance_id,purpose,provider,external_reference,created_at,updated_at) VALUES (?,?,?,?,?,?,?)',
|
||||
);
|
||||
insert.run(
|
||||
'old-ref',
|
||||
'owner',
|
||||
'instance-password',
|
||||
'macos-keychain',
|
||||
reference('old-slot'),
|
||||
older,
|
||||
older,
|
||||
);
|
||||
insert.run(
|
||||
'new-ref',
|
||||
'owner',
|
||||
'instance-password',
|
||||
'macos-keychain',
|
||||
reference('new-slot'),
|
||||
newer,
|
||||
newer,
|
||||
);
|
||||
|
||||
migrateDatabase(database);
|
||||
|
||||
expect(database.prepare('SELECT id FROM secret_references').all()).toEqual([{ id: 'new-ref' }]);
|
||||
expect(
|
||||
database
|
||||
.prepare('SELECT reference,instance_id,purpose,provider FROM secret_cleanup_tasks')
|
||||
.all(),
|
||||
).toEqual([
|
||||
{
|
||||
reference: reference('old-slot'),
|
||||
instance_id: 'owner',
|
||||
purpose: 'instance-password',
|
||||
provider: 'macos-keychain',
|
||||
},
|
||||
]);
|
||||
expect(() => migrateDatabase(database)).not.toThrow();
|
||||
database.close();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user