mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-07-30 11:25:08 +00:00
Offline, CI-safe (70 tests): - sync.test.ts (26): discovery parsing, PKCE, auth URL, scope negotiation, callback server (state mismatch, IdP error, timeout, port fallback), config round-trip - sync-ledger-otlp.test.ts (23): deterministic ID derivation, OTLP structure/attributes, batching, ledger append/dedup/prune/clear - sync-push.test.ts (15): pipeline against a scriptable mock OTLP server — success+ledger+cost, partial-success batch NOT ledgered, 401 stop, 429 wait-and-retry / persistent-429 give-up / Retry-After parsing (delta-seconds, HTTP-date, garbage), 5xx deferral, idempotent failure recovery - sync-e2e.test.ts (6) + fixtures/mock-idp.ts: full setup→refresh→ rotate→logout round-trip against an in-process mock IdP Developer-only (skip unless env vars set, never CI): - sync-headless-e2e.test.ts (1): real browser PKCE flow via Playwright - sync-infra-e2e.test.ts (4): push/auth-reject/batch/idempotent re-push against a deployed backend AI-Origin: human
232 lines
7.1 KiB
TypeScript
232 lines
7.1 KiB
TypeScript
/**
|
|
* Integration test: codeburn sync push against real AWS infrastructure.
|
|
*
|
|
* Requires:
|
|
* - Deployed CodeburnSyncBackend stack
|
|
* - Environment variables: CODEBURN_SYNC_URL, CODEBURN_SYNC_EMAIL, CODEBURN_SYNC_PASSWORD
|
|
* - AWS credentials (for verifying CloudWatch logs)
|
|
*
|
|
* Run:
|
|
* CODEBURN_SYNC_URL=https://xxx.execute-api.us-west-2.amazonaws.com \
|
|
* CODEBURN_SYNC_EMAIL=andklee@amazon.com \
|
|
* CODEBURN_SYNC_PASSWORD='password' \
|
|
* AWS_PROFILE=andklee-dev \
|
|
* npx vitest run tests/sync-infra-e2e.test.ts
|
|
*/
|
|
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
import { randomBytes, createHash } from 'crypto'
|
|
|
|
import { fetchDiscoveryDoc } from '../src/sync/discovery.js'
|
|
import { fetchOidcConfig, refreshToken } from '../src/sync/auth.js'
|
|
import { buildOtlpPayload, deriveSpanId, type CallWithSession } from '../src/sync/otlp.js'
|
|
import type { ParsedApiCall, TokenUsage } from '../src/types.js'
|
|
|
|
const BASE_URL = process.env.CODEBURN_SYNC_URL
|
|
const TEST_EMAIL = process.env.CODEBURN_SYNC_EMAIL
|
|
const TEST_PASSWORD = process.env.CODEBURN_SYNC_PASSWORD
|
|
const AWS_REGION = process.env.AWS_REGION || 'us-west-2'
|
|
|
|
const SKIP = !BASE_URL || !TEST_EMAIL || !TEST_PASSWORD
|
|
|
|
describe.skipIf(SKIP)('sync infra e2e — push + verify', () => {
|
|
let accessToken: string
|
|
let clientId: string
|
|
let tracesEndpoint: string
|
|
const testRunId = randomBytes(4).toString('hex')
|
|
|
|
beforeAll(async () => {
|
|
// Get tokens via admin API (Cognito USER_PASSWORD_AUTH)
|
|
const discovery = await fetchDiscoveryDoc(BASE_URL!)
|
|
clientId = discovery.client_id
|
|
tracesEndpoint = `${BASE_URL}${discovery.traces_path}`
|
|
|
|
// Direct auth (USER_PASSWORD_AUTH) for testing
|
|
const tokenEndpoint = `https://codeburn-sync.auth.${AWS_REGION}.amazoncognito.com/oauth2/token`
|
|
|
|
// Use Cognito initiateAuth via fetch to the Cognito service
|
|
const cognitoUrl = `https://cognito-idp.${AWS_REGION}.amazonaws.com/`
|
|
const userPoolId = discovery.issuer.split('/').pop()!
|
|
|
|
const authResp = await fetch(cognitoUrl, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-amz-json-1.1',
|
|
'X-Amz-Target': 'AWSCognitoIdentityProviderService.InitiateAuth',
|
|
},
|
|
body: JSON.stringify({
|
|
AuthFlow: 'USER_PASSWORD_AUTH',
|
|
ClientId: clientId,
|
|
AuthParameters: {
|
|
USERNAME: TEST_EMAIL,
|
|
PASSWORD: TEST_PASSWORD,
|
|
},
|
|
}),
|
|
})
|
|
|
|
const authResult = await authResp.json() as { AuthenticationResult?: { AccessToken?: string } }
|
|
accessToken = authResult.AuthenticationResult?.AccessToken ?? ''
|
|
expect(accessToken).toBeTruthy()
|
|
})
|
|
|
|
it('pushes a test span and receives 200', async () => {
|
|
const testDedup = `test:infra-e2e:${testRunId}:span1`
|
|
const usage: TokenUsage = {
|
|
inputTokens: 999,
|
|
outputTokens: 111,
|
|
cacheCreationInputTokens: 0,
|
|
cacheReadInputTokens: 0,
|
|
cachedInputTokens: 0,
|
|
reasoningTokens: 0,
|
|
webSearchRequests: 0,
|
|
}
|
|
|
|
const call: ParsedApiCall = {
|
|
provider: 'test',
|
|
model: 'test-model',
|
|
usage,
|
|
costUSD: 0.001,
|
|
tools: ['TestTool'],
|
|
mcpTools: [],
|
|
skills: [],
|
|
subagentTypes: [],
|
|
hasAgentSpawn: false,
|
|
hasPlanMode: false,
|
|
speed: 'standard',
|
|
timestamp: new Date().toISOString(),
|
|
bashCommands: [],
|
|
deduplicationKey: testDedup,
|
|
}
|
|
|
|
const callWithSession: CallWithSession = {
|
|
call,
|
|
sessionId: `test-session-${testRunId}`,
|
|
project: 'infra-e2e-test',
|
|
}
|
|
|
|
const payload = buildOtlpPayload([callWithSession])
|
|
|
|
const response = await fetch(tracesEndpoint, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${accessToken}`,
|
|
},
|
|
body: JSON.stringify(payload),
|
|
})
|
|
|
|
expect(response.status).toBe(200)
|
|
const body = await response.json() as Record<string, unknown>
|
|
// No partial_success means full success
|
|
expect(body.partialSuccess?.rejectedSpans).toBeUndefined()
|
|
})
|
|
|
|
it('rejects unauthenticated requests', async () => {
|
|
const payload = buildOtlpPayload([])
|
|
|
|
const response = await fetch(tracesEndpoint, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': 'Bearer invalid-token',
|
|
},
|
|
body: JSON.stringify(payload),
|
|
})
|
|
|
|
expect(response.status).toBe(401)
|
|
})
|
|
|
|
it('pushes multiple spans in one batch', async () => {
|
|
const calls: CallWithSession[] = Array.from({ length: 5 }, (_, i) => {
|
|
const usage: TokenUsage = {
|
|
inputTokens: 100 * (i + 1),
|
|
outputTokens: 50 * (i + 1),
|
|
cacheCreationInputTokens: 0,
|
|
cacheReadInputTokens: 0,
|
|
cachedInputTokens: 0,
|
|
reasoningTokens: 0,
|
|
webSearchRequests: 0,
|
|
}
|
|
return {
|
|
call: {
|
|
provider: 'test',
|
|
model: 'test-batch-model',
|
|
usage,
|
|
costUSD: 0.01 * (i + 1),
|
|
tools: [],
|
|
mcpTools: [],
|
|
skills: [],
|
|
subagentTypes: [],
|
|
hasAgentSpawn: false,
|
|
hasPlanMode: false,
|
|
speed: 'standard' as const,
|
|
timestamp: new Date().toISOString(),
|
|
bashCommands: [],
|
|
deduplicationKey: `test:infra-e2e:${testRunId}:batch:${i}`,
|
|
},
|
|
sessionId: `test-batch-session-${testRunId}`,
|
|
project: 'infra-e2e-batch',
|
|
}
|
|
})
|
|
|
|
const payload = buildOtlpPayload(calls)
|
|
|
|
const response = await fetch(tracesEndpoint, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${accessToken}`,
|
|
},
|
|
body: JSON.stringify(payload),
|
|
})
|
|
|
|
expect(response.status).toBe(200)
|
|
})
|
|
|
|
it('deterministic span IDs allow safe re-push', async () => {
|
|
const dedup = `test:infra-e2e:${testRunId}:idempotent`
|
|
const spanId = deriveSpanId(dedup)
|
|
|
|
// Push same data twice
|
|
const call: CallWithSession = {
|
|
call: {
|
|
provider: 'test',
|
|
model: 'test-idempotent',
|
|
usage: { inputTokens: 1, outputTokens: 1, cacheCreationInputTokens: 0, cacheReadInputTokens: 0, cachedInputTokens: 0, reasoningTokens: 0, webSearchRequests: 0 },
|
|
costUSD: 0,
|
|
tools: [],
|
|
mcpTools: [],
|
|
skills: [],
|
|
subagentTypes: [],
|
|
hasAgentSpawn: false,
|
|
hasPlanMode: false,
|
|
speed: 'standard',
|
|
timestamp: new Date().toISOString(),
|
|
bashCommands: [],
|
|
deduplicationKey: dedup,
|
|
},
|
|
sessionId: 'idem-session',
|
|
project: 'idem-project',
|
|
}
|
|
|
|
const payload = buildOtlpPayload([call])
|
|
|
|
// First push
|
|
const r1 = await fetch(tracesEndpoint, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}` },
|
|
body: JSON.stringify(payload),
|
|
})
|
|
expect(r1.status).toBe(200)
|
|
|
|
// Second push (identical payload)
|
|
const r2 = await fetch(tracesEndpoint, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}` },
|
|
body: JSON.stringify(payload),
|
|
})
|
|
expect(r2.status).toBe(200)
|
|
|
|
// Both succeed — server tolerates duplicates per OTLP spec
|
|
})
|
|
})
|