diff --git a/internal/httpapi/claude/current_input_file_test.go b/internal/httpapi/claude/current_input_file_test.go index 6f2f864fc..f130eeef8 100644 --- a/internal/httpapi/claude/current_input_file_test.go +++ b/internal/httpapi/claude/current_input_file_test.go @@ -26,6 +26,51 @@ func (claudeHistoryConfig) CurrentInputFileMinChars() int { return 0 } func (claudeHistoryConfig) ContextEngineMode() string { return "off" } func (claudeHistoryConfig) ParserV2Mode() string { return "off" } +type claudeProductionCurrentInputConfig struct { + mockClaudeConfig +} + +func (claudeProductionCurrentInputConfig) CurrentInputFileInlineMaxTokens() int { + return 1 +} + +func (claudeProductionCurrentInputConfig) CurrentInputFileFilenamePolicy() string { + return "neutral_random" +} + +func (claudeProductionCurrentInputConfig) ContextEngineStrategy() string { + return "hybrid_recent" +} + +func assertClaudeNeutralGeneratedFilename(t *testing.T, got, stem string) { + t.Helper() + if !strings.HasPrefix(got, stem+"-") || !strings.HasSuffix(got, ".txt") { + t.Fatalf("expected neutral randomized %s filename, got %q", stem, got) + } + if strings.Contains(got, "DS2API") { + t.Fatalf("generated filename should not expose implementation terms, got %q", got) + } +} + +func assertClaudeHybridCurrentInputContext(t *testing.T, text, latest string) { + t.Helper() + for _, want := range []string{ + "# Conversation Context", + "## Current Task", + latest, + "## Recent Exact Context", + } { + if !strings.Contains(text, want) { + t.Fatalf("expected hybrid current-input context to contain %q, got %q", want, text) + } + } + for _, forbidden := range []string{"DS2API_HISTORY", "DS2API_TOOLS", "# Conversation Transcript"} { + if strings.Contains(text, forbidden) { + t.Fatalf("current-input context should not expose %q, got %q", forbidden, text) + } + } +} + func (claudeCurrentInputAuth) Determine(*http.Request) (*auth.RequestAuth, error) { return &auth.RequestAuth{ DeepSeekToken: "direct-token", @@ -115,7 +160,7 @@ func TestClaudeDirectAppliesCurrentInputFile(t *testing.T) { ds := &claudeCurrentInputDS{} historyStore := chathistory.New(filepath.Join(t.TempDir(), "history.json")) h := &Handler{ - Store: mockClaudeConfig{aliases: map[string]string{"claude-sonnet-4-6": "deepseek-v4-flash"}}, + Store: claudeProductionCurrentInputConfig{mockClaudeConfig: mockClaudeConfig{aliases: map[string]string{"claude-sonnet-4-6": "deepseek-v4-flash"}}}, Auth: claudeCurrentInputAuth{}, DS: ds, ChatHistory: historyStore, @@ -133,9 +178,8 @@ func TestClaudeDirectAppliesCurrentInputFile(t *testing.T) { if len(ds.uploads) != 1 { t.Fatalf("expected one current input upload, got %d", len(ds.uploads)) } - if ds.uploads[0].Filename != "DS2API_HISTORY.txt" { - t.Fatalf("unexpected upload filename: %q", ds.uploads[0].Filename) - } + assertClaudeNeutralGeneratedFilename(t, ds.uploads[0].Filename, "conversation-notes") + assertClaudeHybridCurrentInputContext(t, string(ds.uploads[0].Data), "hello from claude") refIDs, _ := ds.payload["ref_file_ids"].([]any) if len(refIDs) != 1 || refIDs[0] != "file-claude-history" { t.Fatalf("expected uploaded history ref id, got %#v", ds.payload["ref_file_ids"]) @@ -167,7 +211,7 @@ func TestClaudeCurrentInputFileUploadsToolsSeparately(t *testing.T) { ds := &claudeCurrentInputDS{} historyStore := chathistory.New(filepath.Join(t.TempDir(), "history.json")) h := &Handler{ - Store: mockClaudeConfig{aliases: map[string]string{"claude-sonnet-4-6": "deepseek-v4-flash"}}, + Store: claudeProductionCurrentInputConfig{mockClaudeConfig: mockClaudeConfig{aliases: map[string]string{"claude-sonnet-4-6": "deepseek-v4-flash"}}}, Auth: claudeCurrentInputAuth{}, DS: ds, ChatHistory: historyStore, @@ -190,10 +234,10 @@ func TestClaudeCurrentInputFileUploadsToolsSeparately(t *testing.T) { if len(ds.uploads) != 2 { t.Fatalf("expected history and tools uploads, got %d", len(ds.uploads)) } - if ds.uploads[0].Filename != "DS2API_HISTORY.txt" || ds.uploads[1].Filename != "DS2API_TOOLS.txt" { - t.Fatalf("unexpected upload filenames: %#v", ds.uploads) - } + assertClaudeNeutralGeneratedFilename(t, ds.uploads[0].Filename, "conversation-notes") + assertClaudeNeutralGeneratedFilename(t, ds.uploads[1].Filename, "tool-reference") historyText := string(ds.uploads[0].Data) + assertClaudeHybridCurrentInputContext(t, historyText, "search docs") if strings.Contains(historyText, "Description: search docs") { t.Fatalf("history transcript should not embed tool descriptions, got %q", historyText) } diff --git a/internal/httpapi/gemini/handler_test.go b/internal/httpapi/gemini/handler_test.go index 72e01426f..22d5396f8 100644 --- a/internal/httpapi/gemini/handler_test.go +++ b/internal/httpapi/gemini/handler_test.go @@ -26,6 +26,52 @@ func (testGeminiConfig) CurrentInputFileMinChars() int { return 0 } func (testGeminiConfig) ContextEngineMode() string { return "off" } func (testGeminiConfig) ParserV2Mode() string { return "off" } +type testGeminiProductionConfig struct{} + +func (testGeminiProductionConfig) ModelAliases() map[string]string { return nil } +func (testGeminiProductionConfig) CurrentInputFileEnabled() bool { return true } +func (testGeminiProductionConfig) CurrentInputFileMinChars() int { return 0 } +func (testGeminiProductionConfig) ContextEngineMode() string { return "off" } +func (testGeminiProductionConfig) ParserV2Mode() string { return "off" } +func (testGeminiProductionConfig) CurrentInputFileInlineMaxTokens() int { + return 1 +} +func (testGeminiProductionConfig) CurrentInputFileFilenamePolicy() string { + return "neutral_random" +} +func (testGeminiProductionConfig) ContextEngineStrategy() string { + return "hybrid_recent" +} + +func assertGeminiNeutralGeneratedFilename(t *testing.T, got, stem string) { + t.Helper() + if !strings.HasPrefix(got, stem+"-") || !strings.HasSuffix(got, ".txt") { + t.Fatalf("expected neutral randomized %s filename, got %q", stem, got) + } + if strings.Contains(got, "DS2API") { + t.Fatalf("generated filename should not expose implementation terms, got %q", got) + } +} + +func assertGeminiHybridCurrentInputContext(t *testing.T, text, latest string) { + t.Helper() + for _, want := range []string{ + "# Conversation Context", + "## Current Task", + latest, + "## Recent Exact Context", + } { + if !strings.Contains(text, want) { + t.Fatalf("expected hybrid current-input context to contain %q, got %q", want, text) + } + } + for _, forbidden := range []string{"DS2API_HISTORY", "DS2API_TOOLS", "# Conversation Transcript"} { + if strings.Contains(text, forbidden) { + t.Fatalf("current-input context should not expose %q, got %q", forbidden, text) + } + } +} + type testGeminiAuth struct { a *auth.RequestAuth err error @@ -148,7 +194,7 @@ func TestGeminiDirectAppliesCurrentInputFile(t *testing.T) { } historyStore := chathistory.New(filepath.Join(t.TempDir(), "history.json")) h := &Handler{ - Store: testGeminiConfig{}, + Store: testGeminiProductionConfig{}, Auth: testGeminiAuth{}, DS: ds, ChatHistory: historyStore, @@ -168,9 +214,8 @@ func TestGeminiDirectAppliesCurrentInputFile(t *testing.T) { if len(ds.uploadCalls) != 1 { t.Fatalf("expected one current input upload, got %d", len(ds.uploadCalls)) } - if ds.uploadCalls[0].Filename != "DS2API_HISTORY.txt" { - t.Fatalf("unexpected upload filename: %q", ds.uploadCalls[0].Filename) - } + assertGeminiNeutralGeneratedFilename(t, ds.uploadCalls[0].Filename, "conversation-notes") + assertGeminiHybridCurrentInputContext(t, string(ds.uploadCalls[0].Data), "hello from gemini") if len(ds.payloads) != 1 { t.Fatalf("expected one completion payload, got %d", len(ds.payloads)) } @@ -212,7 +257,7 @@ func TestGeminiCurrentInputFileUploadsToolsSeparately(t *testing.T) { resp: makeGeminiUpstreamResponse(`data: {"p":"response/content","v":"ok"}`), } h := &Handler{ - Store: testGeminiConfig{}, + Store: testGeminiProductionConfig{}, Auth: testGeminiAuth{}, DS: ds, } @@ -234,10 +279,10 @@ func TestGeminiCurrentInputFileUploadsToolsSeparately(t *testing.T) { if len(ds.uploadCalls) != 2 { t.Fatalf("expected history and tools uploads, got %d", len(ds.uploadCalls)) } - if ds.uploadCalls[0].Filename != "DS2API_HISTORY.txt" || ds.uploadCalls[1].Filename != "DS2API_TOOLS.txt" { - t.Fatalf("unexpected upload filenames: %#v", ds.uploadCalls) - } + assertGeminiNeutralGeneratedFilename(t, ds.uploadCalls[0].Filename, "conversation-notes") + assertGeminiNeutralGeneratedFilename(t, ds.uploadCalls[1].Filename, "tool-reference") historyText := string(ds.uploadCalls[0].Data) + assertGeminiHybridCurrentInputContext(t, historyText, "run code") if strings.Contains(historyText, "Description: eval") { t.Fatalf("history transcript should not embed tool descriptions, got %q", historyText) } diff --git a/internal/httpapi/openai/history_split_test.go b/internal/httpapi/openai/history_split_test.go index 0a41be786..05d2fa8d6 100644 --- a/internal/httpapi/openai/history_split_test.go +++ b/internal/httpapi/openai/history_split_test.go @@ -61,6 +61,58 @@ func (streamStatusManagedAuthStub) DetermineCaller(_ *http.Request) (*auth.Reque func (streamStatusManagedAuthStub) Release(_ *auth.RequestAuth) {} +type protocolCurrentInputOpenAIConfig struct { + mockOpenAIConfig +} + +func (protocolCurrentInputOpenAIConfig) CurrentInputFileInlineMaxTokens() int { + return 1 +} + +func (protocolCurrentInputOpenAIConfig) CurrentInputFileFilenamePolicy() string { + return "neutral_random" +} + +func (protocolCurrentInputOpenAIConfig) ContextEngineStrategy() string { + return "hybrid_recent" +} + +func assertNeutralGeneratedFilename(t *testing.T, got, stem string) { + t.Helper() + if !strings.HasPrefix(got, stem+"-") || !strings.HasSuffix(got, ".txt") { + t.Fatalf("expected neutral randomized %s filename, got %q", stem, got) + } + if strings.Contains(got, "DS2API") { + t.Fatalf("generated filename should not expose implementation terms, got %q", got) + } +} + +func assertHybridCurrentInputContext(t *testing.T, text, latest string) { + t.Helper() + for _, want := range []string{ + "# Conversation Context", + "## Current Task", + latest, + "## Recent Exact Context", + } { + if !strings.Contains(text, want) { + t.Fatalf("expected hybrid current-input context to contain %q, got %q", want, text) + } + } + for _, forbidden := range []string{ + "DS2API_HISTORY", + "DS2API_TOOLS", + "# Conversation Transcript", + "[file content begin]", + "[file content end]", + "[file name]:", + } { + if strings.Contains(text, forbidden) { + t.Fatalf("current-input context should not expose %q, got %q", forbidden, text) + } + } +} + func TestBuildOpenAICurrentInputContextTranscriptUsesNaturalContextSections(t *testing.T) { transcript := buildOpenAICurrentInputContextTranscript(historySplitTestMessages()) @@ -495,6 +547,44 @@ func TestChatCompletionsCurrentInputFileUploadsContextAndKeepsNeutralPrompt(t *t } } +func TestChatCompletionsCurrentInputFileUsesHybridContextAndNeutralFilenameByDefault(t *testing.T) { + ds := &inlineUploadDSStub{} + h := &openAITestSurface{ + Store: protocolCurrentInputOpenAIConfig{mockOpenAIConfig: mockOpenAIConfig{ + currentInputEnabled: true, + }}, + Auth: streamStatusAuthStub{}, + DS: ds, + } + reqBody, _ := json.Marshal(map[string]any{ + "model": "deepseek-v4-flash", + "messages": historySplitTestMessages(), + "stream": false, + }) + req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(string(reqBody))) + req.Header.Set("Authorization", "Bearer direct-token") + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + + h.ChatCompletions(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String()) + } + if len(ds.uploadCalls) != 1 { + t.Fatalf("expected one current input upload, got %d", len(ds.uploadCalls)) + } + assertNeutralGeneratedFilename(t, ds.uploadCalls[0].Filename, "conversation-notes") + assertHybridCurrentInputContext(t, string(ds.uploadCalls[0].Data), "latest user turn") + promptText, _ := ds.completionReq["prompt"].(string) + if !strings.Contains(promptText, "Use the attached conversation context as the current working state.") { + t.Fatalf("expected continuation-oriented prompt, got %s", promptText) + } + if strings.Contains(promptText, "latest user turn") || strings.Contains(promptText, "DS2API") { + t.Fatalf("expected neutral live prompt without original turns or implementation terms, got %s", promptText) + } +} + func TestResponsesCurrentInputFileUploadsContextAndKeepsNeutralPrompt(t *testing.T) { ds := &inlineUploadDSStub{} h := &openAITestSurface{ @@ -550,6 +640,46 @@ func TestResponsesCurrentInputFileUploadsContextAndKeepsNeutralPrompt(t *testing } } +func TestResponsesCurrentInputFileUsesHybridContextAndNeutralFilenameByDefault(t *testing.T) { + ds := &inlineUploadDSStub{} + h := &openAITestSurface{ + Store: protocolCurrentInputOpenAIConfig{mockOpenAIConfig: mockOpenAIConfig{ + currentInputEnabled: true, + }}, + Auth: streamStatusAuthStub{}, + DS: ds, + } + r := chi.NewRouter() + registerOpenAITestRoutes(r, h) + reqBody, _ := json.Marshal(map[string]any{ + "model": "deepseek-v4-flash", + "messages": historySplitTestMessages(), + "stream": false, + }) + req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(string(reqBody))) + req.Header.Set("Authorization", "Bearer direct-token") + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + + r.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String()) + } + if len(ds.uploadCalls) != 1 { + t.Fatalf("expected one current input upload, got %d", len(ds.uploadCalls)) + } + assertNeutralGeneratedFilename(t, ds.uploadCalls[0].Filename, "conversation-notes") + assertHybridCurrentInputContext(t, string(ds.uploadCalls[0].Data), "latest user turn") + promptText, _ := ds.completionReq["prompt"].(string) + if !strings.Contains(promptText, "Use the attached conversation context as the current working state.") { + t.Fatalf("expected continuation-oriented prompt, got %s", promptText) + } + if strings.Contains(promptText, "latest user turn") || strings.Contains(promptText, "DS2API") { + t.Fatalf("expected neutral live prompt without original turns or implementation terms, got %s", promptText) + } +} + func TestResponsesCurrentInputFileUploadsToolsSeparately(t *testing.T) { ds := &inlineUploadDSStub{} h := &openAITestSurface{