feat: add executor gateway contracts
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
createExecutorAdapterRegistry,
|
||||
createExecutorGateway,
|
||||
createInMemoryExecutorJobStore,
|
||||
createPhase3ExecutorAdapterRegistry,
|
||||
normalizeExecutorJobRequest,
|
||||
} from './executor-gateway.mjs';
|
||||
|
||||
function request(overrides = {}) {
|
||||
return {
|
||||
jobId: 'job-1',
|
||||
idempotencyKey: 'run-1:node-execute:attempt-1',
|
||||
executor: 'aider',
|
||||
task: {
|
||||
type: 'code_change',
|
||||
instruction: 'Fix the login form validation',
|
||||
workspaceRef: { kind: 'mindspace-workspace', id: 'workspace-1' },
|
||||
},
|
||||
subject: { userId: 'user-1' },
|
||||
authorization: { executionAllowed: true, actorId: 'user-1' },
|
||||
policy: { sideEffectsAllowed: true, networkAllowed: false },
|
||||
controls: {
|
||||
timeoutMs: 20_000,
|
||||
cancellationAllowed: true,
|
||||
fallbackExecutor: 'goosed',
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test('executor job contract uses resource references and bounded controls', () => {
|
||||
const normalized = normalizeExecutorJobRequest(request({
|
||||
task: {
|
||||
instruction: 'x'.repeat(40_000),
|
||||
workspaceRef: { kind: 'workspace', id: 'workspace-1' },
|
||||
inputRefs: [
|
||||
{ kind: 'artifact', id: 'artifact-1' },
|
||||
{ kind: '', id: 'invalid' },
|
||||
],
|
||||
},
|
||||
controls: { timeoutMs: 100, fallbackExecutor: 'openhands' },
|
||||
}));
|
||||
assert.equal(normalized.version, 'executor-job-request-v1');
|
||||
assert.equal(normalized.task.instruction.length, 32_000);
|
||||
assert.deepEqual(normalized.task.workspaceRef, { kind: 'workspace', id: 'workspace-1' });
|
||||
assert.deepEqual(normalized.task.inputRefs, [{ kind: 'artifact', id: 'artifact-1' }]);
|
||||
assert.equal(normalized.controls.timeoutMs, 500);
|
||||
assert.equal(normalized.controls.fallbackExecutor, 'openhands');
|
||||
assert.equal('cwd' in normalized.task, false);
|
||||
});
|
||||
|
||||
test('Phase 3 executor catalog exposes disabled Goosed, Aider and OpenHands adapters', () => {
|
||||
const registry = createPhase3ExecutorAdapterRegistry();
|
||||
assert.deepEqual(
|
||||
registry.list().map((adapter) => adapter.id),
|
||||
['goosed', 'aider', 'openhands'],
|
||||
);
|
||||
for (const adapter of registry.list()) {
|
||||
assert.equal(adapter.enabled, false);
|
||||
assert.equal(adapter.dispatchImplemented, false);
|
||||
assert.equal(adapter.status, 'contract-only');
|
||||
}
|
||||
});
|
||||
|
||||
test('executor adapter registry rejects incomplete and duplicate adapters', () => {
|
||||
const registry = createExecutorAdapterRegistry();
|
||||
assert.throws(() => registry.register({ id: 'broken' }), /missing method/);
|
||||
const adapter = {
|
||||
id: 'custom',
|
||||
submit() {},
|
||||
cancel() {},
|
||||
getState() {},
|
||||
async *streamEvents() {},
|
||||
};
|
||||
registry.register(adapter);
|
||||
assert.throws(() => registry.register(adapter), /already registered/);
|
||||
});
|
||||
|
||||
test('Executor Gateway records a blocked idempotent job without calling an adapter', async () => {
|
||||
let now = 1000;
|
||||
const gateway = createExecutorGateway({
|
||||
store: createInMemoryExecutorJobStore(),
|
||||
nowMs: () => now,
|
||||
});
|
||||
const preview = await gateway.preview(request());
|
||||
assert.equal(preview.executor, 'aider');
|
||||
assert.equal(preview.fallbackExecutor, 'goosed');
|
||||
assert.equal(preview.fallbackRegistered, true);
|
||||
assert.equal(preview.fallbackAvailable, false);
|
||||
assert.equal(preview.dispatchAllowed, false);
|
||||
assert.equal(preview.gates.implementation, false);
|
||||
assert.equal(preview.gates.adapterEnabled, false);
|
||||
|
||||
const first = await gateway.createJob(request());
|
||||
assert.equal(first.created, true);
|
||||
assert.equal(first.job.status, 'blocked');
|
||||
assert.equal(first.job.attempts, 0);
|
||||
assert.equal(first.job.reason, 'executor_dispatch_not_implemented');
|
||||
|
||||
now = 2000;
|
||||
const repeated = await gateway.createJob(request());
|
||||
assert.equal(repeated.created, false);
|
||||
assert.equal(repeated.job.jobId, first.job.jobId);
|
||||
assert.equal(repeated.job.createdAt, 1000);
|
||||
});
|
||||
|
||||
test('Executor Gateway rejects idempotency key reuse with a different payload', async () => {
|
||||
const gateway = createExecutorGateway();
|
||||
await gateway.createJob(request());
|
||||
await assert.rejects(
|
||||
() => gateway.createJob(request({
|
||||
jobId: 'job-2',
|
||||
task: {
|
||||
type: 'code_change',
|
||||
instruction: 'A different instruction',
|
||||
workspaceRef: { kind: 'mindspace-workspace', id: 'workspace-1' },
|
||||
},
|
||||
})),
|
||||
(error) => error.code === 'EXECUTOR_IDEMPOTENCY_CONFLICT',
|
||||
);
|
||||
});
|
||||
|
||||
test('Executor Gateway cancellation is idempotent and never invokes a disabled adapter', async () => {
|
||||
let now = 1000;
|
||||
const gateway = createExecutorGateway({ nowMs: () => now });
|
||||
await gateway.createJob(request());
|
||||
now = 1500;
|
||||
const cancelled = await gateway.cancel('job-1', { reason: 'user_cancelled' });
|
||||
assert.equal(cancelled.status, 'cancelled');
|
||||
assert.equal(cancelled.reason, 'user_cancelled');
|
||||
assert.equal(cancelled.completedAt, 1500);
|
||||
assert.deepEqual(await gateway.cancel('job-1'), cancelled);
|
||||
assert.equal(await gateway.cancel('missing-job'), null);
|
||||
});
|
||||
|
||||
test('Executor Gateway status remains hard-disabled with a non-durable reference store', () => {
|
||||
const status = createExecutorGateway().status();
|
||||
assert.equal(status.version, 'executor-gateway-status-v1');
|
||||
assert.equal(status.dispatchImplemented, false);
|
||||
assert.equal(status.executionEnabled, false);
|
||||
assert.deepEqual(status.store, { kind: 'memory', durable: false });
|
||||
});
|
||||
Reference in New Issue
Block a user