feat(billing): bill Cursor executor usage via stream-json token parsing
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:
john
2026-08-27 10:16:38 +08:00
parent 884e819f70
commit f95d766f69
9 changed files with 344 additions and 8 deletions
+92
View File
@@ -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-'));