12 "github.com/stretchr/testify/require"
15 func TestBuildRunStopsOnFailedCallsOrQuotaGrowth(t *testing.T) {
16 quota := func(primary, secondary string) ModelUsageRecord {
17 return ModelUsageRecord{
18 Quota: map[string]string{
19 "x-codex-primary-used-percent": primary,
20 "x-codex-secondary-used-percent": secondary,
21 "x-codex-plan-type": "plus",
26 testCases := []struct {
28 records []ModelUsageRecord
37 name: "growth within the limit",
38 records: []ModelUsageRecord{quota("10", "40"), quota("25", "41")},
42 name: "growth over the limit",
43 records: []ModelUsageRecord{quota("10", "40"), quota("12", "55.5")},
44 want: "x-codex-secondary-used-percent grew from 40 to 55.5",
48 records: []ModelUsageRecord{quota("10", "40"), {
50 Error: "model call: HTTP 400",
52 want: "call failed: model call: HTTP 400",
56 for _, tc := range testCases {
57 t.Run(tc.name, func(t *testing.T) {
58 require.Equal(t, tc.want, buildStop(tc.records))
63 func TestBuildRunStopsWhenTheQuotaGrows(t *testing.T) {
64 server := newModelFake(t)
65 server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
66 w.Header().Set("X-Codex-Primary-Used-Percent", strconv.Itoa(10+call.number))
67 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem(strings.Repeat("s", 400))}, 1000, 800, 50))
70 run := newTestBuildRun(t, server)
71 appended, stop := run.Run(t.Context(), probeLog(probeMessages))
72 sizes, err := run.memory.buildModelLines()
73 require.NoError(t, err)
74 require.NoError(t, run.memory.Close())
76 require.Regexp(t, `^x-codex-primary-used-percent grew from \d+ to \d+$`, stop)
77 require.Less(t, appended, probeMessages)
78 require.NotEmpty(t, sizes)
80 records, err := buildReadUsage(run.usagePath)
81 require.NoError(t, err)
82 require.Greater(t, len(records), int(buildQuotaLimit))
84 var report strings.Builder
85 require.NoError(t, buildWriteReport(&report, appended, stop, records, sizes))
86 require.Contains(t, report.String(), "stopped early: ")
87 require.Contains(t, report.String(), "cached (80.0%)")
88 require.Contains(t, report.String(), "line bytes: min 400, median 400, mean 400, max 400, 0 over 512")
89 require.Contains(t, report.String(), "calls at once: at most ")
90 require.Contains(t, report.String(), "quota at the first call: primary-used-percent=")
93 func TestBuildRunStopsAtTheCallLimit(t *testing.T) {
94 server := newModelFake(t)
95 run := newTestBuildRun(t, server)
96 run.client.LimitCalls(5)
98 entries := make([]DatedMessage, 40)
99 for n := range entries {
100 entries[n] = DatedMessage{
102 Text: strings.Repeat("long output ", 100),
106 _, stop := run.Run(t.Context(), entries)
107 require.NoError(t, run.memory.Close())
108 require.Equal(t, "call limit of 5 reached", stop)
109 require.Equal(t, 5, server.Calls())
112 func TestBuildRunWaitsOnlyBeforeUserMessages(t *testing.T) {
113 gate := make(chan struct{})
115 running, most := 0, 0
116 server := newModelFake(t)
117 server.reply = func(w http.ResponseWriter, _ ModelFakeCall) {
120 most = max(most, running)
129 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("a line")}, 100, 0, 5))
132 run := newTestBuildRun(t, server)
133 long := strings.Repeat("long output ", 100)
134 entries := []DatedMessage{
153 done := make(chan string)
155 _, stop := run.Run(t.Context(), entries)
159 require.Eventually(t, func() bool {
160 return server.Calls() == 2
161 }, 10*time.Second, time.Millisecond)
163 appended := run.memory.Len()
164 require.Equal(t, 2, appended)
167 require.Empty(t, <-done)
168 require.NoError(t, run.memory.Close())
173 require.Equal(t, 2, most)
174 require.Equal(t, 3, server.Calls())
177 func TestReadUsageFileSortsByCallStart(t *testing.T) {
178 path := filepath.Join(t.TempDir(), "usage.jsonl")
179 usage := ModelUsageLog{
183 for _, date := range []string{"2026-01-01T12:00:02Z", "2026-01-01T12:00:00.5Z", "2026-01-01T12:00:01.25Z"} {
184 require.NoError(t, usage.Write(ModelUsageRecord{
189 records, err := buildReadUsage(path)
190 require.NoError(t, err)
191 require.Equal(t, []string{"2026-01-01T12:00:00.5Z", "2026-01-01T12:00:01.25Z", "2026-01-01T12:00:02Z"}, []string{records[0].Date, records[1].Date, records[2].Date})
194 func TestTimeTextCountsTheCallsAtOnce(t *testing.T) {
195 start := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
196 record := func(offset, seconds float64) ModelUsageRecord {
197 return ModelUsageRecord{
198 Date: start.Add(time.Duration(offset * float64(time.Second))).Format(time.RFC3339Nano),
203 text := buildTimeText([]ModelUsageRecord{record(0, 10), record(2, 4), record(3, 2), record(10, 10)})
204 require.Equal(t, "time: 20 s in all, 12.0 calls per minute, 6.5 s per call (median 10.0, max 10.0), calls at once: at most 3, 1.3 on average", text)
207 func newTestBuildRun(t *testing.T, server *ModelFake) *BuildRun {
211 usagePath := filepath.Join(root, "usage.jsonl")
212 client := newModelClient(server.URL, ModelAPIKey(modelTestKey), "onechat-"+probeName, usagePath)
213 memory, err := openMemory(t.Context(), root, newTestClock().Now, newCompactor(client, probePrompt, defaultConfig().Compactor), discardLog())
214 require.NoError(t, err)
219 usagePath: usagePath,