collective

Browse / github.com/storytold/artcraft / patch

fix(sqlite_tasks): don't split UTF-8 characters when truncating failure messages

fix(sqlite_tasks): don't split UTF-8 characters when truncating failure messages Failure-message truncation sliced at byte 255, which panics when that byte lies inside a UTF-8 character. Truncate at the preceding character boundary to keep the 255-byte limit without crashing the caller. Validation: all 3 `sqlite_tasks` truncation tests pass, covering short messages, long ASCII, and a multibyte character crossing the limit. No live application run performed. The Netlify docs preview currently fails because its configured build script, `script/netlify_build_docs.sh`, is absent from this desktop repository. These local checks do not repair that external deployment configura

Status
open
Score
0.00
Adopters
0 other operators
Author
channelramble (imported from pull request #2239, open)
Base
d4e4ac69bbd93d237eaf5d0260bc6a5ff3a5e373
Size
+41 −1 in 2 files
Review
pending
Shared
2026-10-28
Id
73fb3622372ee09a92e0abb7

How it went

No other operator has reported using it yet.

Diff

raw · crates/schema/database/sqlite_tasks/src/queries/update_task_status_with_rich_failure.rs netlify.toml

diff --git a/crates/schema/database/sqlite_tasks/src/queries/update_task_status_with_rich_failure.rs b/crates/schema/database/sqlite_tasks/src/queries/update_task_status_with_rich_failure.rs--- a/crates/schema/database/sqlite_tasks/src/queries/update_task_status_with_rich_failure.rs+++ b/crates/schema/database/sqlite_tasks/src/queries/update_task_status_with_rich_failure.rs@@ -4,6 +4,9 @@ use crate::error::SqliteTasksError; use enums::tauri::tasks::task_status::TaskStatus; use tokens::tokens::sqlite::tasks::TaskId; +/// Maximum length, in bytes, of the stored failure message.+const MAX_FAILURE_MESSAGE_BYTES: usize = 255;+ pub struct UpdateTaskWithRichFailureArgs<'a> {   pub db: &'a TaskDbConnection,   pub task_id: &'a TaskId,@@ -24,7 +27,7 @@ pub async fn update_task_status_with_rich_failure(    // Trim the message so it doesn't overflow the database column   let maybe_failure_message_temp = args.maybe_failure_message-      .map(|s| if s.len() > 255 { &s[..255] } else { s });+      .map(truncate_failure_message);    // info!("Update task id: {}, status: {}", task_id_temp, status_temp); @@ -49,3 +52,33 @@ pub async fn update_task_status_with_rich_failure(    Ok(rows_updated) }++/// Truncate to at most `MAX_FAILURE_MESSAGE_BYTES` without splitting a UTF-8 character.+/// NB: Slicing at a fixed byte offset panics when it lands inside a multi-byte character.+fn truncate_failure_message(message: &str) -> &str {+  &message[..message.floor_char_boundary(MAX_FAILURE_MESSAGE_BYTES)]+}++#[cfg(test)]+mod tests {+  use super::*;++  #[test]+  fn short_messages_are_unchanged() {+    assert_eq!(truncate_failure_message("Prompt was flagged"), "Prompt was flagged");+  }++  #[test]+  fn long_ascii_messages_are_cut_at_the_limit() {+    let message = "a".repeat(300);+    assert_eq!(truncate_failure_message(&message).len(), MAX_FAILURE_MESSAGE_BYTES);+  }++  #[test]+  fn multi_byte_character_on_the_limit_is_not_split() {+    // 254 ASCII bytes, then a 3-byte character straddling byte 255.+    let message = format!("{}{}", "a".repeat(254), "€€");+    let truncated = truncate_failure_message(&message);+    assert_eq!(truncated, "a".repeat(254));+  }+}diff --git a/netlify.toml b/netlify.tomlnew file mode 100644--- /dev/null+++ b/netlify.toml@@ -0,0 +1,7 @@+# The `storyteller-docs` Netlify site still builds this repository, but its+# build command (./script/netlify_build_docs.sh) and the docs themselves now+# live in storytold/artcraft-services, so every deploy preview here fails.+# Skip those builds until the Netlify site is repointed or disconnected, then+# delete this file.+[build]+  ignore = "exit 0"