fix: enforce atomic agent registration contract
This commit is contained in:
1 parent
47d323c17b
commit
f64d00673c
58 files changed
+2692
-1667
No files matched your search
@@ -0,0 +1,163 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { INTEGRATION_TYPES } from '../dist/shared/index.js';
|
||||
import {
|
||||
registerAgent,
|
||||
updateAgentIntegrationType,
|
||||
} from '../dist/services/agent-service.js';
|
||||
|
||||
const EXPECTED_INTEGRATION_TYPES = [
|
||||
'augment', 'claudecode', 'cline', 'codex', 'copilot', 'cursor', 'droid',
|
||||
'gemini', 'grok', 'kimi', 'kirocli', 'kiroide', 'letta', 'opencode', 'qwen',
|
||||
'enterprise', 'gui', 'sdk', 'other',
|
||||
];
|
||||
const PUBLIC_KEY = Buffer.alloc(32, 7).toString('base64url');
|
||||
|
||||
class FakeStatement {
|
||||
constructor(database, sql, values = []) {
|
||||
this.database = database;
|
||||
this.sql = sql;
|
||||
this.values = values;
|
||||
}
|
||||
|
||||
bind(...values) {
|
||||
return new FakeStatement(this.database, this.sql, values);
|
||||
}
|
||||
|
||||
async first() {
|
||||
return this.database.firstResult;
|
||||
}
|
||||
|
||||
async all() {
|
||||
return { results: [] };
|
||||
}
|
||||
|
||||
async run() {
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
||||
class FakeDatabase {
|
||||
constructor(firstResult = null) {
|
||||
this.firstResult = firstResult;
|
||||
this.prepareCount = 0;
|
||||
this.batches = [];
|
||||
}
|
||||
|
||||
prepare(sql) {
|
||||
this.prepareCount += 1;
|
||||
return new FakeStatement(this, sql);
|
||||
}
|
||||
|
||||
async batch(statements) {
|
||||
this.batches.push(statements);
|
||||
return statements.map(() => ({ results: [], success: true }));
|
||||
}
|
||||
}
|
||||
|
||||
function registrationRequest(integrationType) {
|
||||
return {
|
||||
agent_id: `agent-${integrationType}`,
|
||||
integration_type: integrationType,
|
||||
display_name: 'Audit Agent',
|
||||
responsible_entity: 'platform@example.com',
|
||||
keys: [{ kid: 'key-v1', public_key: PUBLIC_KEY, algorithm: 'ed25519' }],
|
||||
};
|
||||
}
|
||||
|
||||
async function expectIntegrationError(operation, messageKey) {
|
||||
await assert.rejects(operation, (error) => {
|
||||
assert.equal(error.name, 'AppError');
|
||||
assert.equal(error.statusCode, 400);
|
||||
assert.equal(error.errorCode, 'VALIDATION_ERROR');
|
||||
assert.equal(error.messageKey, messageKey);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
test('integration types match the public registration contract', () => {
|
||||
assert.deepEqual(INTEGRATION_TYPES, EXPECTED_INTEGRATION_TYPES);
|
||||
});
|
||||
|
||||
test('registration rejects missing and unknown integration types before database access', async () => {
|
||||
for (const [value, messageKey] of [
|
||||
[undefined, 'agent.missingIntegrationType'],
|
||||
['future-cli', 'agent.invalidIntegrationType'],
|
||||
]) {
|
||||
const database = new FakeDatabase();
|
||||
const request = registrationRequest(value);
|
||||
await expectIntegrationError(
|
||||
registerAgent(database, request, 'org-1', 'user-1'),
|
||||
messageKey,
|
||||
);
|
||||
assert.equal(database.prepareCount, 0);
|
||||
assert.equal(database.batches.length, 0);
|
||||
}
|
||||
|
||||
for (const [value, messageKey] of [
|
||||
[undefined, 'agent.missingIntegrationType'],
|
||||
['future-cli', 'agent.invalidIntegrationType'],
|
||||
]) {
|
||||
const database = new FakeDatabase();
|
||||
await expectIntegrationError(
|
||||
updateAgentIntegrationType(database, 'agent-1', value, 'org-1', 'user-1'),
|
||||
messageKey,
|
||||
);
|
||||
assert.equal(database.prepareCount, 0);
|
||||
}
|
||||
});
|
||||
|
||||
test('integration updates stay scoped to the authenticated organization', async () => {
|
||||
const existingAgent = {
|
||||
agent_id: 'agent-1',
|
||||
org_id: 'org-1',
|
||||
display_name: 'Audit Agent',
|
||||
responsible_entity: 'platform@example.com',
|
||||
integration_type: 'sdk',
|
||||
status: 'active',
|
||||
created_at: 1,
|
||||
updated_at: 1,
|
||||
};
|
||||
|
||||
for (const integrationType of INTEGRATION_TYPES) {
|
||||
const database = new FakeDatabase(existingAgent);
|
||||
const response = await updateAgentIntegrationType(
|
||||
database,
|
||||
existingAgent.agent_id,
|
||||
integrationType,
|
||||
existingAgent.org_id,
|
||||
'user-1',
|
||||
);
|
||||
|
||||
assert.equal(response.agent.integration_type, integrationType);
|
||||
const update = database.batches[0].find((statement) => (
|
||||
statement.sql.includes('UPDATE agents SET integration_type')
|
||||
));
|
||||
assert.ok(update);
|
||||
assert.match(update.sql, /WHERE agent_id = \? AND org_id = \?/);
|
||||
assert.equal(update.values[0], integrationType);
|
||||
assert.equal(update.values[2], existingAgent.agent_id);
|
||||
assert.equal(update.values[3], existingAgent.org_id);
|
||||
}
|
||||
});
|
||||
|
||||
test('registration persists every supported integration type without substitution', async () => {
|
||||
for (const integrationType of INTEGRATION_TYPES) {
|
||||
const database = new FakeDatabase();
|
||||
const response = await registerAgent(
|
||||
database,
|
||||
registrationRequest(integrationType),
|
||||
'org-1',
|
||||
'user-1',
|
||||
);
|
||||
|
||||
assert.equal(response.agent.integration_type, integrationType);
|
||||
assert.equal(database.batches.length, 1);
|
||||
const agentInsert = database.batches[0].find((statement) => (
|
||||
statement.sql.includes('INSERT INTO agents')
|
||||
));
|
||||
assert.ok(agentInsert);
|
||||
assert.equal(agentInsert.values[4], integrationType);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user