fix(ui,config): improve activity feed parsing and test isolation
This commit is contained in:
1 parent
96d1ce9029
commit
de45e481da
6 files changed
+143
-6
No files matched your search
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
Reference in new issue
Block a user