Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions __tests__/useLiveVoiceChat.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,26 @@ describe('useLiveVoiceChat', () => {
)
})

test('startCall shows alert if credits are below the new gate of 5', async () => {
mockUseCharacter.mockReturnValue({ data: { id: 'char1', voice: 'en-US', save_to_cloud: 1 } })
mockUseCurrentPlan.mockReturnValue({ remainingCredits: 4 })

let hookRef: ReturnType<typeof useLiveVoiceChat> | null = null
await act(async () => {
create(<TestHarness onMount={(h) => { hookRef = h }} />)
})

await act(async () => {
await hookRef!.startCall()
})

expect(Alert.alert).toHaveBeenCalledWith(
'Insufficient Credits',
expect.any(String),
expect.any(Array),
)
})

test('startCall shows alert if save_to_cloud is disabled', async () => {
mockUseCharacter.mockReturnValue({ data: { id: 'char1', voice: 'en-US', save_to_cloud: 0 } })
mockUseCurrentPlan.mockReturnValue({ remainingCredits: 10 })
Expand Down
2 changes: 1 addition & 1 deletion app/index.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export default function WebIndex() {
<title>Clanker — Design, chat with, and share your own AI characters</title>
<meta
name="description"
content="Create AI characters, chat with them, and talk in real time with live voice calls. Hands-free conversations with web search and shared memory. 1 credit per minute for live voice."
content="Create AI characters, chat with them, and talk in real time with live voice calls. Hands-free conversations with web search and shared memory. 5 credits per minute for live voice."
/>
<meta property="og:title" content="Clanker" />
<meta
Expand Down
4 changes: 4 additions & 0 deletions cloud-agent/src/agent.live.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ test(
history: [],
timezone: 'America/New_York',
embed: mockEmbed,
creditService: {
spendCredit: async () => [{ transactionId: 'live-test-tx', amount: 1 }],
refundCredit: async () => {},
},
})

assert.ok(toolCalls.includes('get_current_time'), 'expected get_current_time to be called')
Expand Down
4 changes: 2 additions & 2 deletions cloud-agent/src/handlers/schedulerTriggerHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ function buildApp(overrides: {
sendProactive?: (token: string, sid: string, tid: string, body: string) => Promise<void>
getExpoPushToken?: (uid: string) => Promise<string | null>
resolveUserId?: (uid: string) => Promise<string | null>
spendCredit?: () => Promise<string>
spendCredit?: () => Promise<{ transactionId: string; amount: number }[]>
refundCredit?: () => Promise<void>
abortPendingTaskIfOffline?: () => Promise<boolean>
writeTaskResult?: () => Promise<void>
Expand Down Expand Up @@ -64,7 +64,7 @@ function buildApp(overrides: {
}

const mockCredit = {
spendCredit: overrides.spendCredit ?? (async () => { creditCalls.spend++; return 'tx1' }),
spendCredit: overrides.spendCredit ?? (async () => { creditCalls.spend++; return [{ transactionId: 'tx1', amount: 1 }] }),
refundCredit: overrides.refundCredit ?? (async () => { creditCalls.refund++ }),
}

Expand Down
28 changes: 21 additions & 7 deletions cloud-agent/src/handlers/schedulerTriggerHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { z } from 'zod'
import type { NextFunction, Request, Response } from 'express'
import type { FirestoreSession } from '../services/firestoreSession.js'
import type { FcmDispatcher } from '../services/fcmDispatcher.js'
import type { CreditService } from '../services/creditService.js'
import type { CreditService, CreditSpendAllocation } from '../services/creditService.js'
import type { TaskDoc } from '../../../shared/dsl-types.js'
import { singleActionSchema } from '../../../shared/dsl-schema.js'
import { intentRequiresAuth } from '../../../shared/constants.js'
Expand Down Expand Up @@ -181,10 +181,10 @@ export function createSchedulerTriggerHandler(
}
}

let txId: string | null = null
let allocations: CreditSpendAllocation[] | null = null
if (!isDuplicateRun) {
try {
txId = await creditService.spendCredit(userId)
allocations = await creditService.spendCredit(userId)
} catch (err) {
const msg = err instanceof Error ? err.message : ''
if (msg === 'INSUFFICIENT_CREDITS') {
Expand Down Expand Up @@ -212,8 +212,15 @@ export function createSchedulerTriggerHandler(
await fs.writeTask(uid, activeSessionId, activeTaskId, taskIntent)
} catch (err) {
console.error('[scheduler-trigger] setup error:', err)
if (txId) {
try { await creditService.refundCredit(userId, txId) } catch { /* logged */ }
if (allocations) {
try {
await creditService.refundCredit(userId, allocations)
} catch (refundErr) {
console.warn('[scheduler-trigger] refundCredit failed during setup rollback:', {
allocations,
error: refundErr instanceof Error ? refundErr.message : refundErr,
})
}
}
try { await fs.closeSession(uid, activeSessionId, 'aborted') } catch { /* ignore */ }
res.status(500).json({ error: 'Internal server error' })
Expand Down Expand Up @@ -259,8 +266,15 @@ export function createSchedulerTriggerHandler(
} catch { /* ignore */ }
}

if (!isDuplicateRun && abortedOffline && txId) {
try { await creditService.refundCredit(userId, txId) } catch { /* logged */ }
if (!isDuplicateRun && abortedOffline && allocations) {
try {
await creditService.refundCredit(userId, allocations)
} catch (refundErr) {
console.warn('[scheduler-trigger] refundCredit failed during offline rollback:', {
allocations,
error: refundErr instanceof Error ? refundErr.message : refundErr,
})
}
}

try { await fs.closeSession(uid, activeSessionId, 'aborted') } catch { /* ignore */ }
Expand Down
8 changes: 4 additions & 4 deletions cloud-agent/src/handlers/wsAgentHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ const mockCharacter = {
const CHAR_UUID = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'

const mockCreditService = {
spendCredit: async (_userId: string): Promise<string> => 'mock-txid',
refundCredit: async (_userId: string, _txId: string): Promise<void> => {},
spendCredit: async (_userId: string): Promise<{ transactionId: string; amount: number }[]> => [{ transactionId: 'mock-txid', amount: 1 }],
refundCredit: async (_userId: string, _allocations: { transactionId: string; amount: number }[]): Promise<void> => {},
getBalance: async (_userId: string): Promise<number> => 42,
}

Expand Down Expand Up @@ -230,11 +230,11 @@ test('times out if auth message not sent within 5 seconds', { timeout: 10_000 },
await close()
})

test('returns INSUFFICIENT_CREDITS error when spendCredit fails', async () => {
test('returns INSUFFICIENT_CREDITS error when balance is zero before agent starts', async () => {
const db = makeMockDb([[mockUser], [mockCharacter]])
const cs = {
...mockCreditService,
spendCredit: async (): Promise<string> => { throw new Error('INSUFFICIENT_CREDITS') },
getBalance: async () => 0,
}
const { server, close } = createTestWsServer({
db,
Expand Down
Loading