606 lines
29 KiB
Go
606 lines
29 KiB
Go
package plugin
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"net/url"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
func managementCall(t *testing.T, app *App, method, path string) ManagementResponse {
|
||
return managementCallBody(t, app, method, path, nil)
|
||
}
|
||
|
||
func managementCallBody(t *testing.T, app *App, method, path string, body []byte) ManagementResponse {
|
||
return managementCallRequest(t, app, ManagementRequest{Method: method, Path: path, Body: body})
|
||
}
|
||
|
||
func managementCallRequest(t *testing.T, app *App, requestValue ManagementRequest) ManagementResponse {
|
||
t.Helper()
|
||
request, err := json.Marshal(requestValue)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
raw, err := app.HandleMethod(MethodManagementHandle, request)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
var envelope Envelope
|
||
if err := json.Unmarshal(raw, &envelope); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
var response ManagementResponse
|
||
if err := json.Unmarshal(envelope.Result, &response); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return response
|
||
}
|
||
|
||
func TestManagementRegistrationDeclaresUsageAPIAndUI(t *testing.T) {
|
||
raw, err := NewApp().HandleMethod(MethodManagementRegister, nil)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
var envelope Envelope
|
||
if err := json.Unmarshal(raw, &envelope); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
var registration ManagementRegistrationResponse
|
||
if err := json.Unmarshal(envelope.Result, ®istration); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(registration.Routes) != 18 || registration.Routes[0].Path != managementBase+routeUsage || registration.Routes[1].Path != managementBase+routeUsageSummary || registration.Routes[2].Path != managementBase+routePrices {
|
||
t.Fatalf("unexpected management routes: %+v", registration.Routes)
|
||
}
|
||
if len(registration.Resources) != 1 || registration.Resources[0].Path != resourceBase+resourceUI {
|
||
t.Fatalf("unexpected resource routes: %+v", registration.Resources)
|
||
}
|
||
}
|
||
|
||
func TestReadOnlyResourceReturnsRealDataWithoutSecrets(t *testing.T) {
|
||
app := NewApp()
|
||
defer app.Shutdown()
|
||
if _, err := app.HandleMethod(MethodPluginRegister, lifecycleRequest(t, SchemaVersion, testConfig(t, "enabled: true\ncodex_only: false\n"))); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
keys := managementCallRequest(t, app, ManagementRequest{
|
||
Method: http.MethodGet,
|
||
Path: resourceBase + resourceUI,
|
||
Query: url.Values{"view": {"keys"}},
|
||
})
|
||
if keys.StatusCode != http.StatusOK || !strings.Contains(string(keys.Body), `"masked_secret":"00******0000"`) {
|
||
t.Fatalf("unexpected read-only keys: status=%d body=%s", keys.StatusCode, keys.Body)
|
||
}
|
||
if strings.Contains(string(keys.Body), `"secret":`) {
|
||
t.Fatalf("read-only response exposed a complete secret: %s", keys.Body)
|
||
}
|
||
usageRaw, _ := json.Marshal(UsageRecord{Provider: "openai", Model: "gpt-test", APIKey: "000000", RequestedAt: time.Now(), Detail: UsageDetail{TotalTokens: 1}})
|
||
if _, err := app.HandleMethod(MethodUsageHandle, usageRaw); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
usage := managementCallRequest(t, app, ManagementRequest{
|
||
Method: http.MethodGet,
|
||
Path: resourceBase + resourceUI,
|
||
Query: url.Values{"view": {"usage"}},
|
||
})
|
||
if usage.StatusCode != http.StatusOK || strings.Contains(string(usage.Body), `"api_key":"000000"`) {
|
||
t.Fatalf("read-only usage exposed a historical key: status=%d body=%s", usage.StatusCode, usage.Body)
|
||
}
|
||
|
||
invalid := managementCallRequest(t, app, ManagementRequest{
|
||
Method: http.MethodGet,
|
||
Path: resourceBase + resourceUI,
|
||
Query: url.Values{"view": {"missing"}},
|
||
})
|
||
if invalid.StatusCode != http.StatusBadRequest || !strings.Contains(string(invalid.Body), `"code":"invalid_view"`) {
|
||
t.Fatalf("unexpected invalid view response: status=%d body=%s", invalid.StatusCode, invalid.Body)
|
||
}
|
||
}
|
||
|
||
func TestModelsDevCatalogRequiresExplicitImportAndConfirmedRefresh(t *testing.T) {
|
||
catalogBody := `{"providers":{"openai":{"id":"openai","name":"OpenAI","models":{"gpt-5.6-sol":{"id":"gpt-5.6-sol","name":"GPT-5.6 Sol","cost":{"input":5,"output":30,"cache_read":0.5,"cache_write":6.25}}}},"free":{"id":"free","name":"Free","models":{"placeholder":{"id":"placeholder","cost":{"input":0,"output":0}}}}}}`
|
||
serverStatus := http.StatusOK
|
||
server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, _ *http.Request) {
|
||
response.WriteHeader(serverStatus)
|
||
_, _ = response.Write([]byte(catalogBody))
|
||
}))
|
||
defer server.Close()
|
||
|
||
directory := t.TempDir()
|
||
config := fmt.Sprintf("database_path: %q\nmodels_dev_url: %q\nmodels_dev_cache_path: %q\nenabled: true\ncodex_only: false\n",
|
||
filepath.Join(directory, "usage.db"), server.URL, filepath.Join(directory, "catalog.json"))
|
||
app := NewApp()
|
||
defer app.Shutdown()
|
||
if _, err := app.HandleMethod(MethodPluginRegister, lifecycleRequest(t, SchemaVersion, config)); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
status := managementCallRequest(t, app, ManagementRequest{Method: http.MethodGet, Path: managementBase + routeCatalog, Query: url.Values{"q": {"gpt"}}})
|
||
if status.StatusCode != http.StatusOK || !strings.Contains(string(status.Body), `"loaded":false`) {
|
||
t.Fatalf("catalog status=%d body=%s", status.StatusCode, status.Body)
|
||
}
|
||
refresh := managementCall(t, app, http.MethodPost, managementBase+routeCatalogRefresh)
|
||
if refresh.StatusCode != http.StatusOK || !strings.Contains(string(refresh.Body), `"changed":0`) {
|
||
t.Fatalf("initial refresh status=%d body=%s", refresh.StatusCode, refresh.Body)
|
||
}
|
||
search := managementCallRequest(t, app, ManagementRequest{Method: http.MethodGet, Path: managementBase + routeCatalog, Query: url.Values{"q": {"gpt-5.6"}}})
|
||
if search.StatusCode != http.StatusOK || !strings.Contains(string(search.Body), `"id":"openai/gpt-5.6-sol"`) || strings.Contains(string(search.Body), "placeholder") {
|
||
t.Fatalf("catalog search status=%d body=%s", search.StatusCode, search.Body)
|
||
}
|
||
|
||
imported := managementCallBody(t, app, http.MethodPost, managementBase+routePriceImport, []byte(`{"model":"deepseek-v4-flash","catalog_id":"openai/gpt-5.6-sol"}`))
|
||
if imported.StatusCode != http.StatusOK || !strings.Contains(string(imported.Body), `"input_per_1m":"5"`) || !strings.Contains(string(imported.Body), `"kind":"models.dev"`) {
|
||
t.Fatalf("catalog import status=%d body=%s", imported.StatusCode, imported.Body)
|
||
}
|
||
|
||
catalogBody = strings.Replace(catalogBody, `"input":5`, `"input":6`, 1)
|
||
preview := managementCall(t, app, http.MethodPost, managementBase+routeCatalogRefresh)
|
||
if preview.StatusCode != http.StatusOK || !strings.Contains(string(preview.Body), `"changed":1`) || !strings.Contains(string(preview.Body), `"input_per_1m":"6"`) {
|
||
t.Fatalf("refresh preview status=%d body=%s", preview.StatusCode, preview.Body)
|
||
}
|
||
pricesBefore := managementCall(t, app, http.MethodGet, managementBase+routePrices)
|
||
if !strings.Contains(string(pricesBefore.Body), `"input_per_1m":"5"`) {
|
||
t.Fatalf("preview silently changed local price: %s", pricesBefore.Body)
|
||
}
|
||
var previewPayload struct {
|
||
Catalog struct {
|
||
Revision string `json:"revision"`
|
||
} `json:"catalog"`
|
||
}
|
||
if err := json.Unmarshal(preview.Body, &previewPayload); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
applyBody, _ := json.Marshal(map[string]any{"revision": previewPayload.Catalog.Revision, "models": []string{"deepseek-v4-flash"}})
|
||
applied := managementCallBody(t, app, http.MethodPost, managementBase+routeCatalogApply, applyBody)
|
||
if applied.StatusCode != http.StatusOK || !strings.Contains(string(applied.Body), `"updated":1`) {
|
||
t.Fatalf("catalog apply status=%d body=%s", applied.StatusCode, applied.Body)
|
||
}
|
||
pricesAfter := managementCall(t, app, http.MethodGet, managementBase+routePrices)
|
||
if !strings.Contains(string(pricesAfter.Body), `"input_per_1m":"6"`) {
|
||
t.Fatalf("confirmed price was not applied: %s", pricesAfter.Body)
|
||
}
|
||
|
||
manual := []byte(`{"model":"deepseek-v4-flash","base":{"input_per_1m":"7","cache_read_per_1m":"0.5","cache_write_per_1m":"6.25","output_per_1m":"30"},"fast_pricing_enabled":false,"fast_multiplier":"2.5"}`)
|
||
if response := managementCallBody(t, app, http.MethodPut, managementBase+routePrices, manual); response.StatusCode != http.StatusOK || !strings.Contains(string(response.Body), `"kind":"manual"`) {
|
||
t.Fatalf("manual save status=%d body=%s", response.StatusCode, response.Body)
|
||
}
|
||
catalogBody = strings.Replace(catalogBody, `"input":6`, `"input":8`, 1)
|
||
manualPreview := managementCall(t, app, http.MethodPost, managementBase+routeCatalogRefresh)
|
||
if manualPreview.StatusCode != http.StatusOK || !strings.Contains(string(manualPreview.Body), `"changed":0`) {
|
||
t.Fatalf("manual price followed catalog: status=%d body=%s", manualPreview.StatusCode, manualPreview.Body)
|
||
}
|
||
|
||
serverStatus = http.StatusBadGateway
|
||
failed := managementCall(t, app, http.MethodPost, managementBase+routeCatalogRefresh)
|
||
if failed.StatusCode != http.StatusBadGateway {
|
||
t.Fatalf("failed refresh status=%d body=%s", failed.StatusCode, failed.Body)
|
||
}
|
||
lastGood := managementCallRequest(t, app, ManagementRequest{Method: http.MethodGet, Path: managementBase + routeCatalog, Query: url.Values{"q": {"gpt"}}})
|
||
if lastGood.StatusCode != http.StatusOK || !strings.Contains(string(lastGood.Body), `"loaded":true`) || !strings.Contains(string(lastGood.Body), `"input_per_1m":"8"`) {
|
||
t.Fatalf("last-known-good catalog unavailable: status=%d body=%s", lastGood.StatusCode, lastGood.Body)
|
||
}
|
||
}
|
||
|
||
func TestUsageManagementSupportsServerPaginationFiltersAndSummary(t *testing.T) {
|
||
app := NewApp()
|
||
if _, err := app.HandleMethod(MethodPluginRegister, lifecycleRequest(t, SchemaVersion, testConfig(t, "enabled: true\ncodex_only: false\n"))); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
base := time.Date(2026, 8, 15, 8, 0, 0, 0, time.UTC)
|
||
for index := 0; index < 205; index++ {
|
||
record := UsageRecord{Provider: "openai", Model: fmt.Sprintf("model-%d", index%2), AuthID: fmt.Sprintf("auth-%d", index%2), RequestedAt: base.Add(time.Duration(index) * time.Second), Failed: index%10 == 0, Detail: UsageDetail{TotalTokens: int64(index + 1)}}
|
||
raw, _ := json.Marshal(record)
|
||
if _, err := app.HandleMethod(MethodUsageHandle, raw); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
response := managementCallRequest(t, app, ManagementRequest{Method: http.MethodGet, Path: managementBase + routeUsage, Query: url.Values{"page": {"2"}, "page_size": {"100"}, "model": {"model-1"}}})
|
||
if response.StatusCode != http.StatusOK {
|
||
t.Fatalf("usage query status=%d body=%s", response.StatusCode, response.Body)
|
||
}
|
||
var payload usageListResponse
|
||
if err := json.Unmarshal(response.Body, &payload); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if payload.Pagination.Total != 102 || payload.Pagination.Page != 2 || payload.Pagination.TotalPages != 2 || len(payload.Records) != 2 {
|
||
t.Fatalf("unexpected paginated response: %+v", payload.Pagination)
|
||
}
|
||
if payload.Records[0].Model != "model-1" || payload.Records[1].Model != "model-1" {
|
||
t.Fatalf("filter leaked records: %+v", payload.Records)
|
||
}
|
||
invalid := managementCallRequest(t, app, ManagementRequest{Method: http.MethodGet, Path: managementBase + routeUsage, Query: url.Values{"page_size": {"101"}}})
|
||
if invalid.StatusCode != http.StatusBadRequest {
|
||
t.Fatalf("invalid page size status=%d", invalid.StatusCode)
|
||
}
|
||
summary := managementCall(t, app, http.MethodGet, managementBase+routeUsageSummary)
|
||
if summary.StatusCode != http.StatusOK || !strings.Contains(string(summary.Body), `"users"`) || !strings.Contains(string(summary.Body), `"days"`) {
|
||
t.Fatalf("unexpected summary: status=%d body=%s", summary.StatusCode, summary.Body)
|
||
}
|
||
}
|
||
|
||
func TestFormatMicrosSupportsNegativeBalances(t *testing.T) {
|
||
if got := formatMicros(-125_000); got != "-0.125" {
|
||
t.Fatalf("formatMicros(-125000)=%q", got)
|
||
}
|
||
}
|
||
|
||
func TestPriceManagementCalculatesUsageWithLongContextAndFastPolicy(t *testing.T) {
|
||
app := NewApp()
|
||
if _, err := app.HandleMethod(MethodPluginRegister, lifecycleRequest(t, SchemaVersion, testConfig(t, "enabled: true\ncodex_only: true\n"))); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
priceBody := []byte(`{
|
||
"model":"gpt-5.6-sol",
|
||
"base":{"input_per_1m":"2.5","cache_read_per_1m":"0.25","cache_write_per_1m":"3.125","output_per_1m":"15"},
|
||
"long_context":{"threshold_input_tokens":272000,"comparison":"gt","input_per_1m":"5","cache_read_per_1m":"0.5","cache_write_per_1m":"6.25","output_per_1m":"22.5"},
|
||
"fast_pricing_enabled":true,
|
||
"fast_multiplier":"2.5"
|
||
}`)
|
||
response := managementCallBody(t, app, http.MethodPut, managementBase+routePrices, priceBody)
|
||
if response.StatusCode != http.StatusOK {
|
||
t.Fatalf("put price status = %d, body = %s", response.StatusCode, response.Body)
|
||
}
|
||
|
||
record := UsageRecord{
|
||
Provider: "codex", Model: "gpt-5.6-sol", ServiceTier: "priority", RequestedAt: time.Now(),
|
||
Detail: UsageDetail{InputTokens: 272_001, OutputTokens: 10_000, TotalTokens: 282_001},
|
||
}
|
||
raw, _ := json.Marshal(record)
|
||
if _, err := app.HandleMethod(MethodUsageHandle, raw); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
usageResponse := managementCall(t, app, http.MethodGet, managementBase+routeUsage)
|
||
var payload usageListResponse
|
||
if err := json.Unmarshal(usageResponse.Body, &payload); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
got := payload.Records[0]
|
||
if !got.CostAvailable || got.CostUSD == nil || *got.CostUSD != 3.962513 || got.PriceTier != "long_context" || !got.FastRequested || !got.FastPricingApplied {
|
||
t.Fatalf("unexpected priced usage: %+v", got)
|
||
}
|
||
|
||
pricesResponse := managementCall(t, app, http.MethodGet, managementBase+routePrices)
|
||
if pricesResponse.StatusCode != http.StatusOK || !strings.Contains(string(pricesResponse.Body), `"fast_multiplier":"2.5"`) {
|
||
t.Fatalf("unexpected prices response: %d %s", pricesResponse.StatusCode, pricesResponse.Body)
|
||
}
|
||
}
|
||
|
||
func TestFastRequestCanUseStandardPricing(t *testing.T) {
|
||
app := NewApp()
|
||
if _, err := app.HandleMethod(MethodPluginRegister, lifecycleRequest(t, SchemaVersion, testConfig(t, "enabled: true\ncodex_only: true\n"))); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
priceBody := []byte(`{"model":"gpt-5.6-sol","base":{"input_per_1m":"2.5","cache_read_per_1m":"0.25","cache_write_per_1m":"3.125","output_per_1m":"15"},"fast_pricing_enabled":false,"fast_multiplier":"2.5"}`)
|
||
if response := managementCallBody(t, app, http.MethodPut, managementBase+routePrices, priceBody); response.StatusCode != http.StatusOK {
|
||
t.Fatalf("put price status = %d, body = %s", response.StatusCode, response.Body)
|
||
}
|
||
record := UsageRecord{Provider: "codex", Model: "gpt-5.6-sol", ServiceTier: "fast", RequestedAt: time.Now(), Detail: UsageDetail{InputTokens: 100_000, TotalTokens: 100_000}}
|
||
raw, _ := json.Marshal(record)
|
||
if _, err := app.HandleMethod(MethodUsageHandle, raw); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
response := managementCall(t, app, http.MethodGet, managementBase+routeUsage)
|
||
var payload usageListResponse
|
||
if err := json.Unmarshal(response.Body, &payload); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
got := payload.Records[0]
|
||
if got.CostUSD == nil || *got.CostUSD != 0.25 || !got.FastRequested || got.FastPricingApplied {
|
||
t.Fatalf("unexpected standard-priced Fast usage: %+v", got)
|
||
}
|
||
}
|
||
|
||
func TestCanceledRequestWithoutUsageRemainsVisible(t *testing.T) {
|
||
app := NewApp()
|
||
if _, err := app.HandleMethod(MethodPluginRegister, lifecycleRequest(t, SchemaVersion, testConfig(t, "enabled: true\ncodex_only: false\n"))); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
startedAt := time.Date(2026, 8, 14, 12, 0, 0, 0, time.UTC)
|
||
completion := RequestCompletion{
|
||
RequestID: "request-canceled", TraceID: "trace-canceled", SourceFormat: "openai-response",
|
||
Model: "deepseek-v4-flash", RequestedModel: "deepseek-flash", Stream: true,
|
||
Outcome: RequestCompletionCanceled, StatusCode: 499, Error: "context canceled",
|
||
StartedAt: startedAt, CompletedAt: startedAt.Add(2 * time.Second),
|
||
Metadata: map[string]any{"request_path": "/v1/responses"},
|
||
}
|
||
raw, _ := json.Marshal(completion)
|
||
if _, err := app.HandleMethod(MethodRequestComplete, raw); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
response := managementCall(t, app, http.MethodGet, managementBase+routeUsage)
|
||
var payload usageListResponse
|
||
if err := json.Unmarshal(response.Body, &payload); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(payload.Records) != 1 {
|
||
t.Fatalf("records = %d, want 1", len(payload.Records))
|
||
}
|
||
got := payload.Records[0]
|
||
if got.RequestID != "request-canceled" || got.TraceID != "trace-canceled" || got.Outcome != "canceled" || !got.Failed || got.StatusCode != 499 || got.RequestType != "SSE" || got.CostAvailable {
|
||
t.Fatalf("unexpected canceled request: %+v", got)
|
||
}
|
||
}
|
||
|
||
func TestCompactUsageIsPricedAndUsesJSONMetrics(t *testing.T) {
|
||
app := NewApp()
|
||
if _, err := app.HandleMethod(MethodPluginRegister, lifecycleRequest(t, SchemaVersion, testConfig(t, "enabled: true\ncodex_only: false\n"))); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
priceBody := []byte(`{"model":"deepseek-v4-flash","base":{"input_per_1m":"2.5","cache_read_per_1m":"0.25","cache_write_per_1m":"3.125","output_per_1m":"15"},"fast_pricing_enabled":true,"fast_multiplier":"2.5"}`)
|
||
if response := managementCallBody(t, app, http.MethodPut, managementBase+routePrices, priceBody); response.StatusCode != http.StatusOK {
|
||
t.Fatalf("put price status = %d, body = %s", response.StatusCode, response.Body)
|
||
}
|
||
startedAt := time.Now()
|
||
record := UsageRecord{
|
||
Provider: "openai", Model: "deepseek-v4-flash", RequestedAt: startedAt,
|
||
Latency: time.Second, TTFT: 200 * time.Millisecond,
|
||
Detail: UsageDetail{InputTokens: 1_000, OutputTokens: 100, TotalTokens: 1_100},
|
||
}
|
||
raw, _ := json.Marshal(record)
|
||
if _, err := app.HandleMethod(MethodUsageHandle, raw); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
completionRaw, _ := json.Marshal(RequestCompletion{
|
||
RequestID: "compact-success", Model: "deepseek-v4-flash", Outcome: RequestCompletionSucceeded,
|
||
StartedAt: startedAt, CompletedAt: startedAt.Add(time.Second),
|
||
Metadata: map[string]any{"request_path": "/v1/responses/compact"},
|
||
})
|
||
if _, err := app.HandleMethod(MethodRequestComplete, completionRaw); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
response := managementCall(t, app, http.MethodGet, managementBase+routeUsage)
|
||
var payload usageListResponse
|
||
if err := json.Unmarshal(response.Body, &payload); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(payload.Records) != 1 {
|
||
t.Fatalf("records = %d, want 1", len(payload.Records))
|
||
}
|
||
got := payload.Records[0]
|
||
if got.RequestType != "JSON" || got.Endpoint != "/v1/responses/compact" || got.TTFTMilliseconds != 0 || got.SpeedTPS != nil {
|
||
t.Fatalf("unexpected compact metadata: %+v", got)
|
||
}
|
||
if !got.CostAvailable || got.CostUSD == nil || *got.CostUSD != 0.004 {
|
||
t.Fatalf("unexpected compact cost: %+v", got)
|
||
}
|
||
}
|
||
|
||
func TestCompactFailureAndCancellationRemainVisible(t *testing.T) {
|
||
for _, test := range []struct {
|
||
name string
|
||
outcome RequestCompletionOutcome
|
||
statusCode int
|
||
}{
|
||
{name: "failed", outcome: RequestCompletionFailed, statusCode: 500},
|
||
{name: "canceled", outcome: RequestCompletionCanceled, statusCode: 499},
|
||
} {
|
||
t.Run(test.name, func(t *testing.T) {
|
||
app := NewApp()
|
||
if _, err := app.HandleMethod(MethodPluginRegister, lifecycleRequest(t, SchemaVersion, testConfig(t, "enabled: true\ncodex_only: false\n"))); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
startedAt := time.Date(2026, 8, 14, 12, 0, 0, 0, time.UTC)
|
||
completion := RequestCompletion{
|
||
RequestID: "compact-" + test.name, Model: "deepseek-v4-flash", Stream: false,
|
||
Outcome: test.outcome, StatusCode: test.statusCode, Error: test.name,
|
||
StartedAt: startedAt, CompletedAt: startedAt.Add(time.Second),
|
||
Metadata: map[string]any{"request_path": "/v1/responses/compact"},
|
||
}
|
||
raw, _ := json.Marshal(completion)
|
||
if _, err := app.HandleMethod(MethodRequestComplete, raw); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
response := managementCall(t, app, http.MethodGet, managementBase+routeUsage)
|
||
var payload usageListResponse
|
||
if err := json.Unmarshal(response.Body, &payload); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(payload.Records) != 1 {
|
||
t.Fatalf("records = %d, want 1", len(payload.Records))
|
||
}
|
||
got := payload.Records[0]
|
||
if got.Outcome != string(test.outcome) || !got.Failed || got.StatusCode != test.statusCode || got.RequestType != "JSON" || got.Endpoint != "/v1/responses/compact" || got.SpeedTPS != nil || got.CostAvailable {
|
||
t.Fatalf("unexpected compact terminal record: %+v", got)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestUsageAndLifecycleMergeInEitherOrderAndDeduplicateUsage(t *testing.T) {
|
||
for _, usageFirst := range []bool{true, false} {
|
||
app := NewApp()
|
||
if _, err := app.HandleMethod(MethodPluginRegister, lifecycleRequest(t, SchemaVersion, testConfig(t, "enabled: true\ncodex_only: false\n"))); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
startedAt := time.Date(2026, 8, 14, 12, 0, 0, 0, time.UTC)
|
||
usage := UsageRecord{Provider: "openai", Model: "deepseek-v4-flash", RequestedAt: startedAt,
|
||
Detail: UsageDetail{InputTokens: 10, TotalTokens: 10}}
|
||
completion := RequestCompletion{
|
||
RequestID: "request-1", TraceID: "trace-1", Model: "deepseek-v4-flash",
|
||
Outcome: RequestCompletionFailed, StatusCode: 500, Error: "upstream failed",
|
||
StartedAt: startedAt, CompletedAt: startedAt.Add(time.Second),
|
||
}
|
||
usageRaw, _ := json.Marshal(usage)
|
||
completionRaw, _ := json.Marshal(completion)
|
||
calls := []struct {
|
||
method string
|
||
raw []byte
|
||
}{{MethodUsageHandle, usageRaw}, {MethodRequestComplete, completionRaw}}
|
||
if !usageFirst {
|
||
calls[0], calls[1] = calls[1], calls[0]
|
||
}
|
||
for _, call := range calls {
|
||
if _, err := app.HandleMethod(call.method, call.raw); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
// 重复的 Usage 回调不能生成第二条执行记录。
|
||
if _, err := app.HandleMethod(MethodUsageHandle, usageRaw); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
response := managementCall(t, app, http.MethodGet, managementBase+routeUsage)
|
||
var payload usageListResponse
|
||
if err := json.Unmarshal(response.Body, &payload); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(payload.Records) != 1 || payload.Records[0].RequestID != "request-1" || payload.Records[0].ExecutionID != "" || payload.Records[0].Outcome != "failed" || payload.Records[0].StatusCode != 500 {
|
||
t.Fatalf("usageFirst=%v records=%+v", usageFirst, payload.Records)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestDistinctContractUsageEventsRemainSeparate(t *testing.T) {
|
||
app := NewApp()
|
||
if _, err := app.HandleMethod(MethodPluginRegister, lifecycleRequest(t, SchemaVersion, testConfig(t, "enabled: true\ncodex_only: false\n"))); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
startedAt := time.Date(2026, 8, 14, 12, 0, 0, 0, time.UTC)
|
||
for index, failed := range []bool{true, false} {
|
||
usage := UsageRecord{
|
||
Provider: "openai", Model: "deepseek-v4-flash", RequestedAt: startedAt.Add(time.Duration(index) * time.Second),
|
||
Failed: failed, Detail: UsageDetail{InputTokens: int64(10 + index), TotalTokens: int64(10 + index)},
|
||
}
|
||
raw, _ := json.Marshal(usage)
|
||
if _, err := app.HandleMethod(MethodUsageHandle, raw); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
completion := RequestCompletion{RequestID: "request-retry", Model: "deepseek-v4-flash", Outcome: RequestCompletionSucceeded, StartedAt: startedAt, CompletedAt: startedAt.Add(2 * time.Second)}
|
||
raw, _ := json.Marshal(completion)
|
||
if _, err := app.HandleMethod(MethodRequestComplete, raw); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
response := managementCall(t, app, http.MethodGet, managementBase+routeUsage)
|
||
var payload usageListResponse
|
||
if err := json.Unmarshal(response.Body, &payload); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(payload.Records) != 2 || payload.Records[0].ExecutionID != "" || payload.Records[0].RequestID != "" || payload.Records[0].Failed || payload.Records[1].ExecutionID != "" || payload.Records[1].RequestID != "request-retry" || !payload.Records[1].Failed {
|
||
t.Fatalf("unexpected usage records: %+v", payload.Records)
|
||
}
|
||
}
|
||
|
||
func TestUsageManagementResponseContainsDisplayFields(t *testing.T) {
|
||
app := NewApp()
|
||
if _, err := app.HandleMethod(MethodPluginRegister, lifecycleRequest(t, SchemaVersion, testConfig(t, "enabled: true\ncodex_only: true\n"))); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
startedAt := time.Date(2026, 8, 14, 12, 0, 0, 0, time.UTC)
|
||
record := UsageRecord{
|
||
Provider: "codex",
|
||
APIKey: "test-key",
|
||
Model: "gpt-5.5",
|
||
ReasoningEffort: "high",
|
||
ServiceTier: "priority",
|
||
ExecutorType: "CodexExecutor",
|
||
RequestedAt: startedAt,
|
||
Latency: 1500 * time.Millisecond,
|
||
TTFT: 250 * time.Millisecond,
|
||
Detail: UsageDetail{
|
||
InputTokens: 10,
|
||
OutputTokens: 5,
|
||
TotalTokens: 15,
|
||
CachedTokens: 4,
|
||
CacheReadTokens: 4,
|
||
CacheCreationTokens: 1,
|
||
ReasoningTokens: 2,
|
||
},
|
||
}
|
||
raw, _ := json.Marshal(record)
|
||
if _, err := app.HandleMethod(MethodUsageHandle, raw); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
completionRaw, _ := json.Marshal(RequestCompletion{
|
||
RequestID: "request-display", TraceID: "trace-display", Model: "gpt-5.5", Stream: true,
|
||
Outcome: RequestCompletionSucceeded, StartedAt: startedAt, CompletedAt: startedAt.Add(1500 * time.Millisecond),
|
||
Metadata: map[string]any{"request_path": "/v1/responses"},
|
||
})
|
||
if _, err := app.HandleMethod(MethodRequestComplete, completionRaw); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
response := managementCall(t, app, http.MethodGet, managementBase+routeUsage)
|
||
if response.StatusCode != http.StatusOK {
|
||
t.Fatalf("status = %d, body = %s", response.StatusCode, response.Body)
|
||
}
|
||
var payload usageListResponse
|
||
if err := json.Unmarshal(response.Body, &payload); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(payload.Records) != 1 {
|
||
t.Fatalf("records = %d, want 1", len(payload.Records))
|
||
}
|
||
got := payload.Records[0]
|
||
if got.APIKey != "test-key" || got.Model != "gpt-5.5" || got.ReasoningEffort != "high" || got.ServiceTier != "priority" || got.ExecutorType != "CodexExecutor" {
|
||
t.Fatalf("unexpected usage identity fields: %+v", got)
|
||
}
|
||
if got.RequestID != "request-display" || got.ExecutionID != "" || got.TraceID != "trace-display" || got.RequestType != "SSE" || got.Endpoint != "/v1/responses" || got.ClientIP != "" {
|
||
t.Fatalf("unexpected request metadata: %+v", got)
|
||
}
|
||
if got.TotalTokens != 15 || got.TTFTMilliseconds != 250 || got.CacheReadTokens != 4 || got.CacheWriteTokens != 1 {
|
||
t.Fatalf("unexpected usage counters: %+v", got)
|
||
}
|
||
if got.SpeedTPS == nil || *got.SpeedTPS != 4 || got.CacheRate == nil || *got.CacheRate != 40 {
|
||
t.Fatalf("unexpected derived display fields: %+v", got)
|
||
}
|
||
if got.KeyAlias != "" || got.CostUSD != nil || got.CostAvailable {
|
||
t.Fatalf("unexpected usage item: %+v", got)
|
||
}
|
||
}
|
||
|
||
func TestUsageResourceServesTablePage(t *testing.T) {
|
||
response := managementCall(t, NewApp(), http.MethodGet, resourceBase+resourceUI)
|
||
page := string(response.Body)
|
||
if response.StatusCode != http.StatusOK || !strings.Contains(page, "请求明细") {
|
||
t.Fatalf("unexpected UI response: status=%d", response.StatusCode)
|
||
}
|
||
for _, column := range []string{"Key / 别名", "推理强度", "生成速度", "缓存写入", "总成本", "客户端 IP"} {
|
||
if !strings.Contains(page, column) {
|
||
t.Fatalf("UI does not contain column %q", column)
|
||
}
|
||
}
|
||
if !strings.Contains(page, `return "compact"`) || !strings.Contains(page, "isCompactEndpoint(record.endpoint)") {
|
||
t.Fatal("UI does not contain compact display rules")
|
||
}
|
||
for _, feature := range []string{"创建 Key", "指定账号", "允许模型", "永久归档"} {
|
||
if !strings.Contains(page, feature) {
|
||
t.Fatalf("UI does not contain managed access feature %q", feature)
|
||
}
|
||
}
|
||
for _, feature := range []string{`<body class="read-only">`, `<input id="key" type="password" autocomplete="off" aria-label="管理密钥">`, `id="page-buttons"`, "const PAGE_SIZE = 100", `id="user-usage"`, `id="daily-chart"`, `id="usage-filter-panel" class="usage-filter-panel"`, `id="usage-from"`, `id="usage-request-id"`, `id="apply-usage-filters"`, "SUMMARY_API", "READONLY_API", "dataFetch", "managementAuthorized", `overflow-y: hidden`, `id="editor-quota"`, `id="editor-reset-period"`, `id="billing-ledger"`, "deepseek-*", "请求结束后按实际费用扣款", `id="key-management" class="surface admin-only"`, `class="price-section admin-only"`, `id="price-editor-content" class="hidden"`, `row.setAttribute("role", "button")`, `id="long-price-section"`, "syncLongSectionVisibility"} {
|
||
if !strings.Contains(page, feature) {
|
||
t.Fatalf("UI does not contain workspace feature %q", feature)
|
||
}
|
||
}
|
||
if strings.Contains(page, "<script src=") || strings.Contains(page, "<link rel=\"stylesheet\" href=") {
|
||
t.Fatal("UI unexpectedly depends on external assets")
|
||
}
|
||
for _, removed := range []string{"下游 Keys", "最近用量记录", "显示 102 条", "最多展示", "1–100 /", "一个 Key 对应一个用户", "查看请求结果、实际上游", "维护模型基础价格", "点击“管理”修改访问、路由与额度", "由 CPA 调度", `copy.textContent = "复制"`, "测试模式", `data-mode="demo"`, "DEMO_STORE_KEY", "demoState", "mode-button", "测试目录 · 不访问 models.dev", `id="access-mode"`, `placeholder="留空时只读"`, `label for="key"`, "密钥无效 · 只读", `id="include-archived"`, `id="reload-keys"`} {
|
||
if strings.Contains(page, removed) {
|
||
t.Fatalf("UI still contains removed description %q", removed)
|
||
}
|
||
}
|
||
for _, feature := range []string{`minlength="6"`, `maskedSecret(key.secret)`, `key.masked_secret`, `className = "credential-copy"`, `return "自由选择"`, `body.classList.toggle("read-only"`} {
|
||
if !strings.Contains(page, feature) {
|
||
t.Fatalf("UI does not contain compact Key display feature %q", feature)
|
||
}
|
||
}
|
||
if strings.Index(page, `id="page-buttons"`) > strings.Index(page, `id="headers"`) {
|
||
t.Fatal("UI pagination controls are not above the usage table")
|
||
}
|
||
for _, feature := range []string{`class="surface price-panel price-layout"`, `id="new-price"`, "基础价格 · $ / 1M Token", "长上下文价格", "Fast 价格", "删除 ${model} 的真实价格配置", `id="catalog-query"`, `id="refresh-catalog"`, "PRICE_IMPORT_API", "发现 ${changed.length} 个已关联价格发生变化"} {
|
||
if !strings.Contains(page, feature) {
|
||
t.Fatalf("UI does not contain redesigned pricing feature %q", feature)
|
||
}
|
||
}
|
||
if strings.Contains(page, "scrollIntoView") {
|
||
t.Fatal("UI pagination still changes the document scroll position")
|
||
}
|
||
}
|