fix(ui,config): improve activity feed parsing and test isolation

This commit is contained in:
Riz Ashraf committed 2026-10-12 05:20:09 +01:00
1 parent 96d1ce9029
commit de45e481da
6 files changed
+143 -6

No files matched your search

+6 -1
View File
@@ -142,10 +142,15 @@ deploy-rules-wsl:
# =========================================================
# Run fast parallel unit tests via cargo-nextest (and type-check UI)
test: check-ui
test: check-ui test-js
@Write-Host "Running fast parallel tests via cargo-nextest..." -ForegroundColor Cyan
$env:MCP_ALLOW_TMP_FALLBACK='1'; cargo nextest run --workspace
# Run JavaScript UI unit tests using Bun
test-js: build-ui
@Write-Host "Running dashboard JavaScript unit tests..." -ForegroundColor Cyan
bun test server/tests/dashboard.test.ts
# Run dashboard UI endpoint and integrity unit tests
test-ui:
@Write-Host "Running dashboard UI endpoint and integrity unit tests..." -ForegroundColor Cyan
+6 -1
View File
@@ -37,10 +37,15 @@ pub fn load_mcp_config_env() {
}
pub fn get_env(key: &str) -> Result<String, env::VarError> {
// Process environment variables must always take precedence over the config file
// to allow testing frameworks to override paths (like MCP_MEMORY_STORE_DIR).
if let Ok(val) = env::var(key) {
return Ok(val);
}
if let Some(map) = CONFIG_ENV.get() {
if let Some(val) = map.get(key) {
return Ok(val.clone());
}
}
env::var(key)
Err(env::VarError::NotPresent)
}
+4 -2
View File
@@ -811,6 +811,9 @@ function handleIncomingActivity(payload) {
} else if (category === "MEMO" || category === "MILESTONE" || category === "HANDOFF_MEMO") {
requestDomainRefresh("memos");
}
if (method === "notifications/resources/updated") {
return;
}
const feed = document.getElementById("activity-feed");
if (feed) {
if (feed.querySelector(".feed-entry") === null) {
@@ -861,8 +864,7 @@ function setupWS() {
return;
}
if (data.type === "activity" || data.type === "gate" || data.type === "task" || data.type === "tech_debt" || data.type === "nvim_telemetry" || data.method === "notifications/activity" || data.method === "notifications/task/completed" || data.method === "notifications/resources/updated") {
const payload = data.params || data.data || data;
handleIncomingActivity(payload);
handleIncomingActivity(data);
}
} catch (e) {
console.error("WebSocket message parse error", e);
+6 -2
View File
@@ -1083,6 +1083,11 @@ function handleIncomingActivity(payload: any): void {
) {
requestDomainRefresh("memos");
}
if (method === "notifications/resources/updated") {
return;
}
const feed = document.getElementById("activity-feed");
if (feed) {
if (feed.querySelector(".feed-entry") === null) {
@@ -1144,8 +1149,7 @@ function setupWS(): void {
data.method === "notifications/task/completed" ||
data.method === "notifications/resources/updated"
) {
const payload = data.params || data.data || data;
handleIncomingActivity(payload);
handleIncomingActivity(data);
}
} catch (e) {
console.error("WebSocket message parse error", e);
+59
View File
@@ -1121,6 +1121,65 @@ pub fn format_tool_activity_description(name: &str, args: &serde_json::Value) ->
.to_string(),
)
}
"omni_search" => {
let query = args.get("query").and_then(|v| v.as_str()).unwrap_or("");
("OmniSearch", query.to_string())
}
"create_relations" => {
let names = args
.get("relations")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|r| {
let source = r.get("source").or_else(|| r.get("from")).and_then(|v| v.as_str()).unwrap_or("?");
let target = r.get("target").or_else(|| r.get("to")).and_then(|v| v.as_str()).unwrap_or("?");
let rel = r.get("relationType").or_else(|| r.get("type")).or_else(|| r.get("relation_type")).and_then(|v| v.as_str()).unwrap_or("?");
Some(format!("{} -[{}]-> {}", source, rel, target))
})
.collect::<Vec<_>>()
.join(", ")
})
.unwrap_or_default();
("Create Relations", names)
}
"log_code_change" => {
let file = args.get("file_path").and_then(|v| v.as_str()).unwrap_or("");
let desc = args.get("description").and_then(|v| v.as_str()).unwrap_or("");
let detail = if !file.is_empty() && !desc.is_empty() {
format!("{} ({})", desc, file)
} else if !file.is_empty() {
file.to_string()
} else {
desc.to_string()
};
("Log Code Change", detail)
}
"log_error_fix" => {
let sig = args.get("signature").and_then(|v| v.as_str()).unwrap_or("");
let sol = args.get("solution").and_then(|v| v.as_str()).unwrap_or("");
let detail = if !sig.is_empty() && !sol.is_empty() {
format!("{}: {}", sig, sol)
} else if !sig.is_empty() {
sig.to_string()
} else {
sol.to_string()
};
("Log Error Fix", detail)
}
"open_nodes" => {
let names = args
.get("names")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|e| e.as_str())
.collect::<Vec<_>>()
.join(", ")
})
.unwrap_or_default();
("Open Nodes", names)
}
_ => return name.to_string(),
};
if detail.is_empty() {
+62
View File
@@ -0,0 +1,62 @@
import { test, expect } from "bun:test";
import { readFileSync } from "fs";
test("dashboard parses and renders activity payload", () => {
// 1. Read dashboard.js (the transpiled JavaScript)
let code = readFileSync("server/src/dashboard.js", "utf8");
// Remove the trailing initialization calls that cause side-effects
code = code.replace("loadVersion();", "");
code = code.replace("loadActivityHistory();", "");
code = code.replace("setupWS();", "");
// 2. Mock necessary DOM and Browser globals
const context = {
window: {},
document: {
documentElement: {},
getElementById: () => null,
createElement: () => ({ classList: { add: () => {} }, prepend: () => {}, removeChild: () => {}, children: [] }),
addEventListener: () => {},
},
location: { protocol: "http:" },
MutationObserver: class { observe() {} },
vis: { DataSet: class {}, Network: class {} },
console: console,
fetch: () => Promise.resolve({ json: () => Promise.resolve({}) }),
WebSocket: class { addEventListener() {} },
setTimeout: setTimeout,
Date: Date,
localStorage: { getItem: () => null, setItem: () => {} },
};
// 3. Create a function that executes the code
const extractFn = new Function(...Object.keys(context), code + `
return {
parseActivityPayload,
handleIncomingActivity,
requestDomainRefresh
};
`);
const exports = extractFn(...Object.values(context));
// 4. Test parseActivityPayload normal case
const payload = {
category: "CODE_CHANGE",
message: "Refactored live activity UI",
};
const html = exports.parseActivityPayload(payload);
expect(html).toContain("CODE_CHANGE");
expect(html).toContain("Refactored live activity UI");
// 5. Test parseActivityPayload with resources/updated
// It should handle fallback correctly if we pass it directly
const resPayload = {
method: "notifications/resources/updated",
params: { uri: "memory://tasks/active" }
};
const htmlRes = exports.parseActivityPayload(resPayload);
expect(htmlRes).toContain("memory://tasks/active");
});