| suppressPackageStartupMessages({ |
| library(asa) |
| }) |
|
|
| args <- commandArgs(trailingOnly = FALSE) |
| file_arg <- "--file=" |
| script_arg <- args[grepl(paste0("^", file_arg), args)] |
| script_path <- if (length(script_arg)) { |
| normalizePath(sub(file_arg, "", script_arg[[1]]), mustWork = TRUE) |
| } else { |
| normalizePath("Tests/api_contract_smoke.R", mustWork = TRUE) |
| } |
|
|
| repo_root <- normalizePath(file.path(dirname(script_path), ".."), mustWork = TRUE) |
| source(file.path(repo_root, "R", "asa_api_helpers.R")) |
|
|
| assert_true <- function(condition, message) { |
| if (!isTRUE(condition)) { |
| stop(message, call. = FALSE) |
| } |
| } |
|
|
| expect_error_contains <- function(expr, pattern) { |
| error_message <- NULL |
| tryCatch( |
| force(expr), |
| error = function(e) { |
| error_message <<- conditionMessage(e) |
| NULL |
| } |
| ) |
|
|
| if (is.null(error_message)) { |
| stop(sprintf("Expected error containing %s, but no error was raised.", sQuote(pattern)), call. = FALSE) |
| } |
| if (!grepl(pattern, error_message, fixed = TRUE)) { |
| stop( |
| sprintf( |
| "Expected error containing %s, got: %s", |
| sQuote(pattern), |
| error_message |
| ), |
| call. = FALSE |
| ) |
| } |
|
|
| invisible(error_message) |
| } |
|
|
| prompts <- asa_api_require_prompts(list(prompts = list(" Hello ", "World "))) |
| assert_true(identical(prompts, c("Hello", "World")), "Valid prompt arrays should be trimmed and preserved.") |
|
|
| expect_error_contains( |
| asa_api_require_prompts(list(prompts = list(list(prompt = "Q1", id = "row1")))), |
| "Structured prompt objects are not supported by `/v1/batch`." |
| ) |
|
|
| expect_error_contains( |
| asa_api_require_prompts(list(prompts = list("Hello", TRUE))), |
| "`prompts` must be a JSON array of non-empty strings." |
| ) |
|
|
| expect_error_contains( |
| asa_api_require_prompts(list(prompts = list("Hello", " "))), |
| "Each entry in `prompts` must be a non-empty string." |
| ) |
|
|
| asa_api_validate_batch_supported_fields(list( |
| prompts = list("Hello", "World"), |
| run = list(output_format = "text", performance_profile = "balanced") |
| )) |
|
|
| expect_error_contains( |
| asa_api_validate_batch_supported_fields(list( |
| prompts = list("Hello"), |
| run = list(expected_schema = list(type = "object")) |
| )), |
| "Unsupported `/v1/batch` `run` keys:" |
| ) |
|
|
| expect_error_contains( |
| asa_api_validate_batch_supported_fields(list( |
| prompts = list("Hello"), |
| use_plan_mode = TRUE |
| )), |
| "Unsupported `/v1/batch` top-level keys:" |
| ) |
|
|
| run_args <- asa_api_build_run_args(list(run = list( |
| expected_schema = list(type = "object"), |
| use_plan_mode = TRUE, |
| performance_profile = "balanced" |
| ))) |
| assert_true( |
| all(c("output_format", "expected_schema", "use_plan_mode", "performance_profile") %in% names(run_args)), |
| "`/v1/run` should continue forwarding newer upstream run_task options." |
| ) |
|
|
| batch_args <- asa_api_build_batch_args(list( |
| run = list(output_format = "json", performance_profile = "quality"), |
| parallel = FALSE |
| )) |
| assert_true( |
| all(c("output_format", "performance_profile", "parallel", "progress") %in% names(batch_args)), |
| "Batch-compatible shared options should still flow into run_task_batch." |
| ) |
|
|
| assert_true( |
| identical(asa_api_has_run_direct_task(), !is.null(asa_api_get_run_direct_task(optional = TRUE))), |
| "Direct-provider capability checks should agree on run_direct_task availability." |
| ) |
|
|
| mock_request <- function(path = "/v1/run", authorization = NULL, x_api_key = NULL) { |
| headers <- list() |
| if (!is.null(authorization)) { |
| headers$authorization <- authorization |
| } |
| if (!is.null(x_api_key)) { |
| headers[["x-api-key"]] <- x_api_key |
| } |
|
|
| list( |
| PATH_INFO = path, |
| HEADERS = headers |
| ) |
| } |
|
|
| assert_true( |
| isTRUE(asa_api_path_requires_bearer_auth("/v1/run")) && |
| isTRUE(asa_api_path_requires_bearer_auth("/v1/batch")), |
| "`/v1/*` routes should require bearer auth." |
| ) |
| assert_true( |
| !isTRUE(asa_api_path_requires_bearer_auth("/healthz")) && |
| !isTRUE(asa_api_path_requires_bearer_auth("/gui/query")), |
| "Health and GUI routes should remain outside bearer auth scope." |
| ) |
| assert_true( |
| identical(asa_api_required_bearer_token(), "999"), |
| "Bearer auth should require the fixed token 999." |
| ) |
| assert_true( |
| identical( |
| asa_api_extract_bearer_token(mock_request(authorization = "Bearer 999")), |
| "999" |
| ), |
| "Bearer extraction should accept the required token." |
| ) |
| assert_true( |
| identical( |
| asa_api_extract_bearer_token(list( |
| PATH_INFO = "/v1/run", |
| HEADERS = list(Authorization = "Bearer 999") |
| )), |
| "999" |
| ), |
| "Bearer extraction should match Authorization headers case-insensitively." |
| ) |
| assert_true( |
| identical( |
| asa_api_extract_bearer_token(mock_request(authorization = "Basic 999")), |
| "" |
| ), |
| "Non-bearer Authorization schemes should not be accepted." |
| ) |
| assert_true( |
| isTRUE(asa_api_has_required_bearer_token(mock_request(authorization = "Bearer 999"))), |
| "Bearer auth should accept Authorization: Bearer 999." |
| ) |
| assert_true( |
| !isTRUE(asa_api_has_required_bearer_token(mock_request(authorization = "Bearer wrong"))), |
| "Bearer auth should reject the wrong token." |
| ) |
| assert_true( |
| !isTRUE(asa_api_has_required_bearer_token(mock_request(x_api_key = "999"))), |
| "Legacy x-api-key auth should no longer be accepted." |
| ) |
|
|
| health_payload <- asa_api_health_payload() |
| assert_true( |
| identical(isTRUE(health_payload$direct_provider_available), isTRUE(asa_api_has_run_direct_task())), |
| "Health payload should report direct-provider availability from the same capability check." |
| ) |
| if (isTRUE(health_payload$direct_provider_available)) { |
| assert_true( |
| is.null(health_payload$direct_provider_note), |
| "Health payload should omit the direct-provider note when the capability is available." |
| ) |
| } else { |
| assert_true( |
| is.character(health_payload$direct_provider_note) && nzchar(trimws(health_payload$direct_provider_note)), |
| "Health payload should explain why direct-provider mode is unavailable." |
| ) |
| } |
|
|
| { |
| original_asa <- asa_api_run_single_via_asa |
| original_direct <- asa_api_run_single_via_direct |
|
|
| dispatch_calls <- character(0) |
| asa_api_run_single_via_asa <- function(payload) { |
| dispatch_calls <<- c(dispatch_calls, "asa") |
| list(status = "success", execution = list(mode = "asa_agent")) |
| } |
| asa_api_run_single_via_direct <- function(payload) { |
| dispatch_calls <<- c(dispatch_calls, "direct") |
| list(status = "success", execution = list(mode = "provider_direct")) |
| } |
|
|
| asa_api_run_single( |
| list(prompt = "Hello", use_direct_provider = TRUE), |
| allow_direct_provider = FALSE |
| ) |
| assert_true( |
| identical(dispatch_calls, "asa"), |
| "Single-run API should ignore the private direct-provider flag." |
| ) |
|
|
| dispatch_calls <- character(0) |
| gui_direct <- asa_api_run_single( |
| list(prompt = "Hello", use_direct_provider = TRUE), |
| allow_direct_provider = TRUE |
| ) |
| assert_true( |
| identical(dispatch_calls, "direct"), |
| "GUI single-run path should dispatch to direct-provider mode when enabled." |
| ) |
| assert_true( |
| identical(gui_direct$execution$mode, "provider_direct"), |
| "Direct-provider dispatch should preserve provider_direct mode metadata." |
| ) |
|
|
| asa_api_run_single_via_asa <- original_asa |
| asa_api_run_single_via_direct <- original_direct |
| } |
|
|
| gui_result <- asa_api_sanitize_gui_result(list( |
| status = "success", |
| execution = list( |
| mode = "provider_direct", |
| config_snapshot = list(backend = "gemini", model = "gemini-3-flash-preview"), |
| payload_integrity = list( |
| backend = "gemini", |
| model = "gemini-3-flash-preview", |
| released_from = "message_text" |
| ), |
| trace_metadata = list( |
| structured_output_backend = "gemini", |
| model_name = "gemini-3-flash-preview" |
| ) |
| ) |
| )) |
| assert_true( |
| is.null(gui_result$execution$config_snapshot$backend) && |
| is.null(gui_result$execution$config_snapshot$model), |
| "GUI responses should not expose backend/model selection in execution metadata." |
| ) |
| assert_true( |
| is.null(gui_result$execution$payload_integrity$backend) && |
| is.null(gui_result$execution$payload_integrity$model), |
| "GUI payload integrity metadata should hide backend/model fields." |
| ) |
|
|
| cat("asa-api contract smoke checks passed\n") |
|
|