TestEmptyReplyNudgeRePromptsThenRecovers: a turn whose first round comes back with no content and no tool call (the dominant silent-death: a thinking model's tool call swallowed into the reasoning channel) must be re-prompted once, not ended silently. Here the second round produces a real summary, s
(t *testing.T)
| 3239 | } |
| 3240 | |
| 3241 | // TestNewestAssistantEmpty pins the anomaly detector behind the empty-reply |
| 3242 | // nudge: only a newest assistant message with neither text nor a structured |
| 3243 | // tool call counts as empty. A summary or a tool call is a normal turn. |
| 3244 | func TestNewestAssistantEmpty(t *testing.T) { |
| 3245 | cases := []struct { |
| 3246 | name string |
| 3247 | hist []chmctx.Message |
| 3248 | want bool |
| 3249 | }{ |
| 3250 | {"empty assistant", []chmctx.Message{ |
| 3251 | {Role: chmctx.RoleUser, Content: "go"}, |
| 3252 | {Role: chmctx.RoleAssistant, Content: ""}, |
| 3253 | }, true}, |
| 3254 | {"whitespace-only assistant", []chmctx.Message{ |
| 3255 | {Role: chmctx.RoleAssistant, Content: " \n\t"}, |
| 3256 | }, true}, |
| 3257 | {"assistant with summary", []chmctx.Message{ |
| 3258 | {Role: chmctx.RoleAssistant, Content: "done"}, |
| 3259 | }, false}, |
| 3260 | {"assistant with tool call", []chmctx.Message{ |
| 3261 | {Role: chmctx.RoleAssistant, ToolCalls: []chmctx.ToolCall{{Name: "bash"}}}, |
| 3262 | }, false}, |
| 3263 | {"newest is tool result, prior assistant empty", []chmctx.Message{ |
| 3264 | {Role: chmctx.RoleAssistant, Content: ""}, |
| 3265 | {Role: chmctx.RoleTool, Content: "out"}, |
| 3266 | }, true}, |
| 3267 | {"no assistant at all", []chmctx.Message{ |
| 3268 | {Role: chmctx.RoleUser, Content: "go"}, |
| 3269 | }, false}, |
| 3270 | } |
| 3271 | for _, tc := range cases { |
| 3272 | if got := newestAssistantEmpty(tc.hist); got != tc.want { |
| 3273 | t.Errorf("%s: newestAssistantEmpty = %v, want %v", tc.name, got, tc.want) |
| 3274 | } |
| 3275 | } |
| 3276 | } |
| 3277 | |
| 3278 | // TestEmptyReplyNudgeRePromptsThenRecovers: a turn whose first round comes back |
| 3279 | // with no content and no tool call (the dominant silent-death: a thinking |
| 3280 | // model's tool call swallowed into the reasoning channel) must be re-prompted |
| 3281 | // once, not ended silently. Here the second round produces a real summary, so |
| 3282 | // the run self-heals. |
| 3283 | func TestEmptyReplyNudgeRePromptsThenRecovers(t *testing.T) { |
| 3284 | var round int |
| 3285 | handler := func(w http.ResponseWriter, _ *http.Request) { |
nothing calls this directly
no test coverage detected