diff --git a/application/single_app/config.py b/application/single_app/config.py
index 6d8303386..b15f9fd7e 100644
--- a/application/single_app/config.py
+++ b/application/single_app/config.py
@@ -101,7 +101,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
-VERSION = "0.261.353"
+VERSION = "0.261.354"
IS_DEVELOPMENT = is_development_env_enabled()
# Opt-out for deployments where App Service Easy Auth is active but the platform
diff --git a/application/single_app/functions_document_queries.py b/application/single_app/functions_document_queries.py
index c276cb51c..9f6992a08 100644
--- a/application/single_app/functions_document_queries.py
+++ b/application/single_app/functions_document_queries.py
@@ -3,6 +3,8 @@
from datetime import datetime, timedelta, timezone
+from functions_document_status import is_document_processing_failure
+
DOCUMENT_PLACE_FILTERS = frozenset({
"all", "recent", "shared", "processing", "errors", "untagged",
@@ -11,8 +13,7 @@
def _document_processing_state(document_item):
- status_text = str(document_item.get("status") or "").lower()
- if "error" in status_text or "failed" in status_text:
+ if is_document_processing_failure(document_item.get("status")):
return "error"
percentage = document_item.get("percentage_complete")
diff --git a/application/single_app/functions_document_status.py b/application/single_app/functions_document_status.py
new file mode 100644
index 000000000..d25201d2e
--- /dev/null
+++ b/application/single_app/functions_document_status.py
@@ -0,0 +1,32 @@
+# functions_document_status.py
+"""Document terminal-state predicates, independent of application bootstrap.
+
+Status details can contain filenames, sheet names, or retry diagnostics. Only
+lifecycle phrases at the start of a status can declare a terminal outcome.
+Keep the browser predicates aligned through document_processing_status_cases.json.
+"""
+
+import re
+
+
+_FAILURE = re.compile(
+ r"^(?:(?:error|failed)(?:$|[\s:!]|\.(?:$|\s))"
+ r"|(?:processing|document processing|approval|content screening) failed(?:$|[\s:;!]|\.(?:$|\s))"
+ r"|video:\s*(?:blob upload|auth|upload|indexing) failed(?:$|[\s:!]|\.(?:$|\s)))",
+ re.IGNORECASE,
+)
+_COMPLETE = re.compile(
+ r"^(?:(?:ready|complete|completed|processed|finished)[.!]?$"
+ r"|processing complete(?:$|[.!]$|\s+[-(]))",
+ re.IGNORECASE,
+)
+
+
+def is_document_processing_failure(status):
+ """Recognize terminal failures, not error words in processing details."""
+ return isinstance(status, str) and _FAILURE.match(status.strip()) is not None
+
+
+def is_document_processing_complete(status):
+ """Recognize final completion, not completion of an intermediate stage."""
+ return isinstance(status, str) and _COMPLETE.match(status.strip()) is not None
diff --git a/application/single_app/functions_documents.py b/application/single_app/functions_documents.py
index eb7536322..0fb841465 100644
--- a/application/single_app/functions_documents.py
+++ b/application/single_app/functions_documents.py
@@ -49,6 +49,7 @@
)
from config import *
from functions_appinsights import log_event
+from functions_document_status import is_document_processing_complete, is_document_processing_failure
from functions_document_provenance import (
ORIGIN_FIELD_NAMES,
ORIGIN_KIND_FIELD,
@@ -3508,10 +3509,10 @@ def calculate_processing_percentage(doc_metadata):
total_chunks_saved = doc_metadata.get('current_file_chunk', 0)
# --- Final States ---
- if "processing complete" in status or current_pct == 100:
+ if is_document_processing_complete(status) or current_pct == 100:
# Ensure it stays 100 if it ever reached it
return 100
- if "error" in status or "failed" in status:
+ if is_document_processing_failure(status):
# Keep the last known percentage on error/failure
return current_pct
@@ -3694,7 +3695,7 @@ def update_document(**kwargs):
marker = existing_document.get(SCREENING_FIELD)
if marker is not None and not document_is_available(existing_document):
requested_status = str(kwargs.get("status") or "").lower()
- if "complete" in requested_status:
+ if is_document_processing_complete(requested_status):
kwargs["status"] = "Content review required" if marker.get("review_required") else "Content screening pending"
if kwargs.get("percentage_complete") == 100:
kwargs["percentage_complete"] = 99
@@ -3763,14 +3764,14 @@ def update_document(**kwargs):
elif isinstance(status_lower, dict):
status_lower = json.dumps(status_lower).lower()
- if "processing complete" in status_lower:
+ if is_document_processing_complete(status_lower):
new_percentage = 100
- elif "error" in status_lower or "failed" in status_lower:
+ elif is_document_processing_failure(status_lower):
pass # Percentage already calculated by helper based on 'failed' status
# Ensure percentage doesn't decrease (unless reset on failure or hitting 100)
# Compare against original_percentage fetched *before* any updates in this call
- if new_percentage < original_percentage and new_percentage != 0 and "failed" not in status_lower and "error" not in status_lower:
+ if new_percentage < original_percentage and new_percentage != 0 and not is_document_processing_failure(status_lower):
existing_document['percentage_complete'] = original_percentage
else:
existing_document['percentage_complete'] = new_percentage
@@ -6881,7 +6882,7 @@ def detect_doc_type(document_id, user_id=None):
def _update_document_for_job(*, operation_guard=None, safe_errors=False, **arguments):
status = arguments.get("status")
- if safe_errors and isinstance(status, str) and any(word in status.lower() for word in ("error", "failed")):
+ if safe_errors and is_document_processing_failure(status):
arguments["status"] = "Document processing failed. Contact an administrator if the problem persists."
if operation_guard is None:
return update_document(**arguments)
diff --git a/application/single_app/route_backend_chats.py b/application/single_app/route_backend_chats.py
index c1cee8b08..3d75f5c81 100644
--- a/application/single_app/route_backend_chats.py
+++ b/application/single_app/route_backend_chats.py
@@ -13,6 +13,7 @@
guard_model_callable,
)
from content_screening.contracts import DocumentHeldError, ScreeningConflictError, ScreeningError, document_is_available
+from functions_document_status import is_document_processing_complete, is_document_processing_failure
from agent_execution_context import AgentExecutionCancelled, DelegationBudget
from action_credential_contracts import ActionCredentialsRequired
from agent_delegation_runtime import AgentExecution, delegation_citations, delegation_usage, prepare_agent_execution
@@ -1533,7 +1534,7 @@ def _is_search_ready_chat_upload_workspace_document(document_item):
return False
status = str(document_item.get('status') or '').strip().lower()
- if 'error' in status or 'failed' in status:
+ if is_document_processing_failure(status):
return False
try:
@@ -1548,7 +1549,7 @@ def _is_search_ready_chat_upload_workspace_document(document_item):
except (TypeError, ValueError):
percentage_complete = 0
- return percentage_complete >= 100 or 'processing complete' in status
+ return percentage_complete >= 100 or is_document_processing_complete(status)
def _get_chat_upload_workspace_document_scope(document_item):
diff --git a/application/single_app/static/js/chat/chat-documents.js b/application/single_app/static/js/chat/chat-documents.js
index 5331b1518..7a662920d 100644
--- a/application/single_app/static/js/chat/chat-documents.js
+++ b/application/single_app/static/js/chat/chat-documents.js
@@ -182,10 +182,10 @@ function isConversationTaskDocumentReady(taskDocument = {}) {
if (window.ContentScreening?.isHeld(taskDocument)) return false;
const statusText = String(taskDocument.status || '').trim().toLowerCase();
const percentageComplete = Number(taskDocument.percentage_complete || taskDocument.percentageComplete || 0);
- if (statusText.includes('error') || statusText.includes('failed')) {
+ if (window.DocumentStatus.isFailure(statusText)) {
return false;
}
- return percentageComplete >= 100 || statusText.includes('processing complete') || statusText.includes('complete');
+ return percentageComplete >= 100 || window.DocumentStatus.isComplete(statusText);
}
function normalizeConversationTaskDocument(documentInfo = {}, fallbackConversationId = null) {
diff --git a/application/single_app/static/js/chat/chat-messages.js b/application/single_app/static/js/chat/chat-messages.js
index 4382630a6..b100356e3 100644
--- a/application/single_app/static/js/chat/chat-messages.js
+++ b/application/single_app/static/js/chat/chat-messages.js
@@ -132,15 +132,13 @@ function getChatWorkspaceProgressValue(value) {
function isChatWorkspaceDocumentComplete(doc) {
const statusText = String(doc?.status || '').toLowerCase();
return getChatWorkspaceProgressValue(doc?.percentage_complete) >= 100
- || statusText.includes('processing complete')
- || statusText.includes('complete')
- || statusText.includes('error')
- || statusText.includes('failed');
+ || window.DocumentStatus.isComplete(statusText)
+ || window.DocumentStatus.isFailure(statusText);
}
function isChatWorkspaceDocumentFailure(doc) {
const statusText = String(doc?.status || '').toLowerCase();
- return statusText.includes('error') || statusText.includes('failed');
+ return window.DocumentStatus.isFailure(statusText);
}
function isChatWorkspaceDocumentSuccessComplete(doc) {
@@ -148,8 +146,7 @@ function isChatWorkspaceDocumentSuccessComplete(doc) {
return !isChatWorkspaceDocumentFailure(doc)
&& (
getChatWorkspaceProgressValue(doc?.percentage_complete) >= 100
- || statusText.includes('processing complete')
- || statusText.includes('complete')
+ || window.DocumentStatus.isComplete(statusText)
);
}
@@ -231,9 +228,9 @@ function buildChatWorkspaceAttachmentHtml(attachment) {
const statusText = String(attachment?.status || 'Queued for workspace processing').trim() || 'Queued for workspace processing';
const detailsId = getChatWorkspaceProgressDetailsId(workspaceDocumentId);
const statusLower = statusText.toLowerCase();
- const progressClass = statusLower.includes('error') || statusLower.includes('failed')
+ const progressClass = window.DocumentStatus.isFailure(statusLower)
? 'bg-danger'
- : (pct >= 100 || statusLower.includes('complete') ? 'bg-success' : 'progress-bar-striped progress-bar-animated bg-info');
+ : (pct >= 100 || window.DocumentStatus.isComplete(statusLower) ? 'bg-success' : 'progress-bar-striped progress-bar-animated bg-info');
return `
= 100 || statusLower.includes('complete'));
+ const hasFailure = window.DocumentStatus.isFailure(statusLower);
+ const hasCompleted = !hasFailure && (pct >= 100 || window.DocumentStatus.isComplete(statusLower));
const progressBar = container.querySelector('.progress-bar');
const statusElement = container.querySelector('.chat-workspace-upload-status-text');
const progressElement = container.querySelector('.progress');
diff --git a/application/single_app/static/js/document-status.js b/application/single_app/static/js/document-status.js
new file mode 100644
index 000000000..a1655ee28
--- /dev/null
+++ b/application/single_app/static/js/document-status.js
@@ -0,0 +1,16 @@
+// document-status.js
+// Kept in parity with Python and React through document_processing_status_cases.json.
+
+window.DocumentStatus = (() => {
+ const failure = /^(?:(?:error|failed)(?:$|[\s:!]|\.(?:$|\s))|(?:processing|document processing|approval|content screening) failed(?:$|[\s:;!]|\.(?:$|\s))|video:\s*(?:blob upload|auth|upload|indexing) failed(?:$|[\s:!]|\.(?:$|\s)))/i;
+ const complete = /^(?:(?:ready|complete|completed|processed|finished)[.!]?$|processing complete(?:$|[.!]$|\s+[-(]))/i;
+
+ return Object.freeze({
+ isFailure(status) {
+ return typeof status === "string" && failure.test(status.trim());
+ },
+ isComplete(status) {
+ return typeof status === "string" && complete.test(status.trim());
+ },
+ });
+})();
diff --git a/application/single_app/static/js/public/public_workspace.js b/application/single_app/static/js/public/public_workspace.js
index 9c1c2d6c3..4d39a66b0 100644
--- a/application/single_app/static/js/public/public_workspace.js
+++ b/application/single_app/static/js/public/public_workspace.js
@@ -1071,9 +1071,8 @@ function getPublicDocumentProcessingState(doc) {
const pctString = String((doc.percentage_complete ?? doc.percentage) || '0');
const pct = /^\d+(\.\d+)?$/.test(pctString) ? parseFloat(pctString) : 0;
const docStatus = doc.status || '';
- const normalizedStatus = docStatus.toLowerCase();
- const hasError = normalizedStatus.includes('error') || normalizedStatus.includes('failed');
- const isComplete = pct >= 100 || normalizedStatus.includes('complete') || hasError;
+ const hasError = window.DocumentStatus.isFailure(docStatus);
+ const isComplete = pct >= 100 || window.DocumentStatus.isComplete(docStatus) || hasError;
return { pct, docStatus, hasError, isComplete };
}
@@ -1667,8 +1666,8 @@ function renderPublicDocumentRow(doc) {
const pctString = String((doc.percentage_complete ?? doc.percentage) || "0");
const pct = /^\d+(\.\d+)?$/.test(pctString) ? parseFloat(pctString) : 0;
const docStatus = doc.status || "";
- const isComplete = pct >= 100 || docStatus.toLowerCase().includes("complete") || docStatus.toLowerCase().includes("error");
- const hasError = docStatus.toLowerCase().includes("error") || docStatus.toLowerCase().includes("failed");
+ const hasError = window.DocumentStatus.isFailure(docStatus);
+ const isComplete = pct >= 100 || window.DocumentStatus.isComplete(docStatus) || hasError;
const isPendingGeneratedArtifact = isPendingGeneratedArtifactDocument(doc);
let firstTdHtml = "";
@@ -1896,8 +1895,8 @@ function pollPublicDocumentStatus(documentId) {
const pctString = String((doc.percentage_complete ?? doc.percentage) || "0");
const pct = /^\d+(\.\d+)?$/.test(pctString) ? parseFloat(pctString) : 0;
const docStatus = doc.status || "";
- const isComplete = pct >= 100 || docStatus.toLowerCase().includes("complete") || docStatus.toLowerCase().includes("error");
- const hasError = docStatus.toLowerCase().includes("error") || docStatus.toLowerCase().includes("failed");
+ const hasError = window.DocumentStatus.isFailure(docStatus);
+ const isComplete = pct >= 100 || window.DocumentStatus.isComplete(docStatus) || hasError;
if (!isComplete && statusRow) {
// Update progress bar and status text if still processing
@@ -3374,8 +3373,8 @@ function buildPublicFolderDocumentsTable(docs) {
const pctString = String((doc.percentage_complete ?? doc.percentage) || '0');
const pct = /^\d+(\.\d+)?$/.test(pctString) ? parseFloat(pctString) : 0;
const docStatus = doc.status || '';
- const isComplete = pct >= 100 || docStatus.toLowerCase().includes('complete') || docStatus.toLowerCase().includes('error');
- const hasError = docStatus.toLowerCase().includes('error') || docStatus.toLowerCase().includes('failed');
+ const hasError = window.DocumentStatus.isFailure(docStatus);
+ const isComplete = pct >= 100 || window.DocumentStatus.isComplete(docStatus) || hasError;
const isSelected = publicSelectedDocuments.has(doc.id);
let firstColHtml = ``;
let actionsHtml = '';
diff --git a/application/single_app/static/js/workspace/workspace-documents.js b/application/single_app/static/js/workspace/workspace-documents.js
index 5abb199e9..54d8ff7c8 100644
--- a/application/single_app/static/js/workspace/workspace-documents.js
+++ b/application/single_app/static/js/workspace/workspace-documents.js
@@ -318,9 +318,8 @@ function getDocumentProcessingState(doc) {
const pctString = String(doc?.percentage_complete ?? "");
const pct = /^\d+(\.\d+)?$/.test(pctString) ? parseFloat(pctString) : 0;
const docStatus = doc?.status || "";
- const normalizedStatus = docStatus.toLowerCase();
- const hasError = normalizedStatus.includes("error");
- const isComplete = pct >= 100 || normalizedStatus.includes("complete") || hasError;
+ const hasError = window.DocumentStatus.isFailure(docStatus);
+ const isComplete = pct >= 100 || window.DocumentStatus.isComplete(docStatus) || hasError;
return { pct, docStatus, hasError, isComplete };
}
@@ -1789,8 +1788,8 @@ function renderDocumentRow(doc) {
const pctString = String(doc.percentage_complete);
const pct = /^\d+(\.\d+)?$/.test(pctString) ? parseFloat(pctString) : 0;
const docStatus = doc.status || "";
- const isComplete = pct >= 100 || docStatus.toLowerCase().includes("complete") || docStatus.toLowerCase().includes("error");
- const hasError = docStatus.toLowerCase().includes("error");
+ const hasError = window.DocumentStatus.isFailure(docStatus);
+ const isComplete = pct >= 100 || window.DocumentStatus.isComplete(docStatus) || hasError;
const docRow = document.createElement("tr");
docRow.id = `doc-row-${docId}`;
@@ -2239,8 +2238,8 @@ function pollDocumentStatus(documentId) {
const pctString = String(doc.percentage_complete);
const pct = /^\d+(\.\d+)?$/.test(pctString) ? parseFloat(pctString) : 0;
const docStatus = doc.status || "";
- const isComplete = pct >= 100 || docStatus.toLowerCase().includes("complete") || docStatus.toLowerCase().includes("error");
- const hasError = docStatus.toLowerCase().includes("error");
+ const hasError = window.DocumentStatus.isFailure(docStatus);
+ const isComplete = pct >= 100 || window.DocumentStatus.isComplete(docStatus) || hasError;
if (!isComplete && statusRow) {
// Update progress bar and status text if still processing
diff --git a/application/single_app/static/js/workspace/workspace-tags.js b/application/single_app/static/js/workspace/workspace-tags.js
index c23d28631..2fa0f8bd2 100644
--- a/application/single_app/static/js/workspace/workspace-tags.js
+++ b/application/single_app/static/js/workspace/workspace-tags.js
@@ -648,10 +648,8 @@ function buildFolderDocumentsTable(docs) {
const pctStr = String(doc.percentage_complete);
const pct = /^\d+(\.\d+)?$/.test(pctStr) ? parseFloat(pctStr) : 0;
const docStatus = doc.status || '';
- const isComplete = pct >= 100
- || docStatus.toLowerCase().includes('complete')
- || docStatus.toLowerCase().includes('error');
- const hasError = docStatus.toLowerCase().includes('error');
+ const hasError = window.DocumentStatus.isFailure(docStatus);
+ const isComplete = pct >= 100 || window.DocumentStatus.isComplete(docStatus) || hasError;
const currentUserId = window.current_user_id;
const isOwner = doc.user_id === currentUserId;
diff --git a/application/single_app/templates/base.html b/application/single_app/templates/base.html
index ce0f8f9f4..d098042be 100644
--- a/application/single_app/templates/base.html
+++ b/application/single_app/templates/base.html
@@ -709,6 +709,7 @@
Workfl
but it's okay to load it afterwards) -->
+
diff --git a/application/single_app/templates/group_workspaces.html b/application/single_app/templates/group_workspaces.html
index 1cc6a98c7..be5810139 100644
--- a/application/single_app/templates/group_workspaces.html
+++ b/application/single_app/templates/group_workspaces.html
@@ -3664,9 +3664,8 @@ Delete Group Workflow
const pctString = String(doc?.percentage_complete ?? "");
const pct = /^\d+(\.\d+)?$/.test(pctString) ? parseFloat(pctString) : 0;
const docStatus = doc?.status || "";
- const normalizedStatus = docStatus.toLowerCase();
- const hasError = normalizedStatus.includes("error");
- const isComplete = pct >= 100 || normalizedStatus.includes("complete") || hasError;
+ const hasError = window.DocumentStatus.isFailure(docStatus);
+ const isComplete = pct >= 100 || window.DocumentStatus.isComplete(docStatus) || hasError;
return { pct, docStatus, hasError, isComplete };
}
@@ -6132,11 +6131,8 @@ Delete Group Document
const pctString = String(doc.percentage_complete);
const pct = /^\d+(\.\d+)?$/.test(pctString) ? parseFloat(pctString) : 0;
const docStatus = doc.status || "";
- const isComplete =
- pct >= 100 ||
- docStatus.toLowerCase().includes("complete") ||
- docStatus.toLowerCase().includes("error");
- const hasError = docStatus.toLowerCase().includes("error");
+ const hasError = window.DocumentStatus.isFailure(docStatus);
+ const isComplete = pct >= 100 || window.DocumentStatus.isComplete(docStatus) || hasError;
const canManage = ["Owner", "Admin", "DocumentManager"].includes(userRole); // Check role for actions
const access = getGroupDocumentAccess(doc);
const isPendingGeneratedArtifact = isPendingGeneratedArtifactDocument(doc);
@@ -6566,11 +6562,8 @@ Delete Group Document
? parseFloat(pctString)
: 0;
const docStatus = doc.status || "";
- const isComplete =
- pct >= 100 ||
- docStatus.toLowerCase().includes("complete") ||
- docStatus.toLowerCase().includes("error");
- const hasError = docStatus.toLowerCase().includes("error");
+ const hasError = window.DocumentStatus.isFailure(docStatus);
+ const isComplete = pct >= 100 || window.DocumentStatus.isComplete(docStatus) || hasError;
if (!isComplete && statusRow) {
// Update progress
@@ -8144,10 +8137,8 @@
const pctStr = String(doc.percentage_complete);
const pct = /^\d+(\.\d+)?$/.test(pctStr) ? parseFloat(pctStr) : 0;
const docStatus = doc.status || '';
- const isComplete = pct >= 100
- || docStatus.toLowerCase().includes('complete')
- || docStatus.toLowerCase().includes('error');
- const hasError = docStatus.toLowerCase().includes('error');
+ const hasError = window.DocumentStatus.isFailure(docStatus);
+ const isComplete = pct >= 100 || window.DocumentStatus.isComplete(docStatus) || hasError;
const isSelected = groupSelectedDocuments.has(docId);
const access = getGroupDocumentAccess(doc);
const canModify = access.isOwnerGroup && groupCanModify;
diff --git a/application/v2_ui/src/lib/chatUploads.ts b/application/v2_ui/src/lib/chatUploads.ts
index 6751119c2..872ebacdb 100644
--- a/application/v2_ui/src/lib/chatUploads.ts
+++ b/application/v2_ui/src/lib/chatUploads.ts
@@ -8,6 +8,7 @@ import {
type ChatUploadTarget,
} from './endpoints';
import { documentStatus } from './documentExplorer';
+import { isDocumentProcessingComplete } from './documentProcessingStatus';
import type { ComposerDraft, ComposerReference, ComposerUpload } from './composerDraft';
import type { ChatMessage, WorkspaceDocument } from './types';
import type { Dispatch, SetStateAction } from 'react';
@@ -203,7 +204,6 @@ export function chatUploadDocumentStatus(document: WorkspaceDocument): UploadSta
if (status.state === 'pending_approval') {
return { state: 'failed', error: 'This file is waiting for access approval.' };
}
- const text = String(document.status ?? '').trim().toLowerCase();
const rawProgress = document.percentage_complete;
const progress = rawProgress === undefined || rawProgress === null
? NaN
@@ -211,7 +211,7 @@ export function chatUploadDocumentStatus(document: WorkspaceDocument): UploadSta
// The explorer treats old records with no progress as ready. A new upload cannot make
// that assumption: a queued response without a percentage is still not usable.
if ((Number.isFinite(progress) && progress >= 100)
- || /^(ready|completed?|processed|finished|processing complete[.!]?)$/.test(text)) {
+ || isDocumentProcessingComplete(document.status)) {
return { state: 'ready', progress: 100 };
}
return {
diff --git a/application/v2_ui/src/lib/documentExplorer.ts b/application/v2_ui/src/lib/documentExplorer.ts
index c1a864815..7d6f9e078 100644
--- a/application/v2_ui/src/lib/documentExplorer.ts
+++ b/application/v2_ui/src/lib/documentExplorer.ts
@@ -18,6 +18,7 @@ import type {
WorkspaceDocument,
} from './types';
import { isScreeningAvailable } from './contentScreening';
+import { isDocumentProcessingFailure } from './documentProcessingStatus';
/* -------------------------------------------------------------------------- */
/* Query state */
@@ -364,13 +365,11 @@ export function generatedArtifactRestriction(document: WorkspaceDocument): strin
* it as permanently stuck at 0% would be worse than saying nothing.
*/
export function documentStatus(document: WorkspaceDocument): DocumentStatus {
- const statusText = String(document.status ?? '').toLowerCase();
-
if (document.generated_artifact_promotion_status === 'approval_failed') {
return { state: 'error', percent: 0, label: 'Publication handoff failed' };
}
- if (statusText.includes('error') || statusText.includes('failed')) {
+ if (isDocumentProcessingFailure(document.status)) {
return { state: 'error', percent: 0, label: 'Error' };
}
diff --git a/application/v2_ui/src/lib/documentProcessingStatus.ts b/application/v2_ui/src/lib/documentProcessingStatus.ts
new file mode 100644
index 000000000..75c4b7300
--- /dev/null
+++ b/application/v2_ui/src/lib/documentProcessingStatus.ts
@@ -0,0 +1,13 @@
+// documentProcessingStatus.ts
+// Kept in parity with Python and classic through document_processing_status_cases.json.
+
+const FAILURE = /^(?:(?:error|failed)(?:$|[\s:!]|\.(?:$|\s))|(?:processing|document processing|approval|content screening) failed(?:$|[\s:;!]|\.(?:$|\s))|video:\s*(?:blob upload|auth|upload|indexing) failed(?:$|[\s:!]|\.(?:$|\s)))/i;
+const COMPLETE = /^(?:(?:ready|complete|completed|processed|finished)[.!]?$|processing complete(?:$|[.!]$|\s+[-(]))/i;
+
+export function isDocumentProcessingFailure(status: unknown): boolean {
+ return typeof status === 'string' && FAILURE.test(status.trim());
+}
+
+export function isDocumentProcessingComplete(status: unknown): boolean {
+ return typeof status === 'string' && COMPLETE.test(status.trim());
+}
diff --git a/docs/explanation/fixes/DOCUMENT_STATUS_TEXT_CLASSIFICATION_FIX.md b/docs/explanation/fixes/DOCUMENT_STATUS_TEXT_CLASSIFICATION_FIX.md
new file mode 100644
index 000000000..e1ffe90a0
--- /dev/null
+++ b/docs/explanation/fixes/DOCUMENT_STATUS_TEXT_CLASSIFICATION_FIX.md
@@ -0,0 +1,119 @@
+# Document Status Text Classification Fix
+
+Version: **0.261.354**
+
+Fixed/Implemented in version: **0.261.354**
+
+Related configuration update: `application/single_app/config.py`, `VERSION = "0.261.354"`.
+
+The base branch advanced to **0.261.353** during review. The combined application
+uses **0.261.354**, preserving the incoming saved-memory and SQL diagnostics fixes
+alongside this status classification fix.
+
+## Issue and root cause
+
+A chat attachment could show **Failed** even though the document processor
+successfully saved and indexed it. Document status messages include filenames,
+sheet names, and processing details. Consumers searched the entire message for
+`error`, `failed`, or `complete`, interpreting those words as lifecycle state.
+
+For example, `Processing file error-budget.md, type: .md` was treated as a
+terminal error. React stopped checking the document immediately, so it never
+observed the successful completion. Classic views could make the opposite
+mistake: a filename such as `incomplete-policy.pdf` enabled completed-document
+actions before processing finished. A filename containing `processing complete`
+could also force backend progress to 100%.
+
+The incident's Azure console logs confirmed successful indexing of one chunk
+after the UI had stopped polling. It failed before plan review or execution;
+the incident did not demonstrate an orchestration or model-provider failure.
+
+## Technical changes
+
+Small, bootstrap-independent predicates now recognize explicit lifecycle
+phrases rather than arbitrary substrings. Python uses
+`functions_document_status.py`; React uses `documentProcessingStatus.ts`; classic
+pages load the local `static/js/document-status.js` asset from `base.html`.
+A shared test-case matrix keeps the three implementations aligned.
+
+Consumers updated:
+
+- Workspace processing/error filters and facets in `functions_document_queries.py`.
+- Percentage calculation, guarded safe-error updates, and screening-related
+ completion wording in `functions_documents.py`.
+- Chat-upload search-readiness checks in `route_backend_chats.py`.
+- React document status and attachment polling in `documentExplorer.ts` and
+ `chatUploads.ts`.
+- Classic chat attachment cards and task-document readiness, personal workspace
+ lists/cards/folders, group document views, and public workspace views.
+
+An explicit failure such as `Error: ...` still takes precedence over 100%
+progress. Metadata warnings and known transient retry messages do not become
+terminal failures. Completion of an intermediate stage, such as vision analysis,
+is not completion of document ingestion.
+
+This changes interpretation, not ingestion or authorization. Publication
+approval, screening holds, sharing restrictions, archived/unlinked documents,
+and indexed-chunk requirements retain their separate checks. Legacy explorer
+records without progress keep their existing behavior; new chat uploads still
+require readiness evidence.
+
+No database migration, filename restriction, timeout increase, automatic retry,
+live upload, deployment, or external harness change is part of this fix.
+
+## Usage and impact
+
+Upload documents normally. Filenames can contain state-like words without
+changing their processing outcome. Processing attachments remain unavailable
+for submission until they are ready, and genuine failures remain visible.
+
+For an already processed attachment stuck in the old UI's Failed state, the
+existing **Retry** status-check action can retrieve the document's authoritative
+state after the fix is deployed. It does not need another file upload.
+Historical stored percentages are not automatically rewritten.
+
+## Validation
+
+Regression coverage includes:
+
+- `functional_tests/test_document_processing_status.py`: shared Python/React/
+ classic predicate cases, backend progress, safe failures, facets and readiness.
+- `functional_tests/test_v2_chat_context_request.ts` and
+ `test_v2_documents_explorer_logic.ts`: actual frontend state and polling.
+- `functional_tests/test_document_search_metadata_sync.py`: real status updates
+ with SDK `CosmosDict` point responses, plain query rows, and ETag protection.
+- `ui_tests/test_v2_chat_upload_status.py`: personal/group uploads with misleading
+ filenames, retry diagnostics, real failures, status-only Retry and removal.
+- `ui_tests/test_document_status_classification.py`: group list/card/folder
+ behavior and personal/public/classic status decisions.
+- `ui_tests/test_chat_workspace_upload_progress_polling.py`: classic progress
+ rendering, status polling and task-document registration.
+
+These tests use local application assets and intercepted external boundaries;
+they do not upload to the deployed application or exercise a live plan.
+
+Local results: the affected browser suites passed **101 tests**, the SDK-backed
+metadata/status suite passed **42 tests**, and the targeted status/explorer/
+screening-readiness suite passed **96 tests**. Frontend typechecking, the
+production build, JavaScript syntax checks, route-policy checks and documentation
+checks passed. With the application's pinned Cosmos SDK **4.9.0**, the full
+network-blocked Cosmos compatibility gate passed **1,163 tests** and **244
+subtests**, with no inventory review gaps.
+
+After merging the **0.261.350** base, combined status, SDK-write, explorer and
+incoming saved-memory regressions passed **194 tests** and **5 subtests**.
+The focused upload/classic browser suites passed **27 tests**, and documentation
+checks and the combined Cosmos inventory remained clean.
+
+After the subsequent **0.261.353** base integration, combined status, saved-memory,
+SQL diagnostics and plugin-route SDK regressions passed **308 tests** and **5
+subtests**. The same **27 browser tests**, documentation checks and combined Cosmos
+inventory passed.
+
+A broader backend run encountered a pre-existing extracted-function fixture
+failure in `test_content_screening_read_boundaries.py`: its `hybrid_search`
+namespace omits `current_public_chat_scope`. The same failure reproduces using
+the original test at HEAD. That unrelated test-infrastructure repair is not
+included here; the directly affected chat-readiness check passes.
+
+Related: [Chat upload conversation task context](CHAT_UPLOAD_CONVERSATION_TASK_CONTEXT_FIX.md).
diff --git a/functional_tests/cosmos_response_boundaries.json b/functional_tests/cosmos_response_boundaries.json
index 0dd7d5cde..70ae65a94 100644
--- a/functional_tests/cosmos_response_boundaries.json
+++ b/functional_tests/cosmos_response_boundaries.json
@@ -110,7 +110,6 @@
"functional_tests/test_document_provenance_origin.py::ConversationStore": "630bfc8422d888ce8a1734e838558088c1ce97442c414b68d92ea8f2ea324664",
"functional_tests/test_document_provenance_publication.py::test_an_unreadable_conversation_never_blocks_publication.UnavailableConversations": "2f5064c80b76b576c350ae31e227f600afe4b68141b950fc3b073e3be39bcd91",
"functional_tests/test_document_provenance_versions_and_guards.py::ConversationReads": "6b6e988a4e5a58c97875c61b731a11c92fcd62567f635898c815e6f9bbed1ba1",
- "functional_tests/test_document_search_metadata_sync.py::FakeDocumentsContainer": "81341dcf6916ebe93a2ad55f6a2e90c747d7d8f19d6b7b019399f440d98ba717",
"functional_tests/test_fact_memory_profile_and_mini_sk.py::FakeFactContainer": "38a457b1b2a3236d946664ca1db4353daef1e21c6f36abadfd47178a28ad1393",
"functional_tests/test_feedback_submission_authorization.py::FakeConversationContainer": "314eaff4d7220b4632b4a838633a4c3928350063fd568f04da566aef8c3d3df0",
"functional_tests/test_feedback_submission_authorization.py::FakeFeedbackContainer": "8280ace4523971c41e58ee4481d244324bc42c7c264451e0fe953a838b0d2dcd",
@@ -6820,7 +6819,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "_upsert_document_and_sync_access_index: point documents use Mapping field reads/JSON encoding/metadata projections; batch status/body lists are separate, never top-level-normalized as documents. Strict domain sinks remain with their storage owners. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -6842,7 +6841,7 @@
}
],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "_get_document_family_items_from_document: query rows/pages are plain JSON, not Cosmos point wrappers. Nested domain shape checks therefore need no point-response repair. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -6864,7 +6863,7 @@
}
],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "create_document: query rows/pages are plain JSON, not Cosmos point wrappers. Nested domain shape checks therefore need no point-response repair. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -6886,7 +6885,7 @@
}
],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "get_document_metadata: query rows/pages are plain JSON, not Cosmos point wrappers. Nested domain shape checks therefore need no point-response repair. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -6898,7 +6897,7 @@
},
"application/single_app/functions_documents.py::update_document": {
"boundary": {
- "sha256": "044b9f8633e1c5f242d861baad67d86126f2ae8592d96b3c49465ae3a46718fd",
+ "sha256": "04e6e05e2932a55f3c08f69e30d0d1b6636724785e8fdcc4f232b87a81211484",
"points": [],
"queries": [
{
@@ -6908,14 +6907,17 @@
}
],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
- "contract": "update_document: query rows/pages are plain JSON, not Cosmos point wrappers. Nested domain shape checks therefore need no point-response repair. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
+ "contract": "Workspace-scoped query rows are plain dictionaries. Status predicates only interpret strings; filenames cannot force success/failure or rewrite screening copy. Strict update returns the SDK saved mapping to mapping-compatible callers; read/write ETags, operation guards, access projection and screening holds are preserved. Runtime evidence exercises personal/group/public status writes with installed-SDK point responses and plain query rows.",
"evidence_scope": "owner-runtime",
"tests": [
"functional_tests/test_workspace_metadata_outcomes.py::test_upload_dispatcher_preserves_ready_content_after_metadata_failure",
- "functional_tests/test_content_screening_upload_only_intake.py::test_model_generated_metadata_applies_without_a_hold"
+ "functional_tests/test_content_screening_upload_only_intake.py::test_model_generated_metadata_applies_without_a_hold",
+ "functional_tests/test_document_search_metadata_sync.py::test_status_updates_preserve_sdk_points_etags_and_query_rows",
+ "functional_tests/test_document_search_metadata_sync.py::test_held_status_updates_distinguish_processing_details_from_final_completion",
+ "functional_tests/test_document_search_metadata_sync.py::test_status_update_cannot_write_after_its_operation_guard_expires"
]
},
"application/single_app/functions_documents.py::get_document_metadata_for_citations": {
@@ -6930,7 +6932,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "get_document_metadata_for_citations: point documents use Mapping field reads/JSON encoding/metadata projections; batch status/body lists are separate, never top-level-normalized as documents. Strict domain sinks remain with their storage owners. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -6952,7 +6954,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "_rebase_document_after_fenced_acl_projection: point documents use Mapping field reads/JSON encoding/metadata projections; batch status/body lists are separate, never top-level-normalized as documents. Strict domain sinks remain with their storage owners. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -6974,7 +6976,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "_read_document_search_sync_record: point documents use Mapping field reads/JSON encoding/metadata projections; batch status/body lists are separate, never top-level-normalized as documents. Strict domain sinks remain with their storage owners. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -6996,7 +6998,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "_replace_document_search_sync_record: point documents use Mapping field reads/JSON encoding/metadata projections; batch status/body lists are separate, never top-level-normalized as documents. Strict domain sinks remain with their storage owners. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -7018,7 +7020,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "request_document_search_metadata_sync: point documents use Mapping field reads/JSON encoding/metadata projections; batch status/body lists are separate, never top-level-normalized as documents. Strict domain sinks remain with their storage owners. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -7040,7 +7042,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "response-unused",
"contract": "_complete_document_search_sync_pass: point response values are discarded; query rows, if present, are separate plain SDK JSON. Existing write errors remain visible.",
@@ -7059,7 +7061,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "response-unused",
"contract": "_delete_document_search_sync_record: point response values are discarded; query rows, if present, are separate plain SDK JSON. Existing write errors remain visible.",
@@ -7078,7 +7080,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "_read_document_for_search_sync: point documents use Mapping field reads/JSON encoding/metadata projections; batch status/body lists are separate, never top-level-normalized as documents. Strict domain sinks remain with their storage owners. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -7100,7 +7102,7 @@
}
],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "process_due_document_search_metadata_syncs: query rows/pages are plain JSON, not Cosmos point wrappers. Nested domain shape checks therefore need no point-response repair. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -7122,7 +7124,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "get_document_record: point documents use Mapping field reads/JSON encoding/metadata projections; batch status/body lists are separate, never top-level-normalized as documents. Strict domain sinks remain with their storage owners. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -7144,7 +7146,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "get_latest_version: point documents use Mapping field reads/JSON encoding/metadata projections; batch status/body lists are separate, never top-level-normalized as documents. Strict domain sinks remain with their storage owners. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -7166,7 +7168,7 @@
}
],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "get_document_version: query rows/pages are plain JSON, not Cosmos point wrappers. Nested domain shape checks therefore need no point-response repair. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -7188,7 +7190,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "delete_from_blob_storage: point documents use Mapping field reads/JSON encoding/metadata projections; batch status/body lists are separate, never top-level-normalized as documents. Strict domain sinks remain with their storage owners. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -7235,7 +7237,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "delete_document: point documents use Mapping field reads/JSON encoding/metadata projections; batch status/body lists are separate, never top-level-normalized as documents. Strict domain sinks remain with their storage owners. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -7262,7 +7264,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "delete_document_revision: point documents use Mapping field reads/JSON encoding/metadata projections; batch status/body lists are separate, never top-level-normalized as documents. Strict domain sinks remain with their storage owners. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -7289,7 +7291,7 @@
}
],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "get_chat_upload_workspace_documents_for_conversation: query rows/pages are plain JSON, not Cosmos point wrappers. Nested domain shape checks therefore need no point-response repair. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -7311,7 +7313,7 @@
}
],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "get_chat_upload_workspace_documents_for_collaboration: query rows/pages are plain JSON, not Cosmos point wrappers. Nested domain shape checks therefore need no point-response repair. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -7333,7 +7335,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "get_document_versions: point documents use Mapping field reads/JSON encoding/metadata projections; batch status/body lists are separate, never top-level-normalized as documents. Strict domain sinks remain with their storage owners. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -7365,7 +7367,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "detect_doc_type: point documents use Mapping field reads/JSON encoding/metadata projections; batch status/body lists are separate, never top-level-normalized as documents. Strict domain sinks remain with their storage owners. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -7387,7 +7389,7 @@
}
],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "extract_document_metadata: query rows/pages are plain JSON, not Cosmos point wrappers. Nested domain shape checks therefore need no point-response repair. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -7409,7 +7411,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "upload_to_blob: point documents use Mapping field reads/JSON encoding/metadata projections; batch status/body lists are separate, never top-level-normalized as documents. Strict domain sinks remain with their storage owners. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -7431,7 +7433,7 @@
}
],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "_query_current_xsd_documents: query rows/pages are plain JSON, not Cosmos point wrappers. Nested domain shape checks therefore need no point-response repair. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -7453,7 +7455,7 @@
}
],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "_workspace_file_name_exists: query rows/pages are plain JSON, not Cosmos point wrappers. Nested domain shape checks therefore need no point-response repair. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -7480,7 +7482,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "sync_chat_upload_workspace_attachment_status: point documents use Mapping field reads/JSON encoding/metadata projections; batch status/body lists are separate, never top-level-normalized as documents. Strict domain sinks remain with their storage owners. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -7502,7 +7504,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "response-unused",
"contract": "queue_personal_workspace_upload_from_temp_file: point response values are discarded; query rows, if present, are separate plain SDK JSON. Existing write errors remain visible.",
@@ -7521,7 +7523,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "response-unused",
"contract": "queue_group_workspace_upload_from_temp_file: point response values are discarded; query rows, if present, are separate plain SDK JSON. Existing write errors remain visible.",
@@ -7545,7 +7547,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "_process_document_upload_background_impl: point documents use Mapping field reads/JSON encoding/metadata projections; batch status/body lists are separate, never top-level-normalized as documents. Strict domain sinks remain with their storage owners. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -7567,7 +7569,7 @@
}
],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "upgrade_legacy_documents: query rows/pages are plain JSON, not Cosmos point wrappers. Nested domain shape checks therefore need no point-response repair. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -7589,7 +7591,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "share_document_with_user: point documents use Mapping field reads/JSON encoding/metadata projections; batch status/body lists are separate, never top-level-normalized as documents. Strict domain sinks remain with their storage owners. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -7611,7 +7613,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "unshare_document_from_user: point documents use Mapping field reads/JSON encoding/metadata projections; batch status/body lists are separate, never top-level-normalized as documents. Strict domain sinks remain with their storage owners. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -7633,7 +7635,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "get_shared_users_for_document: point documents use Mapping field reads/JSON encoding/metadata projections; batch status/body lists are separate, never top-level-normalized as documents. Strict domain sinks remain with their storage owners. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -7655,7 +7657,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "is_document_shared_with_user: point documents use Mapping field reads/JSON encoding/metadata projections; batch status/body lists are separate, never top-level-normalized as documents. Strict domain sinks remain with their storage owners. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -7677,7 +7679,7 @@
}
],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "get_documents_shared_with_user: query rows/pages are plain JSON, not Cosmos point wrappers. Nested domain shape checks therefore need no point-response repair. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -7699,7 +7701,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "share_document_with_group: point documents use Mapping field reads/JSON encoding/metadata projections; batch status/body lists are separate, never top-level-normalized as documents. Strict domain sinks remain with their storage owners. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -7721,7 +7723,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "unshare_document_from_group: point documents use Mapping field reads/JSON encoding/metadata projections; batch status/body lists are separate, never top-level-normalized as documents. Strict domain sinks remain with their storage owners. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -7743,7 +7745,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "get_shared_groups_for_document: point documents use Mapping field reads/JSON encoding/metadata projections; batch status/body lists are separate, never top-level-normalized as documents. Strict domain sinks remain with their storage owners. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -7765,7 +7767,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "is_document_shared_with_group: point documents use Mapping field reads/JSON encoding/metadata projections; batch status/body lists are separate, never top-level-normalized as documents. Strict domain sinks remain with their storage owners. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -7787,7 +7789,7 @@
}
],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "get_documents_shared_with_group: query rows/pages are plain JSON, not Cosmos point wrappers. Nested domain shape checks therefore need no point-response repair. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -7809,7 +7811,7 @@
}
],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "get_workspace_tags: query rows/pages are plain JSON, not Cosmos point wrappers. Nested domain shape checks therefore need no point-response repair. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -7831,7 +7833,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "f3d00ddb30614f061b946ba0854058923903ac6c3e783434bd8ab9a45b9d881a"
+ "module_sha256": "28375fdc378755ea3fa75cbc6b94d3544c3bfdc07b6b5cafc7ab87aac0b7cc46"
},
"classification": "sdk-compatible",
"contract": "propagate_tags_to_blob_metadata: point documents use Mapping field reads/JSON encoding/metadata projections; batch status/body lists are separate, never top-level-normalized as documents. Strict domain sinks remain with their storage owners. Workspace outcome review: existing mapping/query consumers and conditional write contracts are retained; representative module runtime behavior is covered with installed-SDK point responses, including ETags and scope propagation.",
@@ -19237,7 +19239,7 @@
}
],
"batches": [],
- "module_sha256": "67e689829bc9df650c597af9e5277e27542fb6e270fa56f6ed8b81b763f51196"
+ "module_sha256": "a578b7745aa421eca63941beab2683c84f7385aba442f6947c77cb11ade96889"
},
"classification": "response-unused",
"contract": "Plain query rows provide chunk IDs; all chunk/artifact delete responses are discarded and rollback failure accounting remains explicit.",
@@ -19258,7 +19260,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "67e689829bc9df650c597af9e5277e27542fb6e270fa56f6ed8b81b763f51196"
+ "module_sha256": "a578b7745aa421eca63941beab2683c84f7385aba442f6947c77cb11ade96889"
},
"classification": "sdk-compatible",
"contract": "The user-message point mapping supplies nested plain metadata/thread fields for paired image attribution; only user/thread values escape the boundary.",
@@ -19280,7 +19282,7 @@
}
],
"batches": [],
- "module_sha256": "67e689829bc9df650c597af9e5277e27542fb6e270fa56f6ed8b81b763f51196"
+ "module_sha256": "a578b7745aa421eca63941beab2683c84f7385aba442f6947c77cb11ade96889"
},
"classification": "sdk-compatible",
"contract": "Partitioned assistant query rows remain plain dictionaries through soft-delete filtering and saved-analysis history sanitization.",
@@ -19302,7 +19304,7 @@
}
],
"batches": [],
- "module_sha256": "67e689829bc9df650c597af9e5277e27542fb6e270fa56f6ed8b81b763f51196"
+ "module_sha256": "a578b7745aa421eca63941beab2683c84f7385aba442f6947c77cb11ade96889"
},
"classification": "sdk-compatible",
"contract": "Artifact and chunk discovery returns plain query rows to build_message_artifact_payload_map; it is not a point-response normalization boundary.",
@@ -19324,7 +19326,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "67e689829bc9df650c597af9e5277e27542fb6e270fa56f6ed8b81b763f51196"
+ "module_sha256": "a578b7745aa421eca63941beab2683c84f7385aba442f6947c77cb11ade96889"
},
"classification": "response-unused",
"contract": "The local Cosmos delete response is discarded on source-screening rollback; its IfNotModified ETag comes from the persistence owner result. Assistant point normalization remains with persist_chat_reply rather than weakening screening.",
@@ -19345,7 +19347,7 @@
}
],
"batches": [],
- "module_sha256": "67e689829bc9df650c597af9e5277e27542fb6e270fa56f6ed8b81b763f51196"
+ "module_sha256": "a578b7745aa421eca63941beab2683c84f7385aba442f6947c77cb11ade96889"
},
"classification": "sdk-compatible",
"contract": "Workspace lookup consumes plain query rows, rechecks document availability and projects only id/name/title/workspace fields into a plain selection DTO.",
@@ -19372,7 +19374,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "67e689829bc9df650c597af9e5277e27542fb6e270fa56f6ed8b81b763f51196"
+ "module_sha256": "a578b7745aa421eca63941beab2683c84f7385aba442f6947c77cb11ade96889"
},
"classification": "response-unused",
"contract": "Both conversation upsert responses are discarded; the constructed plain conversation is logged and cache-invalidated, with existing persistence failures unchanged.",
@@ -19393,7 +19395,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "67e689829bc9df650c597af9e5277e27542fb6e270fa56f6ed8b81b763f51196"
+ "module_sha256": "a578b7745aa421eca63941beab2683c84f7385aba442f6947c77cb11ade96889"
},
"classification": "sdk-compatible",
"contract": "The conversation point mapping feeds shared-backing and participation authorization using Mapping fields; no raw document reaches an exact-dictionary domain validator or bypasses access checks.",
@@ -19420,7 +19422,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "67e689829bc9df650c597af9e5277e27542fb6e270fa56f6ed8b81b763f51196"
+ "module_sha256": "a578b7745aa421eca63941beab2683c84f7385aba442f6947c77cb11ade96889"
},
"classification": "response-unused",
"contract": "Artifact upsert and cancellation-rollback delete responses are discarded; artifact identities are taken from constructed plain documents and cancellation remains visible.",
@@ -19441,7 +19443,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "67e689829bc9df650c597af9e5277e27542fb6e270fa56f6ed8b81b763f51196"
+ "module_sha256": "a578b7745aa421eca63941beab2683c84f7385aba442f6947c77cb11ade96889"
},
"classification": "sdk-compatible",
"contract": "The user-message point mapping is read through get() into a plain response-context dictionary; nested thread/user metadata stays JSON and read failures retain existing fallback fields.",
@@ -19473,7 +19475,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "67e689829bc9df650c597af9e5277e27542fb6e270fa56f6ed8b81b763f51196"
+ "module_sha256": "a578b7745aa421eca63941beab2683c84f7385aba442f6947c77cb11ade96889"
},
"classification": "response-unused",
"contract": "Safety/user-message upsert and conversation replace responses are discarded; the browser payload is constructed from plain screened documents and explicit public fields.",
@@ -19494,7 +19496,7 @@
}
],
"batches": [],
- "module_sha256": "67e689829bc9df650c597af9e5277e27542fb6e270fa56f6ed8b81b763f51196"
+ "module_sha256": "a578b7745aa421eca63941beab2683c84f7385aba442f6947c77cb11ade96889"
},
"classification": "sdk-compatible",
"contract": "Workspace document discovery consumes plain query rows and builds bounded related-document metadata; no Cosmos point wrapper enters its document catalog.",
@@ -19516,7 +19518,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "67e689829bc9df650c597af9e5277e27542fb6e270fa56f6ed8b81b763f51196"
+ "module_sha256": "a578b7745aa421eca63941beab2683c84f7385aba442f6947c77cb11ade96889"
},
"classification": "sdk-compatible",
"contract": "A new-message upsert may return the SDK mapping to Mapping-compatible chat persistence callers; retry metadata is delegated to patch_chat_message_metadata, which owns conditional update/normalization. No strict top-level DTO is synthesized from arbitrary iterables.",
@@ -19538,7 +19540,7 @@
}
],
"batches": [],
- "module_sha256": "67e689829bc9df650c597af9e5277e27542fb6e270fa56f6ed8b81b763f51196"
+ "module_sha256": "a578b7745aa421eca63941beab2683c84f7385aba442f6947c77cb11ade96889"
},
"classification": "sdk-compatible",
"contract": "Chat attachment resolution consumes plain partitioned query rows and emits display-name/context projections; workspace ownership remains with the document resolver.",
@@ -19560,7 +19562,7 @@
}
],
"batches": [],
- "module_sha256": "67e689829bc9df650c597af9e5277e27542fb6e270fa56f6ed8b81b763f51196"
+ "module_sha256": "a578b7745aa421eca63941beab2683c84f7385aba442f6947c77cb11ade96889"
},
"classification": "sdk-compatible",
"contract": "The TOP 1 thread projection is a plain query row; only thread_id is returned, with the existing read-failure fallback unchanged.",
@@ -19587,7 +19589,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "67e689829bc9df650c597af9e5277e27542fb6e270fa56f6ed8b81b763f51196"
+ "module_sha256": "a578b7745aa421eca63941beab2683c84f7385aba442f6947c77cb11ade96889"
},
"classification": "sdk-compatible",
"contract": "Existing point mappings are checked for owner/deletion state; newly created SDK conversation mappings feed field-based activity logging and cache invalidation, not exact-dictionary result validation.",
@@ -19640,7 +19642,7 @@
}
],
"batches": [],
- "module_sha256": "67e689829bc9df650c597af9e5277e27542fb6e270fa56f6ed8b81b763f51196"
+ "module_sha256": "a578b7745aa421eca63941beab2683c84f7385aba442f6947c77cb11ade96889"
},
"classification": "sdk-compatible",
"contract": "Retry input point mappings supply authorized role/content/thread fields; history queries are plain, and user/system writes and rollback deletes discard responses. Saved-analysis strict result admission remains with its storage owner.",
@@ -19682,7 +19684,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "67e689829bc9df650c597af9e5277e27542fb6e270fa56f6ed8b81b763f51196"
+ "module_sha256": "a578b7745aa421eca63941beab2683c84f7385aba442f6947c77cb11ade96889"
},
"classification": "sdk-compatible",
"contract": "The original Analyze point mapping is checked for role/conversation/content and nested thread metadata; local message writes and cleanup deletes discard responses, while retained analysis contracts remain strict.",
@@ -19709,7 +19711,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "67e689829bc9df650c597af9e5277e27542fb6e270fa56f6ed8b81b763f51196"
+ "module_sha256": "a578b7745aa421eca63941beab2683c84f7385aba442f6947c77cb11ade96889"
},
"classification": "sdk-compatible",
"contract": "The source assistant SDK mapping is authorized by conversation/role and used by Mapping-compatible planned-image lookup; conversation replacement discards its response and public image data is projected explicitly.",
@@ -19731,7 +19733,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "67e689829bc9df650c597af9e5277e27542fb6e270fa56f6ed8b81b763f51196"
+ "module_sha256": "a578b7745aa421eca63941beab2683c84f7385aba442f6947c77cb11ade96889"
},
"classification": "sdk-compatible",
"contract": "The partitioned point mapping is returned to find_planned_proposal_image, which inspects message fields; a missing message returns None, without an empty-document or strict-type fallback.",
@@ -19753,7 +19755,7 @@
}
],
"batches": [],
- "module_sha256": "67e689829bc9df650c597af9e5277e27542fb6e270fa56f6ed8b81b763f51196"
+ "module_sha256": "a578b7745aa421eca63941beab2683c84f7385aba442f6947c77cb11ade96889"
},
"classification": "sdk-compatible",
"contract": "Proposal status iterates plain query rows, validates nested proposal dictionaries and emits explicit public image status fields.",
@@ -19846,7 +19848,7 @@
}
],
"batches": [],
- "module_sha256": "67e689829bc9df650c597af9e5277e27542fb6e270fa56f6ed8b81b763f51196"
+ "module_sha256": "a578b7745aa421eca63941beab2683c84f7385aba442f6947c77cb11ade96889"
},
"classification": "sdk-compatible",
"contract": "Retry/source point mappings supply role/content and nested model/thread metadata through Mapping access; history queries stay plain. Metadata persistence remains with its existing owner and local write responses are discarded. Action recovery now pauses before user-message persistence and model initialization; the original mapping-compatible Cosmos consumers and conditional persistence stay unchanged.",
@@ -19920,7 +19922,7 @@
}
],
"batches": [],
- "module_sha256": "67e689829bc9df650c597af9e5277e27542fb6e270fa56f6ed8b81b763f51196"
+ "module_sha256": "a578b7745aa421eca63941beab2683c84f7385aba442f6947c77cb11ade96889"
},
"classification": "sdk-compatible",
"contract": "Retry point mappings are checked for role/conversation and nested metadata through Mapping access; history queries remain plain, and local create/replace/delete return values are discarded. SSE telemetry does not alter storage contracts. Action recovery now pauses before user-message persistence and model initialization; the original mapping-compatible Cosmos consumers and conditional persistence stay unchanged.",
@@ -19943,7 +19945,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "67e689829bc9df650c597af9e5277e27542fb6e270fa56f6ed8b81b763f51196"
+ "module_sha256": "a578b7745aa421eca63941beab2683c84f7385aba442f6947c77cb11ade96889"
},
"classification": "response-unused",
"contract": "Cancellation refreshes the conversation with a discarded replace response; screened message persistence remains with its existing owner and the stream payload is assembled separately.",
@@ -19975,7 +19977,7 @@
}
],
"batches": [],
- "module_sha256": "67e689829bc9df650c597af9e5277e27542fb6e270fa56f6ed8b81b763f51196"
+ "module_sha256": "a578b7745aa421eca63941beab2683c84f7385aba442f6947c77cb11ade96889"
},
"classification": "sdk-compatible",
"contract": "Point message/conversation mappings and plain fallback query rows use field-based ownership and soft-delete checks; nested mask metadata goes to existing persistence, and public JSON projects mask fields only.",
@@ -19997,7 +19999,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "67e689829bc9df650c597af9e5277e27542fb6e270fa56f6ed8b81b763f51196"
+ "module_sha256": "a578b7745aa421eca63941beab2683c84f7385aba442f6947c77cb11ade96889"
},
"classification": "sdk-compatible",
"contract": "The point message mapping is checked for conversation identity and passed to Mapping-compatible visual-style/persistence owners; public JSON projects nested visual_styles rather than the SDK document.",
@@ -20019,7 +20021,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "67e689829bc9df650c597af9e5277e27542fb6e270fa56f6ed8b81b763f51196"
+ "module_sha256": "a578b7745aa421eca63941beab2683c84f7385aba442f6947c77cb11ade96889"
},
"classification": "sdk-compatible",
"contract": "The point message mapping is checked against the authorized conversation and passed to Mapping-compatible revision readers/persistence; missing messages retain their 404 contract.",
@@ -20041,7 +20043,7 @@
}
],
"batches": [],
- "module_sha256": "67e689829bc9df650c597af9e5277e27542fb6e270fa56f6ed8b81b763f51196"
+ "module_sha256": "a578b7745aa421eca63941beab2683c84f7385aba442f6947c77cb11ade96889"
},
"classification": "sdk-compatible",
"contract": "The partitioned prior-user query returns plain rows; only the first row content string is used as the originating request.",
@@ -20063,7 +20065,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "67e689829bc9df650c597af9e5277e27542fb6e270fa56f6ed8b81b763f51196"
+ "module_sha256": "a578b7745aa421eca63941beab2683c84f7385aba442f6947c77cb11ade96889"
},
"classification": "sdk-compatible",
"contract": "A reread SDK message is checked for conversation membership and inspected by Mapping-compatible block revision helpers; conflict/replay JSON consists of explicit revision fields, not the SDK wrapper.",
@@ -20085,7 +20087,7 @@
],
"queries": [],
"batches": [],
- "module_sha256": "67e689829bc9df650c597af9e5277e27542fb6e270fa56f6ed8b81b763f51196"
+ "module_sha256": "a578b7745aa421eca63941beab2683c84f7385aba442f6947c77cb11ade96889"
},
"classification": "sdk-compatible",
"contract": "The reload callback returns a partitioned SDK message mapping to the Mapping-compatible image revision owner, which rechecks concurrent versions; the route emits only explicit revision/error fields.",
@@ -28086,7 +28088,7 @@
},
"functional_tests/test_document_search_metadata_sync.py::FakeDocumentsContainer": {
"boundary": {
- "sha256": "328730eded73951ad31aafd5c24bebaad155ee9e6d8b6e59b6146b3b53f9b6d5",
+ "sha256": "cd7e418381a1d0e30689d610106821eba9427898434de404917301b11dd2534b",
"methods": [
"create_item",
"read_item",
@@ -28095,10 +28097,10 @@
],
"kind": "class",
"inherited_bases": [],
- "imports_sha256": "31a2f26eebdbbea8a4271f2b1c1b534d60da811f4cab6f4e65e3063976ccf57b"
+ "imports_sha256": "ab3456cf1a8dd8d10b8162c85e19d9f98f65e72e6942605ef6d6eebccfeaace8"
},
- "classification": "legacy-inline",
- "contract": "Unchanged inline Cosmos-shaped test container outside the migrated shared defaults. Its test-specific response behavior is an explicit staged-migration baseline, not blanket runtime SDK proof.",
+ "classification": "sdk-default",
+ "contract": "read_item and create/replace/upsert responses use the installed SDK CosmosDict by default; query rows and nested JSON stay plain dictionaries. The shared store keeps copied document fields, new ETags, scoped query filtering and 409/404/412 failures. Status runtime tests cover strict SDK-returning writes, stale ETags, screening holds and expired operation guards.",
"tests": [
"functional_tests/test_document_search_metadata_sync.py"
]
@@ -32731,7 +32733,7 @@
"inherited_bases": [
"FakeCosmosContainer"
],
- "imports_sha256": "563b8f7d2ce8d5724ae824ab7c1742bce285b8992a29b947dcfd8b691435a6ae"
+ "imports_sha256": "2208c7db731d04d274c9202f2f79fb8273c80f73bb5a3c2cf36c01ee104a1b0c"
},
"classification": "sdk-default",
"contract": "Projection test container inherits partition routing, ETag checks, query filtering and injected failures from the existing model; read/upsert/replace responses are installed-SDK CosmosDict and query rows remain plain dictionaries.",
diff --git a/functional_tests/document_processing_status_cases.json b/functional_tests/document_processing_status_cases.json
new file mode 100644
index 000000000..d68bd7496
--- /dev/null
+++ b/functional_tests/document_processing_status_cases.json
@@ -0,0 +1,52 @@
+[
+ {"status": null, "failure": false, "complete": false},
+ {"status": {}, "failure": false, "complete": false},
+ {"status": "", "failure": false, "complete": false},
+ {"status": "Queued for processing", "failure": false, "complete": false},
+ {"status": "Processing file FED-failed-harness-followup-20261019-a-FED-17-policy-v1.md, type: .md", "failure": false, "complete": false},
+ {"status": "Processing file error-budget.txt, type: .txt", "failure": false, "complete": false},
+ {"status": "Processing file processing complete.md, type: .md", "failure": false, "complete": false},
+ {"status": "Processing incomplete-policy.pdf in 1 file chunk(s)", "failure": false, "complete": false},
+ {"status": "Sending completed-checklist.pdf to Document Intelligence...", "failure": false, "complete": false},
+ {"status": "Saving page/chunk 1/3 of failed-complete.pdf...", "failure": false, "complete": false},
+ {"status": "Processing sheet 'Error: Processing complete'...", "failure": false, "complete": false},
+ {"status": "Uploading failed.md to Blob Storage...", "failure": false, "complete": false},
+ {"status": "Enhanced citations: video at group/error-complete.mp4", "failure": false, "complete": false},
+ {"status": "Retrying Markdown processing after a transient parser concurrency error (1/3)...", "failure": false, "complete": false},
+ {"status": "Retrying compact schema summary for failed-complete.csv...", "failure": false, "complete": false},
+ {"status": "Vision analysis completed (no results)", "failure": false, "complete": false},
+ {"status": "AI vision analysis completed", "failure": false, "complete": false},
+ {"status": "Final metadata extracted", "failure": false, "complete": false},
+ {"status": "Content screening incomplete; retry is required", "failure": false, "complete": false},
+ {"status": "Content review required", "failure": false, "complete": false},
+ {"status": "Pending approval", "failure": false, "complete": false},
+ {"status": "error-budget.md", "failure": false, "complete": false},
+ {"status": "failed.md", "failure": false, "complete": false},
+ {"status": "incomplete", "failure": false, "complete": false},
+ {"status": "Processing completely different content", "failure": false, "complete": false},
+ {"status": "Error", "failure": true, "complete": false},
+ {"status": " ERROR: Unable to read file. ", "failure": true, "complete": false},
+ {"status": "Error processing", "failure": true, "complete": false},
+ {"status": "Error changing extraction: provider unavailable", "failure": true, "complete": false},
+ {"status": "Failed", "failure": true, "complete": false},
+ {"status": "Failed to upload document", "failure": true, "complete": false},
+ {"status": "Processing failed: unavailable", "failure": true, "complete": false},
+ {"status": "Document processing failed. Contact an administrator if the problem persists.", "failure": true, "complete": false},
+ {"status": "Approval failed: unavailable", "failure": true, "complete": false},
+ {"status": "Content screening failed; retry is required", "failure": true, "complete": false},
+ {"status": "VIDEO: blob upload failed : unavailable", "failure": true, "complete": false},
+ {"status": "VIDEO: auth failed : unavailable", "failure": true, "complete": false},
+ {"status": "VIDEO: upload failed : unavailable", "failure": true, "complete": false},
+ {"status": "VIDEO: indexing failed", "failure": true, "complete": false},
+ {"status": "Processing complete", "failure": false, "complete": true},
+ {"status": " processing COMPLETE! ", "failure": false, "complete": true},
+ {"status": "Processing complete.", "failure": false, "complete": true},
+ {"status": "Processing complete - final metadata extracted", "failure": false, "complete": true},
+ {"status": "Processing complete (metadata extraction warning)", "failure": false, "complete": true},
+ {"status": "Processing complete - no content indexed", "failure": false, "complete": true},
+ {"status": "Ready", "failure": false, "complete": true},
+ {"status": "Complete", "failure": false, "complete": true},
+ {"status": "completed", "failure": false, "complete": true},
+ {"status": "Processed", "failure": false, "complete": true},
+ {"status": "Finished.", "failure": false, "complete": true}
+]
diff --git a/functional_tests/test_content_screening_read_boundaries.py b/functional_tests/test_content_screening_read_boundaries.py
index c8fd847b7..bb35d96b0 100644
--- a/functional_tests/test_content_screening_read_boundaries.py
+++ b/functional_tests/test_content_screening_read_boundaries.py
@@ -1,7 +1,7 @@
# test_content_screening_read_boundaries.py
"""
Behavioral tests for search, native caches, and resumed source consumption.
-Version: 0.261.232
+Version: 0.261.354
Implemented in: 0.261.106
Upload-only export checks covered in: 0.261.232
@@ -28,6 +28,7 @@
from test_content_screening_access import APP_ROOT, FakeContainer, ScreeningAccessFixture
from content_screening import access
from content_screening.contracts import DocumentHeldError, ScreeningConflictError, ScreeningError, document_is_available, hash_payload
+from functions_document_status import is_document_processing_complete, is_document_processing_failure
def load_functions(file_name, names, namespace, *, constants=False):
@@ -180,7 +181,11 @@ def test_explicit_full_document_fetch_checks_before_loading_any_chunks(self):
def test_chat_readiness_cannot_infer_clearance_from_progress(self):
namespace = load_functions(
"route_backend_chats.py", {"_is_search_ready_chat_upload_workspace_document"},
- {"document_is_available": document_is_available},
+ {
+ "document_is_available": document_is_available,
+ "is_document_processing_complete": is_document_processing_complete,
+ "is_document_processing_failure": is_document_processing_failure,
+ },
)
self.document.update({"percentage_complete": 100, "status": "Processing complete", "num_chunks": 5})
self.hold()
diff --git a/functional_tests/test_document_processing_status.py b/functional_tests/test_document_processing_status.py
new file mode 100644
index 000000000..308b71974
--- /dev/null
+++ b/functional_tests/test_document_processing_status.py
@@ -0,0 +1,161 @@
+# test_document_processing_status.py
+"""
+Functional regressions for document terminal-state interpretation.
+Version: 0.261.354
+Implemented in: 0.261.354
+
+Execute the real Python, React and classic predicates against shared cases,
+plus backend progress, safe job updates, facets and chat-readiness decisions.
+No settings bootstrap, live storage or provider calls are needed.
+"""
+
+import json
+from pathlib import Path
+import subprocess
+import sys
+
+import pytest
+
+from test_support.app_source import run_definitions
+from test_support.versioning import assert_app_version_at_least
+
+
+ROOT = Path(__file__).resolve().parents[1]
+APP = ROOT / "application" / "single_app"
+UI = ROOT / "application" / "v2_ui"
+sys.path.insert(0, str(APP))
+
+from content_screening.contracts import document_is_available # noqa: E402
+from functions_document_queries import ( # noqa: E402
+ _document_processing_state, build_document_facets, filter_documents_by_place,
+)
+from functions_document_status import ( # noqa: E402
+ is_document_processing_complete, is_document_processing_failure,
+)
+
+
+CASES = json.loads((ROOT / "functional_tests" / "document_processing_status_cases.json").read_text(encoding="utf-8"))
+PREDICATES = {
+ "is_document_processing_complete": is_document_processing_complete,
+ "is_document_processing_failure": is_document_processing_failure,
+}
+
+
+@pytest.mark.parametrize("case", CASES, ids=lambda case: str(case["status"]))
+def test_python_status_contract(case):
+ failure = is_document_processing_failure(case["status"])
+ complete = is_document_processing_complete(case["status"])
+ assert failure is case["failure"]
+ assert complete is case["complete"]
+
+
+def test_browser_predicates_match_python_contract(tmp_path):
+ bundle = tmp_path / "document-status.cjs"
+ subprocess.run([
+ "node", str(UI / "node_modules" / "esbuild" / "bin" / "esbuild"),
+ str(UI / "src" / "lib" / "documentProcessingStatus.ts"),
+ "--bundle", "--platform=node", "--format=cjs", f"--outfile={bundle}", "--log-level=error",
+ ], check=True, capture_output=True, text=True)
+ probe = r"""
+ const fs = require('node:fs');
+ const vm = require('node:vm');
+ const assert = require('node:assert/strict');
+ const react = require(process.argv[1]);
+ const context = vm.createContext({window: {}});
+ vm.runInContext(fs.readFileSync(process.argv[2], 'utf8'), context);
+ const classic = context.window.DocumentStatus;
+ const cases = JSON.parse(fs.readFileSync(process.argv[3], 'utf8'));
+ for (const value of cases) {
+ assert.equal(react.isDocumentProcessingFailure(value.status), value.failure, JSON.stringify(value));
+ assert.equal(react.isDocumentProcessingComplete(value.status), value.complete, JSON.stringify(value));
+ assert.equal(classic.isFailure(value.status), value.failure, JSON.stringify(value));
+ assert.equal(classic.isComplete(value.status), value.complete, JSON.stringify(value));
+ }
+ """
+ subprocess.run([
+ "node", "-e", probe, str(bundle), str(APP / "static" / "js" / "document-status.js"),
+ str(ROOT / "functional_tests" / "document_processing_status_cases.json"),
+ ], check=True, capture_output=True, text=True)
+
+
+@pytest.mark.parametrize("status", [
+ "Processing file failed-complete.md, type: .md",
+ "Processing file processing complete.md, type: .md",
+ "Processing sheet 'error-completed'...",
+ "Retrying Markdown processing after a transient parser concurrency error (1/3)...",
+])
+def test_backend_progress_and_safe_updates_do_not_infer_terminal_state(status):
+ writes = []
+ namespace = {
+ **PREDICATES, "json": json,
+ "update_document": lambda **values: writes.append(values),
+ }
+ run_definitions("functions_documents.py", {
+ "calculate_processing_percentage", "_update_document_for_job",
+ }, namespace)
+ progress = namespace["calculate_processing_percentage"]({
+ "status": status, "percentage_complete": 0,
+ })
+ namespace["_update_document_for_job"](document_id="doc", status=status, safe_errors=True)
+ assert progress == 0
+ assert writes == [{"document_id": "doc", "status": status}]
+
+
+def test_explicit_failure_is_sanitized_and_guarded():
+ writes = []
+ namespace = {
+ **PREDICATES, "update_document": lambda **values: writes.append(values),
+ }
+ run_definitions("functions_documents.py", {"_update_document_for_job"}, namespace)
+ guard = lambda: {"_etag": "revision-1"}
+ namespace["_update_document_for_job"](
+ document_id="doc", status="Error: PRIVATE PROVIDER DETAIL",
+ operation_guard=guard, safe_errors=True,
+ )
+ [written] = writes
+ assert written["status"] == "Document processing failed. Contact an administrator if the problem persists."
+ assert written["expected_etag"] == "revision-1"
+ assert written["strict"] is True
+ assert written["operation_guard"] is guard
+
+
+def test_facets_and_filters_do_not_confuse_details_with_failures():
+ documents = [
+ {"id": "filename", "status": "Processing file failed.md, type: .md", "percentage_complete": 0},
+ {"id": "retry", "status": "Retrying Markdown after an error", "percentage_complete": 20},
+ {"id": "failure", "status": "Error: unavailable", "percentage_complete": 100},
+ {"id": "ready", "status": "Processing complete", "percentage_complete": 100},
+ {"id": "legacy"},
+ ]
+ facets = build_document_facets(documents, "owner")
+ processing = filter_documents_by_place(documents, "processing", "owner")
+ failed = filter_documents_by_place(documents, "errors", "owner")
+ legacy_state = _document_processing_state(documents[-1])
+ assert facets["processing"] == 2
+ assert facets["errors"] == 1
+ assert [item["id"] for item in processing] == ["filename", "retry"]
+ assert [item["id"] for item in failed] == ["failure"]
+ assert legacy_state == "ready"
+
+
+@pytest.mark.parametrize(("changes", "expected"), [
+ ({}, True),
+ ({"status": "Processing file processing complete.md, type: .md", "percentage_complete": 0}, False),
+ ({"status": "Error: unable to read"}, False),
+ ({"number_of_pages": 0}, False),
+ ({"chat_upload_link_state": "unlinked"}, False),
+ ({"search_visibility_state": "archived"}, False),
+])
+def test_search_readiness_preserves_independent_requirements(changes, expected):
+ namespace = {**PREDICATES, "document_is_available": document_is_available}
+ run_definitions("route_backend_chats.py", {"_is_search_ready_chat_upload_workspace_document"}, namespace)
+ document = {
+ "status": "Processing complete", "percentage_complete": 100, "number_of_pages": 1,
+ **changes,
+ }
+ ready = namespace["_is_search_ready_chat_upload_workspace_document"](document)
+ assert ready is expected
+
+
+def test_implementation_version():
+ assert_app_version_at_least("0.261.354")
diff --git a/functional_tests/test_document_search_metadata_sync.py b/functional_tests/test_document_search_metadata_sync.py
index e2c63dfa0..95db21786 100644
--- a/functional_tests/test_document_search_metadata_sync.py
+++ b/functional_tests/test_document_search_metadata_sync.py
@@ -2,7 +2,7 @@
# test_document_search_metadata_sync.py
"""
Functional test for durable, batched document search metadata sync.
-Version: 0.261.268
+Version: 0.261.354
Implemented in: 0.261.268
This test ensures that document metadata edits no longer do per-chunk Search work inside the
@@ -29,6 +29,7 @@
import pytest
from azure.core import MatchConditions
from azure.cosmos.exceptions import CosmosHttpResponseError, CosmosResourceNotFoundError
+from azure.cosmos._cosmos_responses import CosmosDict
REPO_ROOT = Path(__file__).resolve().parents[1]
APP_ROOT = REPO_ROOT / "application" / "single_app"
@@ -43,6 +44,7 @@
document_is_available,
)
import functions_group_document_projection_fence as fence # noqa: E402
+from functions_document_status import is_document_processing_complete, is_document_processing_failure # noqa: E402
from test_support.document_search_sync import ( # noqa: E402
FakeSearchClient,
FakeSettingsContainer,
@@ -75,7 +77,7 @@ class FakeOriginError(Exception):
class FakeDocumentsContainer:
- """Cosmos documents container with etag preconditions, scoped queries and Cosmos error types."""
+ """SDK-shaped points, plain query rows, ETags, scoped queries and Cosmos errors."""
def __init__(self, scope_field):
self.scope_field = scope_field
@@ -87,7 +89,7 @@ def _store(self, body):
item["_etag"] = uuid.uuid4().hex
self.documents[item["id"]] = item
self.writes.append(item["id"])
- return copy.deepcopy(item)
+ return CosmosDict(copy.deepcopy(item), response_headers={})
def query_items(self, query, parameters, enable_cross_partition_query=True):
values = {parameter["name"]: parameter["value"] for parameter in parameters}
@@ -100,7 +102,7 @@ def read_item(self, item, partition_key):
assert item == partition_key
if item not in self.documents:
raise CosmosResourceNotFoundError(status_code=404, message="Not found")
- return copy.deepcopy(self.documents[item])
+ return CosmosDict(copy.deepcopy(self.documents[item]), response_headers={})
def replace_item(self, item, body, etag=None, match_condition=None):
if item not in self.documents:
@@ -164,6 +166,8 @@ def sync_index(document_item, operation=None, **_kwargs):
return {"success": harness.index_success}
namespace = {
+ "is_document_processing_complete": is_document_processing_complete,
+ "is_document_processing_failure": is_document_processing_failure,
"json": json,
"logging": logging,
"re": re,
@@ -203,7 +207,6 @@ def sync_index(document_item, operation=None, **_kwargs):
else harness.user_search
),
"add_file_task_to_file_processing_log": lambda **_kwargs: None,
- "calculate_processing_percentage": lambda document_item: document_item.get("percentage_complete", 0),
"propagate_tags_to_blob_metadata": lambda *args: harness.blob_tag_calls.append(args),
"invalidate_personal_search_cache": lambda user_id: harness.cache_invalidations.append(("personal", user_id)),
"invalidate_group_search_cache": lambda group_id, **_kwargs: harness.cache_invalidations.append(("group", group_id)),
@@ -212,6 +215,10 @@ def sync_index(document_item, operation=None, **_kwargs):
),
"log_event": lambda message, extra=None, **kwargs: harness.logs.append((message, extra, kwargs)),
}
+ nodes.extend(
+ node for node in tree.body
+ if isinstance(node, ast.FunctionDef) and node.name == "calculate_processing_percentage"
+ )
module = ast.Module(body=nodes, type_ignores=[])
ast.fix_missing_locations(module)
exec(compile(module, str(DOCUMENTS_PATH), "exec"), namespace)
@@ -297,6 +304,102 @@ def released_marker():
# --- Engine -------------------------------------------------------------------------------------
+@pytest.mark.parametrize("scope", ["personal", "group", "public"])
+def test_status_updates_preserve_sdk_points_etags_and_query_rows(scope):
+ harness = Harness()
+ if scope == "group":
+ document = harness.add_group_document()
+ container = harness.group_documents
+ identity = {"group_id": "group-1", "user_id": "editor-1"}
+ elif scope == "public":
+ document = {
+ "id": "public-doc-1", "public_workspace_id": "public-1", "user_id": "editor-1",
+ "file_name": "policy.md", "status": "Queued for processing", "percentage_complete": 0,
+ "_etag": "public-revision-1",
+ }
+ container = harness.public_documents
+ container.documents[document["id"]] = document
+ identity = {"public_workspace_id": "public-1", "user_id": "editor-1"}
+ else:
+ document = harness.add_personal_document()
+ container = harness.user_documents
+ identity = {"user_id": "owner-1"}
+ point = container.read_item(document["id"], document["id"])
+ assert type(point) is CosmosDict
+ rows = container.query_items("", [
+ {"name": "@document_id", "value": document["id"]},
+ {"name": f"@{container.scope_field}", "value": document[container.scope_field]},
+ ])
+ assert type(rows[0]) is dict
+ # Start from a new upload, not the fixture's completed document.
+ container.documents[document["id"]].update(status="Queued for processing", percentage_complete=0)
+ saved = harness["update_document"](
+ document_id=document["id"], **identity, strict=True, expected_etag=point["_etag"],
+ status="Processing file processing complete-error-failed.md, type: .md",
+ )
+ assert type(saved) is CosmosDict
+ assert saved["_etag"] != point["_etag"]
+ assert saved["percentage_complete"] == 0
+ with pytest.raises(ScreeningConflictError):
+ harness["update_document"](
+ document_id=document["id"], **identity, strict=True, expected_etag=point["_etag"],
+ status="Processing complete",
+ )
+ completed = harness["update_document"](
+ document_id=document["id"], **identity, strict=True, expected_etag=saved["_etag"],
+ status="Processing complete",
+ )
+ assert type(completed) is CosmosDict
+ assert completed["percentage_complete"] == 100
+
+
+@pytest.mark.parametrize("review_required", [False, True])
+def test_held_status_updates_distinguish_processing_details_from_final_completion(review_required):
+ harness = Harness()
+ document = harness.add_personal_document(
+ percentage_complete=42,
+ content_screening={"state": "scanning", "review_required": review_required},
+ )
+ status = "Processing file processing complete-error-failed.md, type: .md"
+ saved = harness["update_document"](
+ document_id=document["id"], user_id="owner-1", strict=True,
+ expected_etag=document["_etag"], status=status,
+ )
+ assert type(saved) is CosmosDict
+ assert saved["status"] == status
+ assert saved["percentage_complete"] == 42
+ pending = harness["update_document"](
+ document_id=document["id"], user_id="owner-1", strict=True,
+ expected_etag=saved["_etag"], status="Processing complete", percentage_complete=100,
+ )
+ assert pending["status"] == ("Content review required" if review_required else "Content screening pending")
+ assert pending["percentage_complete"] == 99
+ available = document_is_available(pending)
+ assert available is False
+
+
+@pytest.mark.parametrize("blocked_call", [1, 2])
+def test_status_update_cannot_write_after_its_operation_guard_expires(blocked_call):
+ harness = Harness()
+ document = harness.add_personal_document(percentage_complete=0)
+ calls = []
+
+ def guard():
+ calls.append("check")
+ if len(calls) == blocked_call:
+ raise ScreeningConflictError()
+
+ with pytest.raises(ScreeningConflictError):
+ harness["update_document"](
+ document_id=document["id"], user_id="owner-1", strict=True,
+ expected_etag=document["_etag"], operation_guard=guard,
+ status="Processing file failed-complete.md, type: .md",
+ )
+ assert len(calls) == blocked_call
+ assert harness.user_documents.documents[document["id"]] == document
+ assert harness.user_documents.writes == []
+
+
def test_chunk_key_paging_is_key_ordered_scoped_and_id_only():
"""Chunk listing pages by key within the document's active scope and never selects content."""
harness = Harness()
diff --git a/functional_tests/test_group_document_management.py b/functional_tests/test_group_document_management.py
index ff100753a..0dc442879 100644
--- a/functional_tests/test_group_document_management.py
+++ b/functional_tests/test_group_document_management.py
@@ -1,7 +1,7 @@
# test_group_document_management.py
"""
Functional tests for immutable-target group document management.
-Version: 0.261.347
+Version: 0.261.354
Implemented in: 0.261.129
A tag vocabulary conflict answers one coded sentence, from the pre-check or a lost patch: 0.261.167
New tags are defined before any document carries them, so a conflict writes no document: 0.261.168
@@ -263,7 +263,10 @@ def prepare_delete(document_item, actor_id):
)
namespace = env.document_helpers
+ status_helpers = importlib.import_module("functions_document_status")
namespace.update({
+ "is_document_processing_complete": status_helpers.is_document_processing_complete,
+ "is_document_processing_failure": status_helpers.is_document_processing_failure,
"os": os, "json": json, "datetime": datetime, "timezone": timezone,
"tempfile": tempfile, "uuid": uuid, "traceback": traceback, "logging": logging,
"partial": partial, "mimetypes": mimetypes, "BytesIO": BytesIO, "zipfile": zipfile,
@@ -282,7 +285,6 @@ def prepare_delete(document_item, actor_id):
"read_available_document_bytes": env.access.read_available_document_bytes,
"current_extraction": lambda _document_id: None, "is_publication": lambda *_args: False,
"add_file_task_to_file_processing_log": Mock(),
- "calculate_processing_percentage": lambda item: item.get("percentage_complete", 0),
"get_all_chunks": lambda document_id, user_id, group_id=None, public_workspace_id=None: [{"id": f"{document_id}-chunk"}],
"sync_document_access_index_for_document_fail_open": env.index_updates,
"delete_document_access_index_for_document_fail_open": env.index_deletes,
@@ -313,7 +315,7 @@ def prepare_delete(document_item, actor_id):
"get_document_blob_delete_targets", "delete_from_blob_storage",
"_sanitize_download_file_name", "_get_download_content_type", "_get_document_download_entry",
"build_document_download_response", "build_documents_zip_download_response",
- "allowed_file", "ensure_list", "_update_document_for_job",
+ "allowed_file", "ensure_list", "_update_document_for_job", "calculate_processing_percentage",
}, namespace)
# Metadata edits record a durable search sync request; the sync worker merges the latest values
# into an in-memory index with one chunk per document. Tests run queued syncs explicitly.
diff --git a/functional_tests/test_v2_chat_context_request.ts b/functional_tests/test_v2_chat_context_request.ts
index 1a75f50e8..7b918cbed 100644
--- a/functional_tests/test_v2_chat_context_request.ts
+++ b/functional_tests/test_v2_chat_context_request.ts
@@ -1,7 +1,7 @@
// test_v2_chat_context_request.ts
//
// Runtime test for how the V2 composer's context chips become a chat request.
-// Version: 0.261.096
+// Version: 0.261.354
// Implemented in: 0.261.089
// Independent context selection implemented in: 0.261.094
// Shared editor implemented in: 0.261.096
@@ -409,6 +409,28 @@ check('legacy uploads require actual conversation and file-message identities',
}, 'notes.txt'), /group workspace/);
});
+check('upload states ignore filenames and stage details without relaxing real failures or approvals', () => {
+ for (const status of [
+ 'Processing file failed-policy.md, type: .md',
+ 'Processing file error-budget.txt, type: .txt',
+ 'Processing file processing complete.md, type: .md',
+ 'Retrying Markdown processing after a transient parser concurrency error (1/3)...',
+ 'Vision analysis completed (no results)',
+ ]) {
+ assert.equal(chatUploadDocumentStatus({ status, percentage_complete: 0 }).state, 'processing');
+ assert.equal(chatUploadDocumentStatus({ status }).state, 'processing');
+ }
+ assert.equal(chatUploadDocumentStatus({ status: 'Processing complete - no content indexed' }).state, 'ready');
+ assert.equal(chatUploadDocumentStatus({ status: 'Error: failed', percentage_complete: 100 }).state, 'failed');
+ assert.equal(chatUploadDocumentStatus({ status: 'Complete', shared_approval_status: 'not_approved' }).state, 'failed');
+ assert.equal(chatUploadDocumentStatus({
+ status: 'Complete', percentage_complete: 100, generated_artifact_promotion_status: 'approval_failed',
+ }).state, 'failed');
+ assert.equal(chatUploadDocumentStatus({
+ status: 'Complete', percentage_complete: 100, generated_artifact_promotion_status: 'pending_approval',
+ }).state, 'failed');
+});
+
check('upload validation preserves chat format, account and size gates', () => {
const text = new File(['source'], 'notes.txt');
assert.equal(chatUploadValidationError(text, true, 10), null);
@@ -489,6 +511,34 @@ check('processing polls the authorized document endpoint until ready and honors
}
});
+check('filename-bearing processing and transient errors do not stop personal or group polling', async () => {
+ const originalFetch = globalThis.fetch;
+ try {
+ for (const kind of ['personal', 'group'] as const) {
+ let calls = 0;
+ const statuses = [
+ 'Processing file FED-failed-harness-followup-20261019-a-FED-17-policy-v1.md, type: .md',
+ 'Processing file processing complete.md, type: .md',
+ 'Retrying Markdown processing after a transient parser concurrency error (1/3)...',
+ 'Processing complete - final metadata extracted',
+ ];
+ globalThis.fetch = async (url) => {
+ assert.equal(String(url), `/api/${kind === 'group' ? 'group_' : ''}documents/workspace-id`);
+ const status = statuses[calls++];
+ return new Response(JSON.stringify({ status, percentage_complete: calls === 4 ? 100 : 20 }),
+ { headers: { 'Content-Type': 'application/json' } });
+ };
+ const states: string[] = [];
+ await pollChatUpload({ kind: 'document', id: 'workspace-id', scope: { kind, id: 'scope-id' } },
+ new AbortController().signal, (status) => states.push(status.state));
+ assert.deepEqual(states, ['processing', 'processing', 'processing', 'ready']);
+ assert.equal(calls, 4);
+ }
+ } finally {
+ globalThis.fetch = originalFetch;
+ }
+});
+
/* -------------------------------------------------------------------------- */
let failed = 0;
diff --git a/functional_tests/test_v2_documents_explorer.py b/functional_tests/test_v2_documents_explorer.py
index 7c981d65e..a62c936a3 100644
--- a/functional_tests/test_v2_documents_explorer.py
+++ b/functional_tests/test_v2_documents_explorer.py
@@ -3,7 +3,7 @@
"""
Functional test for the V2 workspace documents explorer.
-Version: 0.261.323
+Version: 0.261.354
Implemented in: 0.261.048
Shared workspace shell integration: 0.261.127
@@ -60,6 +60,8 @@
SECTIONS_PY = APP_DIR / "functions_workspace_sections.py"
DOCUMENTS_FUNCTIONS = APP_DIR / "functions_documents.py"
DOCUMENT_QUERY_FUNCTIONS = APP_DIR / "functions_document_queries.py"
+sys.path.insert(0, str(APP_DIR))
+from functions_document_status import is_document_processing_failure # noqa: E402
ENDPOINTS_TS = V2_SRC / "lib" / "endpoints.ts"
EXPLORER_TS = V2_SRC / "lib" / "documentExplorer.ts"
@@ -119,6 +121,7 @@ def is_constant_assignment(node):
"datetime": datetime,
"timedelta": timedelta,
"timezone": timezone,
+ "is_document_processing_failure": is_document_processing_failure,
}
# Constants are executed one at a time and failures are skipped: these modules also
diff --git a/functional_tests/test_v2_documents_explorer_logic.ts b/functional_tests/test_v2_documents_explorer_logic.ts
index f8c0750db..76d7ce5d2 100644
--- a/functional_tests/test_v2_documents_explorer_logic.ts
+++ b/functional_tests/test_v2_documents_explorer_logic.ts
@@ -1,7 +1,7 @@
// test_v2_documents_explorer_logic.ts
//
// Runtime test for the V2 documents explorer rules.
-// Version: 0.261.048
+// Version: 0.261.354
// Implemented in: 0.261.048
//
// The companion test, test_v2_documents_explorer.py, asserts that the pieces are wired
@@ -76,6 +76,22 @@ function check(name: string, fn: () => void) {
const query = (overrides: Record = {}): any => ({ ...DEFAULT_DOCUMENT_QUERY, ...overrides });
const params = (input: string): Record => Object.fromEntries(new URLSearchParams(input));
+check('document state ignores filename failure words and retains progress and approval contracts', () => {
+ for (const status of [
+ 'Processing file failed-policy.md, type: .md',
+ 'Processing file error-budget.txt, type: .txt',
+ 'Processing file completed-checklist.pdf, type: .pdf',
+ 'Retrying Markdown after a transient error',
+ ]) {
+ assert.equal(documentStatus({ status, percentage_complete: 25 }).state, 'processing');
+ }
+ assert.equal(documentStatus({ status: 'Error: failed', percentage_complete: 100 }).state, 'error');
+ assert.equal(documentStatus({ percentage_complete: 'invalid' }).state, 'ready');
+ assert.equal(documentStatus({ status: 'Processing' }).state, 'ready');
+ assert.equal(documentStatus({ percentage_complete: -10 }).percent, 0);
+ assert.equal(documentStatus({ percentage_complete: 100, shared_approval_status: 'not_approved' }).state, 'pending_approval');
+});
+
/* ------------------------------ query parameters ------------------------------ */
check('the default query asks for page one at the default size', () => {
diff --git a/functional_tests/test_workspace_metadata_outcomes.py b/functional_tests/test_workspace_metadata_outcomes.py
index 9d84516ab..47b59dc7c 100644
--- a/functional_tests/test_workspace_metadata_outcomes.py
+++ b/functional_tests/test_workspace_metadata_outcomes.py
@@ -1,7 +1,7 @@
# test_workspace_metadata_outcomes.py
"""
Runtime regressions for shared automatic and manual workspace metadata analysis.
-Version: 0.261.347
+Version: 0.261.354
Implemented in: 0.261.347
Execute the real extraction, merge, worker, and final-upload functions with only
@@ -25,6 +25,7 @@
from test_cosmos_wave5a_document_access_read_switch import (
FakeCosmosContainer, _load_document_access_index_module, _succeeded_backfill_state,
)
+from functions_document_status import is_document_processing_failure
class SdkProjectionContainer(FakeCosmosContainer):
@@ -85,6 +86,7 @@ def complete(**arguments):
client = SimpleNamespace(chat=SimpleNamespace(completions=SimpleNamespace(create=complete)))
namespace = {
+ "is_document_processing_failure": is_document_processing_failure,
"json": json, "logging": logging, "re": re, "partial": partial,
"get_settings": lambda: settings, "get_document_metadata": lambda *args, **kwargs: copy.deepcopy(document),
"update_document": update_document, "hybrid_search": search,
diff --git a/ui_tests/test_chat_workspace_upload_progress_polling.py b/ui_tests/test_chat_workspace_upload_progress_polling.py
index 3f202515d..177fdd446 100644
--- a/ui_tests/test_chat_workspace_upload_progress_polling.py
+++ b/ui_tests/test_chat_workspace_upload_progress_polling.py
@@ -1,7 +1,7 @@
# test_chat_workspace_upload_progress_polling.py
"""
UI test for chat workspace upload progress polling.
-Version: 0.241.203
+Version: 0.261.354
Implemented in: 0.241.203
This test ensures workspace-backed chat upload cards keep the progress bar
@@ -13,6 +13,9 @@
from pathlib import Path
import pytest
+from playwright.sync_api import expect
+
+from test_v2_chat_context_selection import connect_options # noqa: F401
REPO_ROOT = Path(__file__).resolve().parents[1]
@@ -20,23 +23,9 @@
@pytest.mark.ui
-def test_chat_workspace_upload_progress_polling_recovers_from_transient_error():
+def test_chat_workspace_upload_progress_polling_recovers_from_transient_error(page):
"""Validate workspace upload progress polling updates the visible chat card."""
- playwright_sync_api = pytest.importorskip("playwright.sync_api")
- expect = playwright_sync_api.expect
- sync_playwright = playwright_sync_api.sync_playwright
-
- with sync_playwright() as playwright:
- try:
- browser = playwright.chromium.launch()
- except Exception as exc:
- pytest.skip(f"Playwright browser unavailable: {exc}")
-
- page = browser.new_page()
- try:
- run_chat_workspace_upload_progress_polling_scenario(page, expect)
- finally:
- browser.close()
+ run_chat_workspace_upload_progress_polling_scenario(page, expect)
def run_chat_workspace_upload_progress_polling_scenario(page, expect):
@@ -56,6 +45,7 @@ def run_chat_workspace_upload_progress_polling_scenario(page, expect):
"""
)
+ page.add_script_tag(path=str(REPO_ROOT / "application" / "single_app" / "static" / "js" / "document-status.js"))
page.add_script_tag(
content=f"""
const chatWorkspaceUploadPolls = new Map();
diff --git a/ui_tests/test_document_status_classification.py b/ui_tests/test_document_status_classification.py
new file mode 100644
index 000000000..eb9131cde
--- /dev/null
+++ b/ui_tests/test_document_status_classification.py
@@ -0,0 +1,89 @@
+# test_document_status_classification.py
+"""
+Browser coverage for shared classic document status interpretation.
+Version: 0.261.354
+Implemented in: 0.261.354
+
+Exercise real group views and personal/public/card status predicates with local
+assets and intercepted APIs, including in-progress, failed and completed states.
+"""
+
+import re
+
+import pytest
+from playwright.sync_api import expect
+
+from test_group_document_filename_xss_rendering import ( # noqa: F401
+ APP_ROOT, VIEWS, _document, _render, group_page, group_render_sources,
+)
+from test_v2_chat_context_selection import connect_options # noqa: F401
+
+
+pytestmark = pytest.mark.ui
+
+
+@pytest.mark.parametrize("view", VIEWS)
+@pytest.mark.parametrize("filename", ["error-budget.md", "failed-policy.md", "incomplete-policy.md"])
+def test_group_views_do_not_enable_ready_actions_from_filename(group_page, view, filename):
+ page, state = group_page
+ record = _document(filename, status=f"Processing file {filename}, type: .md", percentage_complete=0)
+ rows = _render(group_page, view, [record])
+ expect(rows.first.locator("a.dropdown-item").filter(has_text=re.compile(r"^\s*Share"))).to_have_count(0)
+ if view == "list":
+ assert page.evaluate("window.__pollCallbacks.size") == 1
+ state["documents"] = [{**record, "status": "Processing complete", "percentage_complete": 100}]
+ page.evaluate("() => window.__pollCallbacks.forEach(callback => callback())")
+ expect(rows.first.locator("a.dropdown-item").filter(has_text=re.compile(r"^\s*Share"))).to_have_count(1)
+
+
+@pytest.mark.parametrize(("relative", "function"), [
+ ("static/js/workspace/workspace-documents.js", "getDocumentProcessingState"),
+ ("static/js/public/public_workspace.js", "getPublicDocumentProcessingState"),
+])
+def test_personal_and_public_processing_flags(page, relative, function):
+ source = APP_ROOT.joinpath(*relative.split("/")).read_text(encoding="utf-8")
+ start = source.index(f"function {function}(")
+ end = source.index("\n}", start) + len("\n}")
+ page.set_content('')
+ page.add_script_tag(path=str(APP_ROOT / "static" / "js" / "document-status.js"))
+ page.add_script_tag(content=source[start:end])
+ for status in [
+ "Processing file error-budget.md, type: .md",
+ "Processing file failed-checklist.md, type: .md",
+ "Processing file processing complete.md, type: .md",
+ "Retrying Markdown processing after a transient parser concurrency error (1/3)...",
+ ]:
+ result = page.evaluate(
+ "({name, status}) => window[name]({status, percentage_complete: 0})",
+ {"name": function, "status": status},
+ )
+ assert result["hasError"] is False
+ assert result["isComplete"] is False
+ failure = page.evaluate(
+ "(name) => window[name]({status: 'Error: unavailable', percentage_complete: 100})", function,
+ )
+ assert failure["hasError"] is True
+ assert failure["isComplete"] is True
+
+
+def test_classic_chat_card_polls_instead_of_completing_from_details(page):
+ source = (APP_ROOT / "static" / "js" / "chat" / "chat-messages.js").read_text(encoding="utf-8")
+ start = source.index("function getChatWorkspaceProgressValue(")
+ end = source.index("function normalizeChatWorkspaceDocumentResponse(")
+ page.set_content('')
+ page.add_script_tag(path=str(APP_ROOT / "static" / "js" / "document-status.js"))
+ page.add_script_tag(content=source[start:end])
+ for status in [
+ "Processing file error-budget.md, type: .md",
+ "Processing file incomplete-policy.md, type: .md",
+ "Retrying Markdown processing after a transient parser concurrency error (1/3)...",
+ "Vision analysis completed (no results)",
+ ]:
+ result = page.evaluate(
+ """(status) => {
+ const doc = {status, percentage_complete: 0};
+ return [isChatWorkspaceDocumentComplete(doc), isChatWorkspaceDocumentFailure(doc),
+ isChatWorkspaceDocumentSuccessComplete(doc)];
+ }""", status,
+ )
+ assert result == [False, False, False]
diff --git a/ui_tests/test_group_document_filename_xss_rendering.py b/ui_tests/test_group_document_filename_xss_rendering.py
index 8c9e16e90..8f998839b 100644
--- a/ui_tests/test_group_document_filename_xss_rendering.py
+++ b/ui_tests/test_group_document_filename_xss_rendering.py
@@ -1,7 +1,7 @@
# test_group_document_filename_xss_rendering.py
"""
Browser regressions for group document filename rendering.
-Version: 0.261.029
+Version: 0.261.354
Implemented in: 0.261.029
Run the real template renderers, access/processing checks, fetch/refresh/polling
@@ -201,6 +201,7 @@ def handle_request(route):
page.goto("https://simplechat.test/")
page.add_style_tag(path=str(APP_ROOT / "static" / "css" / "bootstrap.min.css"))
page.add_script_tag(path=str(APP_ROOT / "static" / "js" / "bootstrap" / "bootstrap.bundle.min.js"))
+ page.add_script_tag(path=str(APP_ROOT / "static" / "js" / "document-status.js"))
page.add_script_tag(path=str(APP_ROOT / "static" / "js" / "workspace" / "group-documents-sharing.js"))
page.add_script_tag(content=script)
page.evaluate("initializeGroupSharing()")
diff --git a/ui_tests/test_v2_chat_upload_status.py b/ui_tests/test_v2_chat_upload_status.py
new file mode 100644
index 000000000..a1f32b194
--- /dev/null
+++ b/ui_tests/test_v2_chat_upload_status.py
@@ -0,0 +1,124 @@
+# test_v2_chat_upload_status.py
+"""
+Browser regressions for filename-independent chat upload status.
+Version: 0.261.354
+Implemented in: 0.261.354
+
+Run the real Composer and polling with intercepted upload/document APIs, using
+the existing local/Azure Playwright connection. No live uploads are performed.
+"""
+
+import pytest
+from playwright.sync_api import expect
+
+from test_v2_chat_context_selection import ( # noqa: F401
+ CONVERSATION_ID, connect_options, context_page, mount_workflow,
+)
+
+
+pytestmark = pytest.mark.ui
+
+
+def prepare_upload(page, scope, filename):
+ mount_workflow(page)
+ page.evaluate(
+ """() => {
+ const store = window.OrchHarness.stores.bootstrap.useBootstrapStore;
+ store.setState(state => ({data: {...state.data,
+ features: {...state.data.features, enable_chat_file_uploads: true}
+ }}));
+ }"""
+ )
+ page.get_by_role("textbox", name="Message", exact=True).fill("Review the uploaded policy.")
+ counts = {"uploads": 0, "polls": 0}
+ state = {"status": f"Processing file {filename}, type: .md", "progress": 0}
+
+ def upload(route):
+ counts["uploads"] += 1
+ route.fulfill(json={
+ "conversation_id": CONVERSATION_ID,
+ "file_message_id": "upload-message",
+ "workspace_document_id": "upload-document",
+ "workspace_scope": scope,
+ "workspace_document": {
+ "document_id": "upload-document", "scope": scope,
+ "group_id": "group-1" if scope == "group" else None,
+ "file_name": filename, "status": "Queued for processing", "percentage_complete": 0,
+ },
+ })
+
+ def status(route):
+ counts["polls"] += 1
+ route.fulfill(json={
+ "id": "upload-document", "file_name": filename, "status": state["status"],
+ "percentage_complete": state["progress"],
+ })
+
+ page.route("**/upload", upload)
+ page.route("**/api/get_messages?*", lambda route: route.fulfill(json={"messages": []}))
+ endpoint = "group_documents" if scope == "group" else "documents"
+ page.route(f"**/api/{endpoint}/upload-document", status)
+ page.locator('[data-composer-editor="composer-input"] input[type="file"]').set_input_files({
+ "name": filename, "mimeType": "text/markdown", "buffer": b"# Policy\nLimit: 125000.",
+ })
+ item = page.get_by_role("list", name="Attached files", exact=True).get_by_role("listitem")
+ expect(item.get_by_text("Processing 0%", exact=True)).to_be_visible()
+ return item, counts, state
+
+
+@pytest.mark.parametrize("scope", ["personal", "group"])
+@pytest.mark.parametrize("filename", [
+ "FED-failed-harness-followup-20261019-a-FED-17-policy-v1.md",
+ "error-budget.md",
+ "processing complete.md",
+ "incomplete-policy.md",
+])
+def test_processing_keeps_polling_until_real_completion(context_page, scope, filename):
+ page, _ = context_page
+ item, counts, state = prepare_upload(page, scope, filename)
+ endpoint = "group_documents" if scope == "group" else "documents"
+ with page.expect_request(f"**/api/{endpoint}/upload-document"):
+ expect(page.get_by_role("button", name="Send message", exact=True)).to_be_disabled()
+ expect(item.get_by_role("alert")).to_have_count(0)
+ expect(item.get_by_text("Ready", exact=True)).to_have_count(0)
+ state.update(status="Retrying Markdown processing after a transient parser concurrency error (1/3)...", progress=20)
+ expect(item.get_by_text("Processing 20%", exact=True)).to_be_visible()
+ expect(page.get_by_role("button", name="Send message", exact=True)).to_be_disabled()
+ state.update(status="Processing complete - final metadata extracted", progress=100)
+ expect(item.get_by_text("Ready", exact=True)).to_be_visible(timeout=15000)
+ expect(page.get_by_role("button", name="Send message", exact=True)).to_be_enabled()
+ assert counts["uploads"] == 1
+ assert counts["polls"] >= 4
+
+
+@pytest.mark.parametrize("scope", ["personal", "group"])
+def test_real_failure_stays_visible_and_retry_only_rechecks_status(context_page, scope):
+ page, _ = context_page
+ filename = "error-budget.md"
+ item, counts, state = prepare_upload(page, scope, filename)
+ state.update(status="Error: Unable to process document.", progress=100)
+ expect(item.get_by_text("Failed", exact=True)).to_be_visible(timeout=10000)
+ expect(item.get_by_role("alert")).to_contain_text("File processing failed.")
+ expect(page.get_by_role("button", name="Send message", exact=True)).to_be_disabled()
+ before = counts["polls"]
+ state.update(status="Processing complete", progress=100)
+ page.get_by_role("button", name=f"Retry {filename}", exact=True).click()
+ expect(item.get_by_text("Ready", exact=True)).to_be_visible()
+ assert counts["uploads"] == 1
+ assert counts["polls"] > before
+
+
+def test_removing_processing_upload_cancels_tracking(context_page):
+ page, _ = context_page
+ page.clock.install()
+ filename = "failed-checklist.md"
+ item, counts, state = prepare_upload(page, "personal", filename)
+ page.get_by_role("button", name=f"Remove {filename}", exact=True).click()
+ expect(item).to_have_count(0)
+ before = counts["polls"]
+ state.update(status="Processing complete", progress=100)
+ # Rendering another draft change must not resurrect a removed upload.
+ page.get_by_role("textbox", name="Message", exact=True).fill("No attachment.")
+ page.clock.run_for(6000)
+ expect(page.get_by_role("list", name="Attached files", exact=True)).to_have_count(0)
+ assert counts["polls"] == before