Compare commits

...
Author SHA1 Message Date
Brandon Thomas aa0909e8c0 update queries 2026-03-13 10:43:57 -04:00
Brandon Thomas 86727fdef7 this shouldn't change 2026-03-13 10:42:08 -04:00
Brandon Thomas f1698941a4 update endpoints to pass params 2026-03-13 10:38:46 -04:00
Brandon Thomas 79bb6a73a0 add job failure test piece 2026-03-13 10:30:56 -04:00
Brandon Thomas fcd1b2d4b2 update query infra 2026-03-13 10:29:48 -04:00
Brandon Thomas a030056b78 test for synthetic failure 2026-03-13 10:18:19 -04:00
Brandon Thomas b1e02916ed API returns backwards compat types 2026-03-13 09:58:51 -04:00
Brandon Thomas 2af19432c9 new enums 2026-03-13 09:47:55 -04:00
49 changed files with 515 additions and 36 deletions
@@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = NULL,\n maybe_model_token = NULL,\n\n maybe_input_source_token = NULL,\n maybe_input_source_token_type = NULL,\n\n maybe_download_url = NULL,\n maybe_cover_image_media_file_token = NULL,\n \n maybe_prompt_token = ?,\n\n maybe_raw_inference_text = NULL,\n\n maybe_inference_args = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n priority_level = 0,\n is_keepalive_required = FALSE,\n max_duration_seconds = 0,\n\n is_debug_request = FALSE,\n maybe_routing_tag = NULL,\n\n status = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 14
},
"nullable": []
},
"hash": "002cdb3d086f3b4181e2997befa54bd8516cb1d3d3bb6e389862695318d5c299"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = NULL,\n maybe_model_token = NULL,\n\n maybe_input_source_token = NULL,\n maybe_input_source_token_type = NULL,\n\n maybe_download_url = NULL,\n maybe_cover_image_media_file_token = NULL,\n\n maybe_prompt_token = ?,\n\n maybe_raw_inference_text = NULL,\n\n maybe_inference_args = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n priority_level = 0,\n is_keepalive_required = FALSE,\n max_duration_seconds = 0,\n\n is_debug_request = FALSE,\n maybe_routing_tag = NULL,\n\n frontend_failure_category = ?,\n failure_reason = ?,\n\n status = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 16
},
"nullable": []
},
"hash": "29916e73a1784ea0b70ef4f637520b05efccc31e01840f681e9b042f82eb8f7e"
}
@@ -9,6 +9,8 @@ use serde::Deserialize;
use serde::Serialize;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use utoipa::{IntoParams, ToSchema};
use enums::api_safe::by_table::generic_inference_jobs::frontend_failure_category_for_api_clients::FrontendFailureCategoryForApiClients;
use enums::api_safe::by_table::generic_inference_jobs::frontend_failure_category_for_old_clients::FrontendFailureCategoryForOldClients;
use tokens::tokens::batch_generations::BatchGenerationToken;
pub const LIST_SESSION_JOBS_URL_PATH: &str = "/v1/jobs/session";
@@ -98,9 +100,16 @@ pub struct ListSessionStatusDetailsResponse {
/// This is typically only for non-premium users.
pub requires_keepalive: bool,
/// NB: DO NOT CHANGE: Old clients can't read the new enum payload values as they are introduced.
///
/// An enum the frontend can use to display localized/I18N error
/// messages. These pertain to both transient and permanent failures.
pub maybe_failure_category: Option<FrontendFailureCategory>,
#[deprecated(note="use maybe_failure_category_updated")]
pub maybe_failure_category: Option<FrontendFailureCategoryForOldClients>,
/// An enum the frontend can use to display localized/I18N error
/// messages. These pertain to both transient and permanent failures.
pub maybe_failure_category_updated: Option<FrontendFailureCategoryForApiClients>,
/// This is an integer number between 0 and 100 (both inclusive) that
/// reports the completeness.
@@ -125,4 +134,3 @@ pub struct ListSessionResultDetailsResponse {
pub maybe_successfully_completed_at: Option<DateTime<Utc>>,
}
@@ -63,6 +63,8 @@ pub async fn insert_generic_inference_job_for_fal_queue<'e, 'c : 'e, E>(args: In
creator_ip_address: args.creator_ip_address,
creator_set_visibility: args.creator_set_visibility,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
mysql_executor: args.mysql_executor,
phantom: args.phantom,
};
@@ -2,6 +2,7 @@ use idempotency::uuid::generate_random_uuid;
use sqlx::{Executor, MySql};
use std::marker::PhantomData;
use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
use enums::common::job_status_plus::JobStatusPlus;
use enums::common::visibility::Visibility;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
@@ -37,6 +38,9 @@ pub struct InsertGenericInferenceForFalMockFailureArgs<'e, 'c, E>
pub creator_ip_address: &'e str,
pub creator_set_visibility: Visibility,
pub maybe_frontend_failure_category: Option<FrontendFailureCategory>,
pub maybe_failure_reason: Option<&'e str>,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
@@ -62,6 +66,8 @@ pub async fn insert_generic_inference_job_for_fal_queue_mock_failure<'e, 'c: 'e,
creator_ip_address: args.creator_ip_address,
creator_set_visibility: args.creator_set_visibility,
starting_job_status_override: Some(JobStatusPlus::CompleteFailure),
maybe_frontend_failure_category: args.maybe_frontend_failure_category,
maybe_failure_reason: args.maybe_failure_reason,
mysql_executor: args.mysql_executor,
phantom: args.phantom,
};
@@ -4,41 +4,38 @@ use sqlx::MySqlPool;
use sqlx::{Executor, MySql};
use std::marker::PhantomData;
use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
use enums::by_table::generic_inference_jobs::inference_input_source_token_type::InferenceInputSourceTokenType;
use enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty;
use enums::by_table::generic_inference_jobs::inference_job_product_category::InferenceJobProductCategory;
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
use enums::by_table::generic_inference_jobs::inference_model_type::InferenceModelType;
use enums::common::job_status_plus::JobStatusPlus;
use enums::common::visibility::Visibility;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::prompts::PromptToken;
use tokens::tokens::users::UserToken;
use crate::errors::database_query_error::DatabaseQueryError;
use crate::payloads::generic_inference_args::generic_inference_args::GenericInferenceArgs;
use crate::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
use crate::queries::tts::tts_inference_jobs::kill_tts_inference_jobs::JobStatus;
pub struct InsertGenericInferenceForFalWithAprioriJobTokenArgs<'e, 'c, E>
pub struct InsertGenericInferenceForFalWithAprioriJobTokenArgs<'e, 'c, E>
where E: 'e + Executor<'c, Database = MySql>
{
pub uuid_idempotency_token: &'e str,
// NOTE: We'll generate this ahead of time so we can save it with billing info!
pub apriori_job_token: &'e InferenceJobToken,
/// The external primary key identifier for the job.
pub maybe_external_third_party_id: &'e str,
pub fal_category: FalCategory,
pub maybe_inference_args: Option<GenericInferenceArgs>,
pub maybe_prompt_token: Option<&'e PromptToken>,
pub maybe_creator_user_token: Option<&'e UserToken>,
@@ -50,8 +47,11 @@ pub struct InsertGenericInferenceForFalWithAprioriJobTokenArgs<'e, 'c, E>
/// Set to `Some(JobStatusPlus::CompleteFailure)` for mock/test failure jobs.
pub starting_job_status_override: Option<JobStatusPlus>,
pub maybe_frontend_failure_category: Option<FrontendFailureCategory>,
pub maybe_failure_reason: Option<&'e str>,
pub mysql_executor: E,
// TODO: Not sure if this works to tell the compiler we need the lifetime annotation.
// See: https://doc.rust-lang.org/std/marker/struct.PhantomData.html#unused-lifetime-parameters
pub phantom: PhantomData<&'c E>,
@@ -87,11 +87,16 @@ pub async fn insert_generic_inference_job_for_fal_queue_with_apriori_job_token<'
),
};
let maybe_external_third_party = InferenceJobExternalThirdParty::Fal;
let status = args.starting_job_status_override.unwrap_or(JobStatusPlus::Pending);
let maybe_frontend_failure_category_str = args.maybe_frontend_failure_category
.map(|c| c.to_str());
let maybe_truncated_failure_reason = args.maybe_failure_reason
.map(|s| if s.len() > 255 { &s[..255] } else { s });
let query = sqlx::query!(
r#"
INSERT INTO generic_inference_jobs
@@ -115,7 +120,7 @@ SET
maybe_download_url = NULL,
maybe_cover_image_media_file_token = NULL,
maybe_prompt_token = ?,
maybe_raw_inference_text = NULL,
@@ -134,19 +139,22 @@ SET
is_debug_request = FALSE,
maybe_routing_tag = NULL,
frontend_failure_category = ?,
failure_reason = ?,
status = ?
"#,
args.apriori_job_token.as_str(),
args.uuid_idempotency_token,
JOB_TYPE.to_str(),
maybe_external_third_party.to_str(),
args.maybe_external_third_party_id,
product_category.to_str(),
inference_category.to_str(),
args.maybe_prompt_token.map(|t| t.to_string()),
serialized_args_payload,
@@ -156,6 +164,9 @@ SET
args.creator_ip_address,
args.creator_set_visibility.to_str(),
maybe_frontend_failure_category_str,
maybe_truncated_failure_reason,
status.to_str(),
);
@@ -0,0 +1,122 @@
use crate::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
use utoipa::ToSchema;
/// A forward-compatible version of `FrontendFailureCategory` for API clients.
///
/// Contains all known variants plus an `Unknown(String)` catch-all so that
/// newer server-side values never cause deserialization failures on the client.
#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum FrontendFailureCategoryForApiClients {
FaceNotDetected,
KeepAliveElapsed,
NotYetImplemented,
RetryableWorkerError,
ModelRulesViolation,
RuleBansUserImage,
RuleBansUserImageWithFaces,
RuleBansUserTextPrompt,
RuleBansUserContent,
RuleBansGeneratedVideo,
RuleBansGeneratedAudio,
RuleBansGeneratedContent,
GenerationFailed,
/// Catch-all for values the client doesn't yet know about.
/// The contained string is the raw serialized value from the server.
#[serde(untagged)]
Unknown(String),
}
impl FrontendFailureCategoryForApiClients {
pub fn from_db_enum(value: FrontendFailureCategory) -> Self {
match value {
FrontendFailureCategory::FaceNotDetected => Self::FaceNotDetected,
FrontendFailureCategory::KeepAliveElapsed => Self::KeepAliveElapsed,
FrontendFailureCategory::NotYetImplemented => Self::NotYetImplemented,
FrontendFailureCategory::RetryableWorkerError => Self::RetryableWorkerError,
FrontendFailureCategory::ModelRulesViolation => Self::ModelRulesViolation,
FrontendFailureCategory::RuleBansUserImage => Self::RuleBansUserImage,
FrontendFailureCategory::RuleBansUserImageWithFaces => Self::RuleBansUserImageWithFaces,
FrontendFailureCategory::RuleBansUserTextPrompt => Self::RuleBansUserTextPrompt,
FrontendFailureCategory::RuleBansUserContent => Self::RuleBansUserContent,
FrontendFailureCategory::RuleBansGeneratedVideo => Self::RuleBansGeneratedVideo,
FrontendFailureCategory::RuleBansGeneratedAudio => Self::RuleBansGeneratedAudio,
FrontendFailureCategory::RuleBansGeneratedContent => Self::RuleBansGeneratedContent,
FrontendFailureCategory::GenerationFailed => Self::GenerationFailed,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_deserialize_known_variants() {
let cases = vec![
("\"face_not_detected\"", FrontendFailureCategoryForApiClients::FaceNotDetected),
("\"keep_alive_elapsed\"", FrontendFailureCategoryForApiClients::KeepAliveElapsed),
("\"not_yet_implemented\"", FrontendFailureCategoryForApiClients::NotYetImplemented),
("\"retryable_worker_error\"", FrontendFailureCategoryForApiClients::RetryableWorkerError),
("\"model_rules_violation\"", FrontendFailureCategoryForApiClients::ModelRulesViolation),
("\"rule_bans_user_image\"", FrontendFailureCategoryForApiClients::RuleBansUserImage),
("\"rule_bans_user_image_with_faces\"", FrontendFailureCategoryForApiClients::RuleBansUserImageWithFaces),
("\"rule_bans_user_text_prompt\"", FrontendFailureCategoryForApiClients::RuleBansUserTextPrompt),
("\"rule_bans_user_content\"", FrontendFailureCategoryForApiClients::RuleBansUserContent),
("\"rule_bans_generated_video\"", FrontendFailureCategoryForApiClients::RuleBansGeneratedVideo),
("\"rule_bans_generated_audio\"", FrontendFailureCategoryForApiClients::RuleBansGeneratedAudio),
("\"rule_bans_generated_content\"", FrontendFailureCategoryForApiClients::RuleBansGeneratedContent),
("\"generation_failed\"", FrontendFailureCategoryForApiClients::GenerationFailed),
];
for (json, expected) in cases {
let parsed: FrontendFailureCategoryForApiClients = serde_json::from_str(json)
.unwrap_or_else(|e| panic!("failed to parse {}: {}", json, e));
assert_eq!(parsed, expected, "mismatch for {}", json);
}
}
#[test]
fn test_deserialize_unknown_variant() {
let json = "\"some_future_category\"";
let parsed: FrontendFailureCategoryForApiClients = serde_json::from_str(json).unwrap();
assert_eq!(parsed, FrontendFailureCategoryForApiClients::Unknown("some_future_category".to_string()));
}
#[test]
fn test_deserialize_another_unknown_variant() {
let json = "\"totally_new_thing\"";
let parsed: FrontendFailureCategoryForApiClients = serde_json::from_str(json).unwrap();
assert_eq!(parsed, FrontendFailureCategoryForApiClients::Unknown("totally_new_thing".to_string()));
}
#[test]
fn test_serialize_known_variants() {
assert_eq!(serde_json::to_string(&FrontendFailureCategoryForApiClients::FaceNotDetected).unwrap(), "\"face_not_detected\"");
assert_eq!(serde_json::to_string(&FrontendFailureCategoryForApiClients::ModelRulesViolation).unwrap(), "\"model_rules_violation\"");
assert_eq!(serde_json::to_string(&FrontendFailureCategoryForApiClients::GenerationFailed).unwrap(), "\"generation_failed\"");
}
#[test]
fn test_serialize_unknown_variant() {
let unknown = FrontendFailureCategoryForApiClients::Unknown("some_future_category".to_string());
assert_eq!(serde_json::to_string(&unknown).unwrap(), "\"some_future_category\"");
}
#[test]
fn test_from_db_enum() {
assert_eq!(
FrontendFailureCategoryForApiClients::from_db_enum(FrontendFailureCategory::FaceNotDetected),
FrontendFailureCategoryForApiClients::FaceNotDetected
);
assert_eq!(
FrontendFailureCategoryForApiClients::from_db_enum(FrontendFailureCategory::GenerationFailed),
FrontendFailureCategoryForApiClients::GenerationFailed
);
assert_eq!(
FrontendFailureCategoryForApiClients::from_db_enum(FrontendFailureCategory::RuleBansGeneratedContent),
FrontendFailureCategoryForApiClients::RuleBansGeneratedContent
);
}
}
@@ -0,0 +1,81 @@
use crate::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
use utoipa::ToSchema;
/// A restricted subset of `FrontendFailureCategory` that only includes values
/// known to old deployed clients. New failure categories are omitted so that
/// old clients never receive an enum value they cannot deserialize.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Deserialize, Serialize, ToSchema)]
pub enum FrontendFailureCategoryForOldClients {
#[serde(rename = "face_not_detected")]
FaceNotDetected,
#[serde(rename = "keep_alive_elapsed")]
KeepAliveElapsed,
#[serde(rename = "not_yet_implemented")]
NotYetImplemented,
#[serde(rename = "retryable_worker_error")]
RetryableWorkerError,
}
impl FrontendFailureCategoryForOldClients {
/// Attempts to convert a database-level `FrontendFailureCategory` into the
/// restricted old-client enum. Returns `None` for categories that old
/// clients do not understand.
pub fn try_from_db_enum(value: FrontendFailureCategory) -> Option<Self> {
match value {
FrontendFailureCategory::FaceNotDetected => Some(Self::FaceNotDetected),
FrontendFailureCategory::KeepAliveElapsed => Some(Self::KeepAliveElapsed),
FrontendFailureCategory::NotYetImplemented => Some(Self::NotYetImplemented),
FrontendFailureCategory::RetryableWorkerError => Some(Self::RetryableWorkerError),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_known_variants_convert() {
assert_eq!(
FrontendFailureCategoryForOldClients::try_from_db_enum(FrontendFailureCategory::FaceNotDetected),
Some(FrontendFailureCategoryForOldClients::FaceNotDetected)
);
assert_eq!(
FrontendFailureCategoryForOldClients::try_from_db_enum(FrontendFailureCategory::KeepAliveElapsed),
Some(FrontendFailureCategoryForOldClients::KeepAliveElapsed)
);
assert_eq!(
FrontendFailureCategoryForOldClients::try_from_db_enum(FrontendFailureCategory::NotYetImplemented),
Some(FrontendFailureCategoryForOldClients::NotYetImplemented)
);
assert_eq!(
FrontendFailureCategoryForOldClients::try_from_db_enum(FrontendFailureCategory::RetryableWorkerError),
Some(FrontendFailureCategoryForOldClients::RetryableWorkerError)
);
}
#[test]
fn test_new_variants_return_none() {
assert_eq!(FrontendFailureCategoryForOldClients::try_from_db_enum(FrontendFailureCategory::ModelRulesViolation), None);
assert_eq!(FrontendFailureCategoryForOldClients::try_from_db_enum(FrontendFailureCategory::RuleBansUserImage), None);
assert_eq!(FrontendFailureCategoryForOldClients::try_from_db_enum(FrontendFailureCategory::RuleBansUserImageWithFaces), None);
assert_eq!(FrontendFailureCategoryForOldClients::try_from_db_enum(FrontendFailureCategory::RuleBansUserTextPrompt), None);
assert_eq!(FrontendFailureCategoryForOldClients::try_from_db_enum(FrontendFailureCategory::RuleBansUserContent), None);
assert_eq!(FrontendFailureCategoryForOldClients::try_from_db_enum(FrontendFailureCategory::RuleBansGeneratedVideo), None);
assert_eq!(FrontendFailureCategoryForOldClients::try_from_db_enum(FrontendFailureCategory::RuleBansGeneratedAudio), None);
assert_eq!(FrontendFailureCategoryForOldClients::try_from_db_enum(FrontendFailureCategory::RuleBansGeneratedContent), None);
assert_eq!(FrontendFailureCategoryForOldClients::try_from_db_enum(FrontendFailureCategory::GenerationFailed), None);
}
#[test]
fn test_serialization() {
assert_eq!(serde_json::to_string(&FrontendFailureCategoryForOldClients::FaceNotDetected).unwrap(), "\"face_not_detected\"");
assert_eq!(serde_json::to_string(&FrontendFailureCategoryForOldClients::KeepAliveElapsed).unwrap(), "\"keep_alive_elapsed\"");
assert_eq!(serde_json::to_string(&FrontendFailureCategoryForOldClients::NotYetImplemented).unwrap(), "\"not_yet_implemented\"");
assert_eq!(serde_json::to_string(&FrontendFailureCategoryForOldClients::RetryableWorkerError).unwrap(), "\"retryable_worker_error\"");
}
}
@@ -0,0 +1,2 @@
pub mod frontend_failure_category_for_old_clients;
pub mod frontend_failure_category_for_api_clients;
@@ -0,0 +1 @@
pub mod generic_inference_jobs;
@@ -0,0 +1 @@
pub mod by_table;
+1
View File
@@ -37,3 +37,4 @@ pub mod error;
pub mod no_table;
pub mod tauri;
pub mod traits;
pub mod api_safe;
@@ -1,3 +1,31 @@
use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
pub struct SyntheticFailure {
pub frontend_failure_category: FrontendFailureCategory,
pub frontend_failure_message: Option<String>,
}
/// If the prompt contains a trigger phrase, parse a `FrontendFailureCategory`
/// from the remaining words (falling back to `GenerationFailed`) and return a
/// `SyntheticFailure`. Returns `None` when the trigger phrase is absent.
pub fn test_synthetic_failure_reason(prompt: &str) -> Option<SyntheticFailure> {
let lower = prompt.to_ascii_lowercase();
if !lower.contains("simulate_artcraft_failure") && !lower.contains("test_artcraft_failure") {
return None;
}
let maybe_category = prompt
.split_whitespace()
.find_map(|word| FrontendFailureCategory::from_str(word).ok());
let category = maybe_category.unwrap_or(FrontendFailureCategory::GenerationFailed);
Some(SyntheticFailure {
frontend_failure_category: category,
frontend_failure_message: Some("This was a simulated failure".to_string()),
})
}
/// Returns `true` if the prompt contains the secret test phrase that triggers
/// a synthetic job failure. The job record is inserted with
/// `JobStatusPlus::CompleteFailure` immediately, without calling any external
@@ -5,3 +33,57 @@
pub fn is_job_failure_test(prompt: &str) -> bool {
prompt.to_ascii_lowercase().contains("artcraft_test_job_failure")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_trigger_returns_none() {
assert!(test_synthetic_failure_reason("a normal prompt").is_none());
}
#[test]
fn trigger_simulate_with_no_category_defaults_to_generation_failed() {
let result = test_synthetic_failure_reason("simulate_artcraft_failure").unwrap();
assert_eq!(result.frontend_failure_category, FrontendFailureCategory::GenerationFailed);
assert_eq!(result.frontend_failure_message.as_deref(), Some("This was a simulated failure"));
}
#[test]
fn trigger_test_with_no_category_defaults_to_generation_failed() {
let result = test_synthetic_failure_reason("test_artcraft_failure").unwrap();
assert_eq!(result.frontend_failure_category, FrontendFailureCategory::GenerationFailed);
assert_eq!(result.frontend_failure_message.as_deref(), Some("This was a simulated failure"));
}
#[test]
fn trigger_with_known_category() {
let result = test_synthetic_failure_reason("simulate_artcraft_failure face_not_detected").unwrap();
assert_eq!(result.frontend_failure_category, FrontendFailureCategory::FaceNotDetected);
}
#[test]
fn trigger_with_category_before_keyword() {
let result = test_synthetic_failure_reason("rule_bans_user_image simulate_artcraft_failure").unwrap();
assert_eq!(result.frontend_failure_category, FrontendFailureCategory::RuleBansUserImage);
}
#[test]
fn trigger_with_model_rules_violation() {
let result = test_synthetic_failure_reason("test_artcraft_failure model_rules_violation please").unwrap();
assert_eq!(result.frontend_failure_category, FrontendFailureCategory::ModelRulesViolation);
}
#[test]
fn trigger_is_case_insensitive() {
let result = test_synthetic_failure_reason("SIMULATE_ARTCRAFT_FAILURE").unwrap();
assert_eq!(result.frontend_failure_category, FrontendFailureCategory::GenerationFailed);
}
#[test]
fn trigger_embedded_in_text() {
let result = test_synthetic_failure_reason("hello world simulate_artcraft_failure keep_alive_elapsed goodbye").unwrap();
assert_eq!(result.frontend_failure_category, FrontendFailureCategory::KeepAliveElapsed);
}
}
@@ -1,2 +1,3 @@
pub mod job_failure_test;
pub mod payments_error_test;
pub mod payments_error_test;
mod test_synthetic_failure_reason;
@@ -0,0 +1,83 @@
use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
pub struct SyntheticFailure {
pub frontend_failure_category: FrontendFailureCategory,
pub frontend_failure_message: Option<String>,
}
/// If the prompt contains the secret trigger phrase, parse a
/// `FrontendFailureCategory` from the remaining words (falling back to
/// `GenerationFailed`) and return a `SyntheticFailure`.
///
/// Returns `None` when the trigger phrase is absent.
pub fn test_synthetic_failure_reason(prompt: &str) -> Option<SyntheticFailure> {
let lower = prompt.to_ascii_lowercase();
if !lower.contains("simulate_artcraft_failure") && !lower.contains("test_artcraft_failure") {
return None;
}
let maybe_category = prompt
.split_whitespace()
.find_map(|word| FrontendFailureCategory::from_str(word).ok());
let category = maybe_category.unwrap_or(FrontendFailureCategory::GenerationFailed);
Some(SyntheticFailure {
frontend_failure_category: category,
frontend_failure_message: Some("This was a simulated failure".to_string()),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_trigger_returns_none() {
assert!(test_synthetic_failure_reason("a normal prompt").is_none());
}
#[test]
fn trigger_simulate_with_no_category_defaults_to_generation_failed() {
let result = test_synthetic_failure_reason("simulate_artcraft_failure").unwrap();
assert_eq!(result.frontend_failure_category, FrontendFailureCategory::GenerationFailed);
assert_eq!(result.frontend_failure_message.as_deref(), Some("This was a simulated failure"));
}
#[test]
fn trigger_test_with_no_category_defaults_to_generation_failed() {
let result = test_synthetic_failure_reason("test_artcraft_failure").unwrap();
assert_eq!(result.frontend_failure_category, FrontendFailureCategory::GenerationFailed);
assert_eq!(result.frontend_failure_message.as_deref(), Some("This was a simulated failure"));
}
#[test]
fn trigger_with_known_category() {
let result = test_synthetic_failure_reason("simulate_artcraft_failure face_not_detected").unwrap();
assert_eq!(result.frontend_failure_category, FrontendFailureCategory::FaceNotDetected);
}
#[test]
fn trigger_with_category_before_keyword() {
let result = test_synthetic_failure_reason("rule_bans_user_image simulate_artcraft_failure").unwrap();
assert_eq!(result.frontend_failure_category, FrontendFailureCategory::RuleBansUserImage);
}
#[test]
fn trigger_with_model_rules_violation() {
let result = test_synthetic_failure_reason("test_artcraft_failure model_rules_violation please").unwrap();
assert_eq!(result.frontend_failure_category, FrontendFailureCategory::ModelRulesViolation);
}
#[test]
fn trigger_is_case_insensitive() {
let result = test_synthetic_failure_reason("SIMULATE_ARTCRAFT_FAILURE").unwrap();
assert_eq!(result.frontend_failure_category, FrontendFailureCategory::GenerationFailed);
}
#[test]
fn trigger_embedded_in_text() {
let result = test_synthetic_failure_reason("hello world simulate_artcraft_failure keep_alive_elapsed goodbye").unwrap();
assert_eq!(result.frontend_failure_category, FrontendFailureCategory::KeepAliveElapsed);
}
}
@@ -216,6 +216,8 @@ pub async fn flux_2_lora_edit_image_angle_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -217,6 +217,8 @@ pub async fn qwen_edit_2511_edit_image_angle_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -279,6 +279,8 @@ pub async fn gemini_25_flash_edit_image_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -301,6 +301,8 @@ pub async fn gpt_image_1_edit_image_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -253,6 +253,8 @@ pub async fn qwen_edit_image_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -2,7 +2,7 @@ use std::sync::Arc;
use crate::billing::wallets::temporary_test_wallet_deduction::temporary_test_wallet_deduction;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoints::generate::common::job_failure_test::is_job_failure_test;
use crate::http_server::endpoints::generate::common::job_failure_test::test_synthetic_failure_reason;
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::state::server_state::ServerState;
@@ -10,6 +10,7 @@ use actix_web::web::Json;
use actix_web::{web, HttpRequest};
use artcraft_api_defs::generate::image::edit::flux_pro_kontext_max_edit_image::FluxProKontextMaxEditImageNumImages;
use artcraft_api_defs::generate::image::text::generate_flux_1_dev_text_to_image::{GenerateFlux1DevTextToImageAspectRatio, GenerateFlux1DevTextToImageNumImages, GenerateFlux1DevTextToImageRequest, GenerateFlux1DevTextToImageResponse};
use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::model_type::ModelType;
@@ -95,13 +96,15 @@ pub async fn generate_flux_1_dev_text_to_image_handler(
})?;
// Secret test hook: insert a synthetic "complete_failure" job without calling Fal.
if is_job_failure_test(request.prompt.as_deref().unwrap_or("")) {
if let Some(synthetic_failure) = test_synthetic_failure_reason(request.prompt.as_deref().unwrap_or("")) {
return insert_mock_failure_job(
&http_request,
request.prompt.as_deref(),
&request.uuid_idempotency_token,
maybe_user_session.as_ref().map(|s| &s.user_token),
maybe_avt_token.as_ref(),
Some(synthetic_failure.frontend_failure_category),
synthetic_failure.frontend_failure_message.as_deref(),
&mut mysql_connection,
).await;
}
@@ -252,6 +255,8 @@ async fn insert_mock_failure_job(
uuid_idempotency_token: &str,
maybe_creator_user_token: Option<&UserToken>,
maybe_avt_token: Option<&AnonymousVisitorTrackingToken>,
maybe_frontend_failure_category: Option<FrontendFailureCategory>,
maybe_failure_reason: Option<&str>,
mysql_connection: &mut sqlx::pool::PoolConnection<sqlx::MySql>,
) -> Result<Json<GenerateFlux1DevTextToImageResponse>, CommonWebError> {
let ip_address = get_request_ip(http_request);
@@ -295,6 +300,8 @@ async fn insert_mock_failure_job(
maybe_avt_token,
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_frontend_failure_category,
maybe_failure_reason,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
@@ -188,6 +188,8 @@ pub async fn generate_flux_pro_11_text_to_image_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -192,6 +192,8 @@ pub async fn generate_flux_pro_11_ultra_text_to_image_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -235,6 +235,8 @@ pub async fn generate_gpt_image_1_text_to_image_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -245,7 +247,7 @@ pub async fn generate_gpt_image_1_text_to_image_handler(
return Err(CommonWebError::ServerError);
}
};
let _r = transaction
.commit()
.await
@@ -275,6 +275,8 @@ pub async fn bytedance_seedream_5_lite_multi_function_image_gen_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -300,6 +300,8 @@ pub async fn bytedance_seedream_v4_multi_function_image_gen_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -298,6 +298,8 @@ pub async fn bytedance_seedream_v4p5_multi_function_image_gen_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -388,6 +388,8 @@ pub async fn gpt_image_1p5_multi_function_image_gen_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -298,6 +298,8 @@ pub async fn nano_banana_2_multi_function_image_gen_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -331,6 +331,8 @@ pub async fn nano_banana_multi_function_image_gen_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -346,6 +346,8 @@ pub async fn nano_banana_pro_multi_function_image_gen_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -249,6 +249,8 @@ pub async fn generate_kling_1_6_pro_video_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -234,6 +234,8 @@ pub async fn generate_kling_2_1_master_video_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -249,6 +249,8 @@ pub async fn generate_kling_2_1_pro_video_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -264,6 +264,8 @@ pub async fn generate_seedance_1_0_lite_image_to_video_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -241,6 +241,8 @@ pub async fn generate_seedance_1_0_pro_image_to_video_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -251,7 +253,7 @@ pub async fn generate_seedance_1_0_pro_image_to_video_handler(
return Err(CommonWebError::ServerError);
}
};
let _r = transaction
.commit()
.await
@@ -247,6 +247,8 @@ pub async fn generate_veo_2_image_to_video_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -240,6 +240,8 @@ pub async fn generate_veo_3_fast_image_to_video_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -248,6 +248,8 @@ pub async fn generate_veo_3_image_to_video_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -318,6 +318,8 @@ pub async fn kling_2p5_turbo_pro_multi_function_video_gen_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -317,6 +317,8 @@ pub async fn kling_2p6_pro_multi_function_video_gen_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -366,6 +366,8 @@ pub async fn kling_3p0_pro_multi_function_video_gen_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -366,6 +366,8 @@ pub async fn kling_3p0_standard_multi_function_video_gen_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -371,6 +371,8 @@ pub async fn seedance_1p5_pro_multi_function_video_gen_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -337,6 +337,8 @@ pub async fn sora_2_multi_function_video_gen_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -339,6 +339,8 @@ pub async fn sora_2_pro_multi_function_video_gen_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -402,6 +402,8 @@ pub async fn veo_3p1_fast_multi_function_video_gen_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -412,7 +414,7 @@ pub async fn veo_3p1_fast_multi_function_video_gen_handler(
return Err(CommonWebError::ServerError);
}
};
let _r = transaction
.commit()
.await
@@ -402,6 +402,8 @@ pub async fn veo_3p1_multi_function_video_gen_handler(
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}).await;
@@ -412,7 +414,7 @@ pub async fn veo_3p1_multi_function_video_gen_handler(
return Err(CommonWebError::ServerError);
}
};
let _r = transaction
.commit()
.await
@@ -21,6 +21,8 @@ use artcraft_api_defs::jobs::list_session_jobs::{ListSessionJobsItem, ListSessio
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use bucket_paths::legacy::typified_paths::public::voice_conversion_results::bucket_file_path::VoiceConversionResultOriginalFilePath;
use chrono::{DateTime, Utc};
use enums::api_safe::by_table::generic_inference_jobs::frontend_failure_category_for_api_clients::FrontendFailureCategoryForApiClients;
use enums::api_safe::by_table::generic_inference_jobs::frontend_failure_category_for_old_clients::FrontendFailureCategoryForOldClients;
use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
use enums::common::job_status_plus::JobStatusPlus;
@@ -290,7 +292,12 @@ fn db_record_to_response_payload(
maybe_first_started_at: record.maybe_first_started_at,
attempt_count: record.attempt_count as u8,
requires_keepalive: record.is_keepalive_required,
maybe_failure_category: record.maybe_frontend_failure_category,
maybe_failure_category: record
.maybe_frontend_failure_category
.and_then(|val| FrontendFailureCategoryForOldClients::try_from_db_enum(val)),
maybe_failure_category_updated: record
.maybe_frontend_failure_category
.map(|val| FrontendFailureCategoryForApiClients::from_db_enum(val)),
progress_percentage,
maybe_current_execution_duration_seconds,
},