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
87 changes: 60 additions & 27 deletions relay/helper/stream_scanner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,58 +302,91 @@ func TestStreamScannerHandler_ScannerDecoupledFromSlowHandler(t *testing.T) {
t.Parallel()

const numChunks = 50
const upstreamDelay = 10 * time.Millisecond
const handlerDelay = 20 * time.Millisecond

pr, pw := io.Pipe()
var upstreamBusy atomic.Int64
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
handlerEntered := make(chan struct{})
releaseHandler := make(chan struct{})
readAhead := make(chan struct{})
producerDone := make(chan struct{})
done := make(chan struct{})
var releaseOnce sync.Once
t.Cleanup(func() {
cancel()
releaseOnce.Do(func() { close(releaseHandler) })
_ = pr.Close()
_ = pw.Close()
for _, worker := range []<-chan struct{}{producerDone, done} {
select {
case <-worker:
case <-time.After(time.Second):
t.Error("stream test worker did not stop after cancellation")
}
}
})
go func() {
defer close(producerDone)
defer pw.Close()
feedStart := time.Now()
for i := 0; i < numChunks; i++ {
fmt.Fprintf(pw, "data: {\"id\":%d}\n", i)
time.Sleep(upstreamDelay)
if _, err := fmt.Fprintf(pw, "data: {\"id\":%d}\n", i); err != nil {
return
}
if i == 1 {
close(readAhead)
}
}
fmt.Fprint(pw, "data: [DONE]\n")
upstreamBusy.Store(int64(time.Since(feedStart)))
}()

recorder := httptest.NewRecorder()
c, _ := gin.CreateTestContext(recorder)
c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil)
c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil).WithContext(ctx)

resp := &http.Response{Body: pr}
info := &relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{}}

var count atomic.Int64
var handlerBusy atomic.Int64
start := time.Now()
done := make(chan struct{})
var received []string
go func() {
defer close(done)
StreamScannerHandler(c, resp, info, func(data string, sr *StreamResult) {
chunkStart := time.Now()
time.Sleep(handlerDelay)
handlerBusy.Add(int64(time.Since(chunkStart)))
count.Add(1)
if count.Add(1) == 1 {
close(handlerEntered)
select {
case <-releaseHandler:
case <-ctx.Done():
return
}
}
received = append(received, data)
})
close(done)
}()

// A coupled scanner cannot read the second pipe write while its first
// callback is blocked. Synchronize that contract without sleep-speed ratios.
for _, phase := range []struct {
name string
done <-chan struct{}
}{{"first handler", handlerEntered}, {"scanner read-ahead", readAhead}} {
select {
case <-phase.done:
case <-ctx.Done():
t.Fatalf("timed out waiting for %s", phase.name)
}
}
assert.Equal(t, int64(1), count.Load(), "only the blocked first callback can run")
releaseOnce.Do(func() { close(releaseHandler) })
select {
case <-done:
case <-time.After(15 * time.Second):
case <-ctx.Done():
t.Fatal("StreamScannerHandler did not complete in time")
}

elapsed := time.Since(start)
assert.Equal(t, int64(numChunks), count.Load())

// 用实测耗时估算"耦合"基线,避免系统繁忙时 sleep 超时偏移导致名义值失真
coupledTime := time.Duration(upstreamBusy.Load() + handlerBusy.Load())
t.Logf("elapsed=%v, coupled_estimate=%v", elapsed, coupledTime)

assert.Less(t, elapsed, coupledTime*85/100,
"decoupled elapsed time (%v) should be significantly less than coupled estimate (%v)", elapsed, coupledTime)
assert.Equal(t, numChunks, info.ReceivedResponseCount)
require.Len(t, received, numChunks)
for i, data := range received {
assert.Equal(t, fmt.Sprintf("{\"id\":%d}", i), data)
}
assert.Equal(t, relaycommon.StreamEndReasonDone, info.StreamStatus.EndReason)
}

func TestStreamScannerHandler_SlowUpstreamFastHandler(t *testing.T) {
Expand Down
9 changes: 9 additions & 0 deletions web/default/scripts/run-tests.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -274,13 +274,22 @@ function runTestFiles(files, label, bunArguments) {
if (files.length === 0) return 'skipped'

console.log(`\n==> ${label} (${files.length} file${files.length === 1 ? '' : 's'})`)
const startedAt = performance.now()
const result = Bun.spawnSync({
cmd: buildTestCommand(files, bunArguments),
cwd: projectRoot,
env: process.env,
stdout: 'pipe',
stderr: 'pipe',
})
console.log(
`MAX_API_TEST_STAGE ${JSON.stringify({
label,
fileCount: files.length,
exitCode: result.exitCode,
wallMilliseconds: Math.round(performance.now() - startedAt),
})}`
)
writeChildOutput(result)
if (result.exitCode === 0) return 'passed'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import type { ApiKeyGroupOption } from './api-key-group-combobox'
let ApiKeyRoutingEditor: typeof import('./api-key-routing-editor').ApiKeyRoutingEditor
let cleanup: typeof import('@testing-library/react/pure').cleanup
let fireEvent: typeof import('@testing-library/react/pure').fireEvent
let isInaccessible: typeof import('@testing-library/react/pure').isInaccessible
let render: typeof import('@testing-library/react/pure').render
let within: typeof import('@testing-library/react/pure').within

Expand Down Expand Up @@ -87,7 +88,7 @@ before(async (): Promise<void> => {
}),
})
// React and Base UI must detect the browser before mounting interactive portals.
;({ cleanup, fireEvent, render, within } =
;({ cleanup, fireEvent, isInaccessible, render, within } =
await import('@testing-library/react/pure'))
;({ ApiKeyRoutingEditor } = await import('./api-key-routing-editor'))
})
Expand Down Expand Up @@ -147,6 +148,37 @@ function RoutingHarness(props: RoutingHarnessProps): ReactElement {
)
}

async function findRoutingOptions(
view: ReturnType<typeof render>
): Promise<Map<string, HTMLElement>> {
// Compute each visible option's accessible name once instead of rescanning
// every option for each group. Keep the same role, visibility and name checks.
const names = new Map<Element, string>()
const options = await view.findAllByRole('option', {
name: (name: string, element: Element): boolean => {
names.set(element, name)
return true
},
})
const result = new Map<string, HTMLElement>()
for (const option of options) {
const name = names.get(option)
assert.ok(name !== undefined)
assert.equal(result.has(name), false, `duplicate option name: ${name}`)
result.set(name, option)
}
return result
}

function routingOption(
options: Map<string, HTMLElement>,
name: string
): HTMLElement {
const option = options.get(name)
assert.ok(option, `missing accessible routing option: ${name}`)
return option
}

describe('ApiKeyRoutingEditor', (): void => {
for (const initialGroups of [[], groups]) {
test(`clears ${initialGroups.length} stored manual selections when automatic routing is disabled`, async (): Promise<void> => {
Expand All @@ -164,15 +196,13 @@ describe('ApiKeyRoutingEditor', (): void => {
)

fireEvent.click(view.getByRole('combobox', { name: 'Select groups' }))
const options = await view.findAllByRole('option')
assert.equal(options.length, availableGroups.length)
const options = await findRoutingOptions(view)
assert.equal(options.size, availableGroups.length)
for (const group of availableGroups) {
const option = view.getByRole('option', {
name: `${group}1x Ratio`,
})
const option = routingOption(options, `${group}1x Ratio`)
assert.notEqual(option.getAttribute('aria-disabled'), 'true')
}
fireEvent.click(view.getByRole('option', { name: 'group-141x Ratio' }))
fireEvent.click(routingOption(options, 'group-141x Ratio'))
within(view.getByRole('combobox', { name: 'Select groups' })).getByText(
'group-14'
)
Expand Down Expand Up @@ -225,27 +255,28 @@ describe('ApiKeyRoutingEditor', (): void => {
fireEvent.click(trigger)

assert.equal(trigger.getAttribute('aria-expanded'), 'true')
const options = await view.findAllByRole('option')
assert.equal(options.length, availableGroups.length)
const options = await findRoutingOptions(view)
assert.equal(options.size, availableGroups.length)
for (const group of availableGroups) {
const option = view.getByRole('option', {
name: `${group}${group}1x Ratio`,
})
const option = routingOption(options, `${group}${group}1x Ratio`)
assert.equal(
option.getAttribute('aria-disabled'),
String(!groups.includes(group))
)
}
fireEvent.click(
view.getByRole('option', { name: 'group-14group-141x Ratio' })
)
fireEvent.click(routingOption(options, 'group-14group-141x Ratio'))
assert.equal(changedGroups, undefined)

const removeButtons = view.getAllByRole('button', {
name: /^Remove group-/,
})
const removeButtons = view.getAllByLabelText(/^Remove group-/)
assert.equal(removeButtons.length, MAX_MANUAL_ROUTING_GROUPS)
for (const removeButton of removeButtons) {
assert.equal(removeButton.tagName, 'BUTTON')
assert.ok(
removeButton.getAttribute('role') === null ||
removeButton.getAttribute('role') === 'button'
)
assert.equal(removeButton.getAttribute('aria-labelledby'), null)
assert.equal(isInaccessible(removeButton), false)
assert.equal(trigger.contains(removeButton), false)
}

Expand Down
44 changes: 27 additions & 17 deletions web/default/src/features/smart-ops/alerts.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ function ManualSettlementEvidenceHarness(): ReactElement {

return (
<>
<button type='button'>Unrelated control</button>
<button type='button' onClick={() => setFailed(true)}>
Cause reconciliation error
</button>
Expand Down Expand Up @@ -662,22 +663,31 @@ describe('SmartOps active alerts', () => {
)

try {
await waitFor(() => {
assert.ok(
within(view.container).getByRole('button', {
name: 'Enter exact quota',
})
)
const { exactQuotaButton, causeErrorButton } = await waitFor(() => {
// Resolve both controls by role and accessible name in one scan.
const names = new Map<Element, string>()
const buttons = within(view.container).getAllByRole('button', {
name: (name: string, element: Element): boolean => {
names.set(element, name)
return true
},
})
const namedButton = (name: string): HTMLElement => {
const matches = buttons.filter((button) => names.get(button) === name)
assert.equal(
matches.length,
1,
`expected one accessible ${name} button`
)
return matches[0]
}
return {
exactQuotaButton: namedButton('Enter exact quota'),
causeErrorButton: namedButton('Cause reconciliation error'),
}
})
const causeErrorButton = view.container.querySelector(
'button'
) as HTMLButtonElement
assert.equal(causeErrorButton.textContent, 'Cause reconciliation error')
await view.click(
within(view.container).getByRole('button', {
name: 'Enter exact quota',
})
)
await view.click(exactQuotaButton)

const dialog = await waitFor(() => {
const currentDialog = document.body.querySelector(
Expand Down Expand Up @@ -748,6 +758,9 @@ describe('SmartOps active alerts', () => {
)

try {
const causeErrorButton = within(view.container).getByRole('button', {
name: 'Cause reconciliation error',
})
await view.click(
await waitFor(() =>
within(view.container).getByRole('checkbox', {
Expand Down Expand Up @@ -785,9 +798,6 @@ describe('SmartOps active alerts', () => {
return button
})

const causeErrorButton = view.container.querySelector(
'button'
) as HTMLButtonElement
assert.equal(causeErrorButton.textContent, 'Cause reconciliation error')
await view.click(causeErrorButton)
await waitFor(() => {
Expand Down
Loading