fix: explain chat session and runtime failures
This commit was merged in pull request #88.
This commit is contained in:
@@ -37,6 +37,19 @@ func TestChatPageServedWithoutInternalIDs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatScriptExplainsSessionAndRuntimeFailures(t *testing.T) {
|
||||
body, err := staticFiles.ReadFile("static/assets/chat.js")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
script := string(body)
|
||||
for _, required := range []string{"class ChatApiError", "Сессия устарела", "основной runtime сейчас недоступен", "Проверьте текст и повторите попытку"} {
|
||||
if !strings.Contains(script, required) {
|
||||
t.Fatalf("chat script is missing recovery message %q", required)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicSurfacesSeparateChatAndConsole(t *testing.T) {
|
||||
handler, err := newHandler("http://core", "http://continuity", "http://commit", http.DefaultClient, "http://cognitive")
|
||||
if err != nil {
|
||||
|
||||
@@ -24,6 +24,14 @@ let conversationID = sessionStorage.getItem("domovoy.chat.conversation") || "";
|
||||
let localHistory = [];
|
||||
let cognitiveStatusTimer = null;
|
||||
|
||||
class ChatApiError extends Error {
|
||||
constructor(status, detail = "") {
|
||||
super(detail || `chat api returned ${status}`);
|
||||
this.name = "ChatApiError";
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
function containsSecretShapedText(value) {
|
||||
return /(-----BEGIN [A-Z ]+PRIVATE KEY-----|\b(?:password|passwd|пароль|token|токен|api[_ -]?key|секрет|ключ)\s*[:=]|\bsk-[a-zA-Z0-9_-]{12,})/i.test(value);
|
||||
}
|
||||
@@ -58,7 +66,11 @@ async function loadCognitiveStatus() {
|
||||
try {
|
||||
const result = await api("/api/capabilities/cognitive-proposal/health", {method: "GET"});
|
||||
renderCognitiveStatus(result.State || result.state || "UNKNOWN");
|
||||
} catch {
|
||||
} catch (error) {
|
||||
if (error instanceof ChatApiError && (error.status === 401 || error.status === 403)) {
|
||||
expireSession("Сессия устарела. Войдите снова, чтобы продолжить разговор.");
|
||||
return;
|
||||
}
|
||||
renderCognitiveStatus("UNAVAILABLE");
|
||||
}
|
||||
}
|
||||
@@ -116,10 +128,21 @@ async function api(path, options = {}) {
|
||||
const headers = {"Content-Type": "application/json", ...(options.headers || {})};
|
||||
if (token) headers.Authorization = `Bearer ${token}`;
|
||||
const response = await fetch(path, {...options, headers});
|
||||
if (!response.ok) throw new Error(await response.text());
|
||||
if (!response.ok) throw new ChatApiError(response.status, await response.text());
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function expireSession(message = "") {
|
||||
token = "";
|
||||
conversationID = "";
|
||||
sessionStorage.removeItem("domovoy.chat.token");
|
||||
sessionStorage.removeItem("domovoy.chat.conversation");
|
||||
messages.replaceChildren();
|
||||
updateActiveFocus("");
|
||||
if (message) loginError.textContent = message;
|
||||
showLogin();
|
||||
}
|
||||
|
||||
function humanMeta(result) {
|
||||
if (result.reminder_scheduled) return "Напоминание сохранено. Я вернусь к нему сам.";
|
||||
if (result.delegation_id && result.delegation_state) return `Передал в coding-agent: ${humanDelegationState(result.delegation_state)}.`;
|
||||
@@ -249,8 +272,14 @@ async function sendChatMessage(text, options = {}) {
|
||||
await loadToday();
|
||||
activateTab("today-panel");
|
||||
}
|
||||
} catch {
|
||||
appendMessage("assistant", "Чат сейчас недоступен. Сообщение не принято runtime.");
|
||||
} catch (error) {
|
||||
if (error instanceof ChatApiError && (error.status === 401 || error.status === 403)) {
|
||||
expireSession("Сессия устарела. Войдите снова, чтобы продолжить разговор.");
|
||||
} else if (error instanceof ChatApiError && error.status >= 500) {
|
||||
appendMessage("assistant", "Домовой не смог завершить обработку: основной runtime сейчас недоступен. Встроенный канал проверяется; повторите сообщение через несколько секунд.");
|
||||
} else {
|
||||
appendMessage("assistant", "Домовой не принял сообщение. Проверьте текст и повторите попытку.");
|
||||
}
|
||||
} finally {
|
||||
sendButton.disabled = false;
|
||||
messageInput.focus();
|
||||
@@ -323,13 +352,7 @@ if (aiDataForm) {
|
||||
if (aiDataInput) aiDataInput.addEventListener("input", () => showAIDataError());
|
||||
|
||||
logoutButton.addEventListener("click", () => {
|
||||
token = "";
|
||||
conversationID = "";
|
||||
sessionStorage.removeItem("domovoy.chat.token");
|
||||
sessionStorage.removeItem("domovoy.chat.conversation");
|
||||
messages.replaceChildren();
|
||||
updateActiveFocus("");
|
||||
showLogin();
|
||||
expireSession();
|
||||
});
|
||||
|
||||
if (token) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"task_file": "dev/tasks/DEV-000089-domovoy-cognitive-upgrade-integration.json",
|
||||
"task_id": "DEV-000089",
|
||||
"task_file": "dev/tasks/DEV-000090-chat-runtime-error-recovery.json",
|
||||
"task_id": "DEV-000090",
|
||||
"task_revision": 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000090",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "DEV-000073-baseline",
|
||||
"base_repo_revision": "57fb572d5b7dadff9fd82e995448c1fea30e1b5f",
|
||||
"risk_tier": "E",
|
||||
"intent": "Make Domovoy chat failures understandable and recoverable by distinguishing expired sessions, runtime outages, and invalid requests in the human UI.",
|
||||
"requirement_ids": ["Human sees the actual recovery state", "Expired sessions return to login", "Runtime failure does not pretend to be a model answer"],
|
||||
"allowed_change_scope": {"modules": ["creator-web", "development-governance"], "paths": ["cmd/creator-web/chat_test.go", "cmd/creator-web/static/assets/chat.js", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000090-chat-runtime-error-recovery.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
|
||||
"contracts": {"consume": ["existing creator-web HTTP status contract"], "may_change": ["human-facing chat error handling"]},
|
||||
"semantic_owner": "creator-web",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["401/403 -> clear session and show login", "5xx -> explain runtime outage and allow retry", "other client error -> explain invalid request"],
|
||||
"side_effects_and_privileges": ["No new privilege", "No internal identifiers or raw upstream error text shown"],
|
||||
"persistence_and_migrations": ["No persistence or migration"],
|
||||
"failure_retry_restart": ["Session recovery is local and deterministic", "Runtime outage remains retryable"],
|
||||
"assumptions": ["Creator Web status codes remain the source of truth for session and runtime failure classes"],
|
||||
"acceptance_scenarios": ["Expired browser token returns to login with an explanation", "Runtime outage shows a useful retry message", "Valid responses remain unchanged", "No generic 'message not accepted runtime' text remains"],
|
||||
"required_tests": ["go test ./cmd/creator-web", "go test ./...", "go vet ./...", "devctl preflight", "devctl check-diff", "git diff --check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
Reference in New Issue
Block a user