2024-08-24 04:36:08 -04:00
|
|
|
const mongoose = require('mongoose');
|
|
|
|
|
|
|
|
jest.mock('./Transaction', () => ({
|
|
|
|
Transaction: {
|
|
|
|
create: jest.fn(),
|
|
|
|
createStructured: jest.fn(),
|
|
|
|
},
|
|
|
|
}));
|
|
|
|
|
|
|
|
jest.mock('./Balance', () => ({
|
|
|
|
findOne: jest.fn(),
|
|
|
|
findOneAndUpdate: jest.fn(),
|
|
|
|
}));
|
|
|
|
|
|
|
|
jest.mock('~/config', () => ({
|
|
|
|
logger: {
|
|
|
|
debug: jest.fn(),
|
|
|
|
error: jest.fn(),
|
|
|
|
},
|
|
|
|
}));
|
|
|
|
|
2025-03-21 22:48:11 +01:00
|
|
|
// New config module
|
|
|
|
const { getBalanceConfig } = require('~/server/services/Config');
|
|
|
|
jest.mock('~/server/services/Config');
|
|
|
|
|
2024-08-24 04:36:08 -04:00
|
|
|
// Import after mocking
|
|
|
|
const { spendTokens, spendStructuredTokens } = require('./spendTokens');
|
|
|
|
const { Transaction } = require('./Transaction');
|
|
|
|
const Balance = require('./Balance');
|
2025-03-21 22:48:11 +01:00
|
|
|
|
2024-08-24 04:36:08 -04:00
|
|
|
describe('spendTokens', () => {
|
|
|
|
beforeEach(() => {
|
|
|
|
jest.clearAllMocks();
|
2025-03-21 22:48:11 +01:00
|
|
|
getBalanceConfig.mockResolvedValue({ enabled: true });
|
2024-08-24 04:36:08 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should create transactions for both prompt and completion tokens', async () => {
|
|
|
|
const txData = {
|
|
|
|
user: new mongoose.Types.ObjectId(),
|
|
|
|
conversationId: 'test-convo',
|
|
|
|
model: 'gpt-3.5-turbo',
|
|
|
|
context: 'test',
|
|
|
|
};
|
|
|
|
const tokenUsage = {
|
|
|
|
promptTokens: 100,
|
|
|
|
completionTokens: 50,
|
|
|
|
};
|
|
|
|
|
|
|
|
Transaction.create.mockResolvedValueOnce({ tokenType: 'prompt', rawAmount: -100 });
|
|
|
|
Transaction.create.mockResolvedValueOnce({ tokenType: 'completion', rawAmount: -50 });
|
|
|
|
Balance.findOne.mockResolvedValue({ tokenCredits: 10000 });
|
|
|
|
Balance.findOneAndUpdate.mockResolvedValue({ tokenCredits: 9850 });
|
|
|
|
|
|
|
|
await spendTokens(txData, tokenUsage);
|
|
|
|
|
|
|
|
expect(Transaction.create).toHaveBeenCalledTimes(2);
|
|
|
|
expect(Transaction.create).toHaveBeenCalledWith(
|
|
|
|
expect.objectContaining({
|
|
|
|
tokenType: 'prompt',
|
|
|
|
rawAmount: -100,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
expect(Transaction.create).toHaveBeenCalledWith(
|
|
|
|
expect.objectContaining({
|
|
|
|
tokenType: 'completion',
|
|
|
|
rawAmount: -50,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should handle zero completion tokens', async () => {
|
|
|
|
const txData = {
|
|
|
|
user: new mongoose.Types.ObjectId(),
|
|
|
|
conversationId: 'test-convo',
|
|
|
|
model: 'gpt-3.5-turbo',
|
|
|
|
context: 'test',
|
|
|
|
};
|
|
|
|
const tokenUsage = {
|
|
|
|
promptTokens: 100,
|
|
|
|
completionTokens: 0,
|
|
|
|
};
|
|
|
|
|
|
|
|
Transaction.create.mockResolvedValueOnce({ tokenType: 'prompt', rawAmount: -100 });
|
|
|
|
Transaction.create.mockResolvedValueOnce({ tokenType: 'completion', rawAmount: -0 });
|
|
|
|
Balance.findOne.mockResolvedValue({ tokenCredits: 10000 });
|
|
|
|
Balance.findOneAndUpdate.mockResolvedValue({ tokenCredits: 9850 });
|
|
|
|
|
|
|
|
await spendTokens(txData, tokenUsage);
|
|
|
|
|
|
|
|
expect(Transaction.create).toHaveBeenCalledTimes(2);
|
|
|
|
expect(Transaction.create).toHaveBeenCalledWith(
|
|
|
|
expect.objectContaining({
|
|
|
|
tokenType: 'prompt',
|
|
|
|
rawAmount: -100,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
expect(Transaction.create).toHaveBeenCalledWith(
|
|
|
|
expect.objectContaining({
|
|
|
|
tokenType: 'completion',
|
2025-03-21 22:48:11 +01:00
|
|
|
rawAmount: -0,
|
2024-08-24 04:36:08 -04:00
|
|
|
}),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should handle undefined token counts', async () => {
|
|
|
|
const txData = {
|
|
|
|
user: new mongoose.Types.ObjectId(),
|
|
|
|
conversationId: 'test-convo',
|
|
|
|
model: 'gpt-3.5-turbo',
|
|
|
|
context: 'test',
|
|
|
|
};
|
|
|
|
const tokenUsage = {};
|
|
|
|
|
|
|
|
await spendTokens(txData, tokenUsage);
|
|
|
|
|
|
|
|
expect(Transaction.create).not.toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
|
2025-03-21 22:48:11 +01:00
|
|
|
it('should not update balance when the balance feature is disabled', async () => {
|
|
|
|
// Override configuration: disable balance updates.
|
|
|
|
getBalanceConfig.mockResolvedValue({ enabled: false });
|
2024-08-24 04:36:08 -04:00
|
|
|
const txData = {
|
|
|
|
user: new mongoose.Types.ObjectId(),
|
|
|
|
conversationId: 'test-convo',
|
|
|
|
model: 'gpt-3.5-turbo',
|
|
|
|
context: 'test',
|
|
|
|
};
|
|
|
|
const tokenUsage = {
|
|
|
|
promptTokens: 100,
|
|
|
|
completionTokens: 50,
|
|
|
|
};
|
|
|
|
|
|
|
|
Transaction.create.mockResolvedValueOnce({ tokenType: 'prompt', rawAmount: -100 });
|
|
|
|
Transaction.create.mockResolvedValueOnce({ tokenType: 'completion', rawAmount: -50 });
|
|
|
|
|
|
|
|
await spendTokens(txData, tokenUsage);
|
|
|
|
|
|
|
|
expect(Transaction.create).toHaveBeenCalledTimes(2);
|
2025-03-21 22:48:11 +01:00
|
|
|
// When balance updates are disabled, Balance methods should not be called.
|
2024-08-24 04:36:08 -04:00
|
|
|
expect(Balance.findOne).not.toHaveBeenCalled();
|
|
|
|
expect(Balance.findOneAndUpdate).not.toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should create structured transactions for both prompt and completion tokens', async () => {
|
|
|
|
const txData = {
|
|
|
|
user: new mongoose.Types.ObjectId(),
|
|
|
|
conversationId: 'test-convo',
|
|
|
|
model: 'claude-3-5-sonnet',
|
|
|
|
context: 'test',
|
|
|
|
};
|
|
|
|
const tokenUsage = {
|
|
|
|
promptTokens: {
|
|
|
|
input: 10,
|
|
|
|
write: 100,
|
|
|
|
read: 5,
|
|
|
|
},
|
|
|
|
completionTokens: 50,
|
|
|
|
};
|
|
|
|
|
|
|
|
Transaction.createStructured.mockResolvedValueOnce({
|
|
|
|
rate: 3.75,
|
|
|
|
user: txData.user.toString(),
|
|
|
|
balance: 9570,
|
|
|
|
prompt: -430,
|
|
|
|
});
|
|
|
|
Transaction.create.mockResolvedValueOnce({
|
|
|
|
rate: 15,
|
|
|
|
user: txData.user.toString(),
|
|
|
|
balance: 8820,
|
|
|
|
completion: -750,
|
|
|
|
});
|
|
|
|
|
|
|
|
const result = await spendStructuredTokens(txData, tokenUsage);
|
|
|
|
|
|
|
|
expect(Transaction.createStructured).toHaveBeenCalledWith(
|
|
|
|
expect.objectContaining({
|
|
|
|
tokenType: 'prompt',
|
|
|
|
inputTokens: -10,
|
|
|
|
writeTokens: -100,
|
|
|
|
readTokens: -5,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
expect(Transaction.create).toHaveBeenCalledWith(
|
|
|
|
expect.objectContaining({
|
|
|
|
tokenType: 'completion',
|
|
|
|
rawAmount: -50,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
|
|
prompt: expect.objectContaining({
|
|
|
|
rate: 3.75,
|
|
|
|
user: txData.user.toString(),
|
|
|
|
balance: 9570,
|
|
|
|
prompt: -430,
|
|
|
|
}),
|
|
|
|
completion: expect.objectContaining({
|
|
|
|
rate: 15,
|
|
|
|
user: txData.user.toString(),
|
|
|
|
balance: 8820,
|
|
|
|
completion: -750,
|
|
|
|
}),
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|