feat: stream challenge submissions and tidy challenge imports

This commit is contained in:
Joey Yakimowich-Payne 2025-10-01 14:33:44 -06:00
commit cb4cde0ed2
No known key found for this signature in database
GPG key ID: 6BFE655FA5ABD1E1
13 changed files with 309 additions and 93 deletions

View file

@ -1,23 +1,33 @@
import { submitChallengeAttempt } from '@/service/challenges'
import { postPublic } from '@/service/base'
import { PUBLIC_API_PREFIX } from '@/config'
jest.mock('@/service/base', () => ({
getPublic: jest.fn(),
postPublic: jest.fn(),
}))
const mockedPostPublic = postPublic as jest.MockedFunction<typeof postPublic>
const originalFetch = globalThis.fetch
let fetchMock: jest.Mock
let ssePostMock: jest.SpyInstance
jest.mock('@/service/base', () => {
const actual = jest.requireActual('@/service/base')
return {
...actual,
ssePost: jest.fn(),
}
})
jest.mock('@/app/components/share/utils', () => ({
...jest.requireActual('@/app/components/share/utils'),
getInitialTokenV2: () => ({ version: 2 }),
isTokenV1: () => false,
}))
describe('submitChallengeAttempt', () => {
beforeEach(() => {
fetchMock = jest.fn()
globalThis.fetch = fetchMock as unknown as typeof fetch
mockedPostPublic.mockReset()
mockedPostPublic.mockResolvedValue({ result: 'success' } as any)
ssePostMock = jest.spyOn(require('@/service/base'), 'ssePost').mockImplementation((_url: string, _options: any, handlers: any) => {
handlers.onCompleted?.()
return Promise.resolve()
})
localStorage.clear()
})
@ -36,7 +46,7 @@ describe('submitChallengeAttempt', () => {
).rejects.toThrow('Challenge app is not published')
expect(fetchMock).not.toHaveBeenCalled()
expect(mockedPostPublic).not.toHaveBeenCalled()
expect(ssePostMock).not.toHaveBeenCalled()
})
it('requests a passport token and submits chat attempts through /chat-messages', async () => {
@ -46,6 +56,14 @@ describe('submitChallengeAttempt', () => {
json: jest.fn().mockResolvedValue({ access_token: passportToken }),
})
ssePostMock.mockImplementation((_url, _options, handlers) => {
handlers.getAbortController?.(new AbortController())
handlers.onData?.('Hello', true, { messageId: 'msg-1' } as any)
handlers.onMessageEnd?.({ metadata: { outputs: { challenge_succeeded: true }, answer: 'All good' } } as any)
handlers.onCompleted?.()
return Promise.resolve()
})
await submitChallengeAttempt('challenge-123', 'app-abc', 'site-code-xyz', 'chat', 'solve this')
expect(fetchMock).toHaveBeenCalledWith(`${PUBLIC_API_PREFIX}/passport`, {
@ -56,14 +74,14 @@ describe('submitChallengeAttempt', () => {
credentials: 'include',
})
expect(mockedPostPublic).toHaveBeenCalledWith('/chat-messages', expect.objectContaining({
expect(ssePostMock).toHaveBeenCalledWith('/chat-messages', expect.objectContaining({
body: {
query: 'solve this',
inputs: {},
response_mode: 'blocking',
response_mode: 'streaming',
conversation_id: '',
},
}))
}), expect.any(Object))
const storedToken = JSON.parse(localStorage.getItem('token') || '{}')
expect(storedToken.version).toBe(2)
@ -77,6 +95,14 @@ describe('submitChallengeAttempt', () => {
json: jest.fn().mockResolvedValue({ access_token: passportToken }),
})
ssePostMock.mockImplementation((_url, _options, handlers) => {
handlers.getAbortController?.(new AbortController())
handlers.onTextChunk?.({ data: { text: 'partial' } } as any)
handlers.onWorkflowFinished?.({ data: { outputs: { challenge_succeeded: false, message: 'nope' } } } as any)
handlers.onCompleted?.()
return Promise.resolve()
})
await submitChallengeAttempt('challenge-456', 'app-def', 'site-code-xyz', 'workflow', 'my answer')
expect(fetchMock).toHaveBeenCalledWith(`${PUBLIC_API_PREFIX}/passport`, {
@ -87,14 +113,14 @@ describe('submitChallengeAttempt', () => {
credentials: 'include',
})
expect(mockedPostPublic).toHaveBeenCalledWith('/workflows/run', expect.objectContaining({
expect(ssePostMock).toHaveBeenCalledWith('/workflows/run', expect.objectContaining({
body: {
inputs: {
user_prompt: 'my answer',
},
response_mode: 'blocking',
response_mode: 'streaming',
},
}))
}), expect.any(Object))
const storedToken = JSON.parse(localStorage.getItem('token') || '{}')
expect(storedToken['challenge-456'].DEFAULT).toBe(passportToken)