|
| 1 | +package agent |
| 2 | + |
| 3 | +import ( |
| 4 | + "bytes" |
| 5 | + "encoding/json" |
| 6 | + "io" |
| 7 | + "net/http" |
| 8 | + "os" |
| 9 | + "testing" |
| 10 | + "time" |
| 11 | + |
| 12 | + "github.com/Use-Tusk/tusk-drift-cli/internal/api" |
| 13 | + "github.com/Use-Tusk/tusk-drift-cli/internal/version" |
| 14 | +) |
| 15 | + |
| 16 | +// TestSystemPromptAcceptedByBackend verifies that the current system prompt |
| 17 | +// is accepted by the Tusk backend proxy. This test ensures that when the CLI's |
| 18 | +// system prompt changes, a corresponding update to the backend's PROMPT_VERSION_RANGES |
| 19 | +// has been deployed. |
| 20 | +// |
| 21 | +// Required environment variables: |
| 22 | +// - TUSK_PROXY_TEST_API_KEY: Tusk API key for authentication (does not expire) |
| 23 | +// |
| 24 | +// Optional environment variables: |
| 25 | +// - TUSK_CLOUD_API_URL: Override the backend URL (defaults to https://api.usetusk.ai) |
| 26 | +// |
| 27 | +// This test uses validate-only mode (x-tusk-validate-only header) to avoid |
| 28 | +// Anthropic API calls and associated costs. It only validates that the system |
| 29 | +// prompt is accepted by the backend. |
| 30 | +// |
| 31 | +// This test is skipped if TUSK_PROXY_TEST_API_KEY is not set. |
| 32 | +func TestSystemPromptAcceptedByBackend(t *testing.T) { |
| 33 | + apiKey := os.Getenv("TUSK_PROXY_TEST_API_KEY") |
| 34 | + if apiKey == "" { |
| 35 | + t.Skip("Skipping integration test: TUSK_PROXY_TEST_API_KEY must be set") |
| 36 | + } |
| 37 | + |
| 38 | + baseURL := api.GetBaseURL() // Defaults to https://api.usetusk.ai |
| 39 | + |
| 40 | + // Build request body using the same struct as production code |
| 41 | + requestBody := createMessageRequest{ |
| 42 | + Model: "claude-sonnet-4-5-20250929", |
| 43 | + MaxTokens: 1, |
| 44 | + System: SystemPrompt, |
| 45 | + Messages: []Message{{Role: "user", Content: []Content{{Type: "text", Text: "test"}}}}, |
| 46 | + Stream: false, |
| 47 | + } |
| 48 | + |
| 49 | + bodyBytes, err := json.Marshal(requestBody) |
| 50 | + if err != nil { |
| 51 | + t.Fatalf("Failed to marshal request body: %v", err) |
| 52 | + } |
| 53 | + |
| 54 | + // Create request |
| 55 | + endpoint := baseURL + "/api/drift/setup-agent" |
| 56 | + req, err := http.NewRequest("POST", endpoint, bytes.NewReader(bodyBytes)) |
| 57 | + if err != nil { |
| 58 | + t.Fatalf("Failed to create request: %v", err) |
| 59 | + } |
| 60 | + |
| 61 | + // Set headers |
| 62 | + req.Header.Set("Content-Type", "application/json") |
| 63 | + req.Header.Set("x-api-key", apiKey) // API key auth (no expiration) |
| 64 | + req.Header.Set("x-tusk-cli-version", version.Version) |
| 65 | + req.Header.Set("x-tusk-validate-only", "true") // Skip Anthropic proxy, just validate |
| 66 | + |
| 67 | + // Send request |
| 68 | + client := &http.Client{Timeout: 30 * time.Second} |
| 69 | + resp, err := client.Do(req) |
| 70 | + if err != nil { |
| 71 | + t.Fatalf("Request failed: %v", err) |
| 72 | + } |
| 73 | + defer func() { _ = resp.Body.Close() }() |
| 74 | + |
| 75 | + // Read response body for error details |
| 76 | + respBody, _ := io.ReadAll(resp.Body) |
| 77 | + |
| 78 | + // Check for prompt mismatch error (403) |
| 79 | + if resp.StatusCode == http.StatusForbidden { |
| 80 | + var errorResp struct { |
| 81 | + Error string `json:"error"` |
| 82 | + Code string `json:"code"` |
| 83 | + } |
| 84 | + if json.Unmarshal(respBody, &errorResp) == nil && errorResp.Code == "PROMPT_MISMATCH" { |
| 85 | + t.Fatalf(` |
| 86 | +================================================================================ |
| 87 | +SYSTEM PROMPT MISMATCH DETECTED |
| 88 | +================================================================================ |
| 89 | +The backend rejected the current system prompt. This means the CLI's system.md |
| 90 | +has changed but the backend's PROMPT_VERSION_RANGES has not been updated. |
| 91 | +
|
| 92 | +To fix this: |
| 93 | +1. Update PROMPT_VERSION_RANGES in tusk/backend/src/api/drift/config/setupAgentPrompts.ts |
| 94 | +2. Deploy the backend changes before releasing this CLI version |
| 95 | +
|
| 96 | +Error: %s |
| 97 | +Code: %s |
| 98 | +CLI Version: %s |
| 99 | +================================================================================ |
| 100 | +`, errorResp.Error, errorResp.Code, version.Version) |
| 101 | + } |
| 102 | + |
| 103 | + // Other 403 errors (e.g., auth issues) |
| 104 | + t.Fatalf("Request returned 403 Forbidden: %s", string(respBody)) |
| 105 | + } |
| 106 | + |
| 107 | + // Check for other client errors (400-499) that might indicate issues |
| 108 | + if resp.StatusCode >= 400 && resp.StatusCode < 500 { |
| 109 | + t.Fatalf("Request returned client error %d: %s", resp.StatusCode, string(respBody)) |
| 110 | + } |
| 111 | + |
| 112 | + // 200 or streaming response means the prompt was accepted |
| 113 | + // (We don't need to consume the full response - just knowing it started is enough) |
| 114 | + t.Logf("System prompt accepted by backend (status: %d, version: %s)", resp.StatusCode, version.Version) |
| 115 | +} |
0 commit comments