feat(billing): bill Cursor executor usage via stream-json token parsing
Memind CI / Test, build, and release guards (push) Failing after 14m36s
Memind CI / Test, build, and release guards (push) Failing after 14m36s
Parse Cursor CLI usage from stream-json output and charge through billSessionUsage so subscription quota is consumed first and wallet overage applies when needed. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3131,6 +3131,98 @@ test('required Aider run persists a validated result into a chat session', async
|
||||
);
|
||||
});
|
||||
|
||||
test('required Cursor run bills usage after validated delivery', async () => {
|
||||
const pool = createFakePool();
|
||||
const workdir = await fs.mkdtemp(path.join(os.tmpdir(), 'memind-cursor-billing-'));
|
||||
const deliveries = [];
|
||||
const billingCalls = [];
|
||||
const gateway = createAgentRunGateway({
|
||||
pool,
|
||||
userAuth: {
|
||||
async resolveWorkingDir() {
|
||||
return workdir;
|
||||
},
|
||||
async getBillingState() {
|
||||
return { lastInputTokens: 100, lastOutputTokens: 20 };
|
||||
},
|
||||
async billSessionUsage(userId, sessionId, tokenState, requestId) {
|
||||
billingCalls.push({ userId, sessionId, tokenState, requestId });
|
||||
return {
|
||||
ok: true,
|
||||
costCents: 3,
|
||||
deltaInputTokens: tokenState.accumulatedInputTokens - 100,
|
||||
deltaOutputTokens: tokenState.accumulatedOutputTokens - 20,
|
||||
};
|
||||
},
|
||||
},
|
||||
tkmindProxy: {},
|
||||
directChatService: {
|
||||
async respondDeterministically(options) {
|
||||
deliveries.push(options);
|
||||
await options.onSessionReady('h5direct_cursor_result');
|
||||
return { sessionId: 'h5direct_cursor_result' };
|
||||
},
|
||||
},
|
||||
toolGateway: {
|
||||
getStatus() {
|
||||
return {
|
||||
enabled: true,
|
||||
protocol: 'agent-run-v1',
|
||||
executors: ['cursor', 'aider', 'openhands'],
|
||||
};
|
||||
},
|
||||
async executeJob() {
|
||||
return {
|
||||
ok: true,
|
||||
dryRun: false,
|
||||
executor: 'cursor',
|
||||
exitCode: 0,
|
||||
cwd: workdir,
|
||||
stdout: '{"type":"result","subtype":"success","result":"done","usage":{"inputTokens":1000,"outputTokens":200}}',
|
||||
displayStdout: 'done',
|
||||
usage: { inputTokens: 1000, outputTokens: 200 },
|
||||
stderr: '',
|
||||
};
|
||||
},
|
||||
},
|
||||
retryDelaysMs: [],
|
||||
});
|
||||
|
||||
const run = await gateway.createRun('user-1', {
|
||||
requestId: 'req-cursor-billing',
|
||||
userMessage: {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: 'build the page' }],
|
||||
metadata: {
|
||||
displayText: 'build the page',
|
||||
memindRun: {
|
||||
requiredExecutor: 'cursor',
|
||||
},
|
||||
},
|
||||
},
|
||||
toolMode: 'code',
|
||||
taskType: 'h5_chat_code_task',
|
||||
});
|
||||
|
||||
await waitFor(() => pool.runs.get(run.id)?.status === 'succeeded');
|
||||
assert.equal(deliveries.length, 1);
|
||||
assert.match(deliveries[0].reply, /done/);
|
||||
assert.equal(billingCalls.length, 1);
|
||||
assert.equal(billingCalls[0].userId, 'user-1');
|
||||
assert.equal(billingCalls[0].sessionId, 'h5direct_cursor_result');
|
||||
assert.equal(billingCalls[0].requestId, 'req-cursor-billing');
|
||||
assert.deepEqual(billingCalls[0].tokenState, {
|
||||
accumulatedInputTokens: 1100,
|
||||
accumulatedOutputTokens: 220,
|
||||
});
|
||||
assert.equal(
|
||||
pool.events.some(
|
||||
(event) => event.runId === run.id && event.eventType === 'tool_gateway_billed',
|
||||
),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('agent run fails non-retryably when tool gateway artifact validation fails', async () => {
|
||||
const pool = createFakePool();
|
||||
const workdir = await fs.mkdtemp(path.join(os.tmpdir(), 'memind-tool-validation-missing-'));
|
||||
|
||||
Reference in New Issue
Block a user