214 lines
7.1 KiB
Go
214 lines
7.1 KiB
Go
package logstore
|
|
|
|
import (
|
|
"context"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/maximhq/bifrost/core/schemas"
|
|
configstoreTables "github.com/maximhq/bifrost/framework/configstore/tables"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/valyala/fasthttp"
|
|
)
|
|
|
|
type asyncTestLogger struct{}
|
|
|
|
func (asyncTestLogger) Debug(string, ...any) {}
|
|
func (asyncTestLogger) Info(string, ...any) {}
|
|
func (asyncTestLogger) Warn(string, ...any) {}
|
|
func (asyncTestLogger) Error(string, ...any) {}
|
|
func (asyncTestLogger) Fatal(string, ...any) {}
|
|
func (asyncTestLogger) SetLevel(schemas.LogLevel) {}
|
|
func (asyncTestLogger) SetOutputType(schemas.LoggerOutputType) {}
|
|
func (asyncTestLogger) LogHTTPRequest(schemas.LogLevel, string) schemas.LogEventBuilder {
|
|
return schemas.NoopLogEvent
|
|
}
|
|
|
|
type testGovernanceStore struct {
|
|
virtualKeys map[string]*configstoreTables.TableVirtualKey
|
|
}
|
|
|
|
func (t *testGovernanceStore) GetVirtualKey(_ context.Context, vkValue string) (*configstoreTables.TableVirtualKey, bool) {
|
|
vk, ok := t.virtualKeys[vkValue]
|
|
return vk, ok
|
|
}
|
|
|
|
func newTestAsyncExecutor(t *testing.T) *AsyncJobExecutor {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
|
|
store, err := newSqliteLogStore(ctx, &SQLiteConfig{Path: ":memory:"}, asyncTestLogger{})
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { store.Close(ctx) })
|
|
|
|
govStore := &testGovernanceStore{
|
|
virtualKeys: map[string]*configstoreTables.TableVirtualKey{
|
|
"sk-bf-test": {ID: "vk-123", Value: "sk-bf-test"},
|
|
},
|
|
}
|
|
|
|
return NewAsyncJobExecutor(store, govStore, asyncTestLogger{})
|
|
}
|
|
|
|
// waitForJobCompletion polls until the operation callback has been invoked.
|
|
func waitForJobCompletion(t *testing.T, done *atomic.Bool) {
|
|
t.Helper()
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
if done.Load() {
|
|
return
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
t.Fatal("timed out waiting for async job execution")
|
|
}
|
|
|
|
// waitForJobStatus polls FindAsyncJobByID until the job reaches a terminal
|
|
// status (completed or failed), or times out. This avoids a fragile time.Sleep
|
|
// between the operation callback completing and the DB update finishing.
|
|
// Processing is intermediate and must not be treated as terminal.
|
|
func waitForJobStatus(t *testing.T, store LogStore, jobID string) *AsyncJob {
|
|
t.Helper()
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
job, err := store.FindAsyncJobByID(context.Background(), jobID)
|
|
if err == nil && (job.Status == schemas.AsyncJobStatusCompleted || job.Status == schemas.AsyncJobStatusFailed) {
|
|
return job
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
t.Fatal("timed out waiting for async job to reach terminal status")
|
|
return nil
|
|
}
|
|
|
|
func TestSubmitJob_PropagatesContextValues(t *testing.T) {
|
|
executor := newTestAsyncExecutor(t)
|
|
|
|
capturedCtx := schemas.NewBifrostContext(context.Background(), <-time.After(1*time.Minute))
|
|
capturedCtx.SetValue(schemas.BifrostContextKeyVirtualKey, "sk-bf-test")
|
|
capturedCtx.SetValue(schemas.BifrostContextKey("x-bf-eh-custom"), "custom-value")
|
|
capturedCtx.SetValue(schemas.BifrostContextKey("x-bf-prom-env"), "production")
|
|
var done atomic.Bool
|
|
|
|
operation := func(bgCtx *schemas.BifrostContext) (interface{}, *schemas.BifrostError) {
|
|
capturedCtx = bgCtx
|
|
done.Store(true)
|
|
return map[string]string{"status": "ok"}, nil
|
|
}
|
|
|
|
job, err := executor.SubmitJob(capturedCtx, 3600, operation, schemas.ChatCompletionRequest)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, job)
|
|
|
|
waitForJobCompletion(t, &done)
|
|
|
|
assert.Equal(t, "sk-bf-test", capturedCtx.Value(schemas.BifrostContextKeyVirtualKey))
|
|
assert.Equal(t, "production", capturedCtx.Value(schemas.BifrostContextKey("x-bf-prom-env")))
|
|
assert.Equal(t, "custom-value", capturedCtx.Value(schemas.BifrostContextKey("x-bf-eh-custom")))
|
|
assert.Equal(t, true, capturedCtx.Value(schemas.BifrostIsAsyncRequest))
|
|
}
|
|
|
|
func TestSubmitJob_NilContextValues(t *testing.T) {
|
|
executor := newTestAsyncExecutor(t)
|
|
|
|
var capturedCtx *schemas.BifrostContext
|
|
var done atomic.Bool
|
|
|
|
operation := func(bgCtx *schemas.BifrostContext) (interface{}, *schemas.BifrostError) {
|
|
capturedCtx = bgCtx
|
|
done.Store(true)
|
|
return map[string]string{"status": "ok"}, nil
|
|
}
|
|
|
|
job, err := executor.SubmitJob(capturedCtx, 3600, operation, schemas.ChatCompletionRequest)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, job)
|
|
|
|
waitForJobCompletion(t, &done)
|
|
|
|
assert.NotNil(t, capturedCtx)
|
|
assert.Equal(t, true, capturedCtx.Value(schemas.BifrostIsAsyncRequest))
|
|
}
|
|
|
|
func TestSubmitJob_EmptyContextValues(t *testing.T) {
|
|
executor := newTestAsyncExecutor(t)
|
|
|
|
var capturedCtx *schemas.BifrostContext
|
|
var done atomic.Bool
|
|
|
|
operation := func(bgCtx *schemas.BifrostContext) (interface{}, *schemas.BifrostError) {
|
|
capturedCtx = bgCtx
|
|
done.Store(true)
|
|
return map[string]string{"status": "ok"}, nil
|
|
}
|
|
|
|
job, err := executor.SubmitJob(capturedCtx, 3600, operation, schemas.ChatCompletionRequest)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, job)
|
|
|
|
waitForJobCompletion(t, &done)
|
|
|
|
assert.NotNil(t, capturedCtx)
|
|
assert.Equal(t, true, capturedCtx.Value(schemas.BifrostIsAsyncRequest))
|
|
}
|
|
|
|
func TestSubmitJob_AsyncFlagOverridesContextValues(t *testing.T) {
|
|
executor := newTestAsyncExecutor(t)
|
|
|
|
inputCtx := schemas.NewBifrostContext(context.Background(), schemas.NoDeadline)
|
|
inputCtx.SetValue(schemas.BifrostIsAsyncRequest, false)
|
|
|
|
var capturedCtx *schemas.BifrostContext
|
|
var done atomic.Bool
|
|
operation := func(bgCtx *schemas.BifrostContext) (interface{}, *schemas.BifrostError) {
|
|
capturedCtx = bgCtx
|
|
done.Store(true)
|
|
return map[string]string{"status": "ok"}, nil
|
|
}
|
|
|
|
job, err := executor.SubmitJob(inputCtx, 3600, operation, schemas.ChatCompletionRequest)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, job)
|
|
|
|
waitForJobCompletion(t, &done)
|
|
|
|
// BifrostIsAsyncRequest must be true — set AFTER restoring context values
|
|
assert.Equal(t, true, capturedCtx.Value(schemas.BifrostIsAsyncRequest))
|
|
}
|
|
|
|
func TestSubmitJob_OperationFailure_PreservesContext(t *testing.T) {
|
|
executor := newTestAsyncExecutor(t)
|
|
|
|
inputCtx := schemas.NewBifrostContext(context.Background(), schemas.NoDeadline)
|
|
inputCtx.SetValue(schemas.BifrostContextKeyVirtualKey, "sk-bf-test")
|
|
|
|
var capturedCtx *schemas.BifrostContext
|
|
var done atomic.Bool
|
|
|
|
statusCode := fasthttp.StatusBadRequest
|
|
operation := func(bgCtx *schemas.BifrostContext) (interface{}, *schemas.BifrostError) {
|
|
capturedCtx = bgCtx
|
|
done.Store(true)
|
|
return nil, &schemas.BifrostError{
|
|
StatusCode: &statusCode,
|
|
Error: &schemas.ErrorField{Message: "test error"},
|
|
}
|
|
}
|
|
|
|
job, err := executor.SubmitJob(inputCtx, 3600, operation, schemas.ChatCompletionRequest)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, job)
|
|
|
|
waitForJobCompletion(t, &done)
|
|
|
|
// Context values should still be available even when operation fails
|
|
assert.Equal(t, "sk-bf-test", capturedCtx.Value(schemas.BifrostContextKeyVirtualKey))
|
|
assert.Equal(t, true, capturedCtx.Value(schemas.BifrostIsAsyncRequest))
|
|
|
|
// Verify job was marked as failed — poll until DB update completes
|
|
retrievedJob := waitForJobStatus(t, executor.logstore, job.ID)
|
|
assert.Equal(t, schemas.AsyncJobStatusFailed, retrievedJob.Status)
|
|
}
|