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)
|
# 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
|
@Write-Host "Running fast parallel tests via cargo-nextest..." -ForegroundColor Cyan
|
||||||
$env:MCP_ALLOW_TMP_FALLBACK='1'; cargo nextest run --workspace
|
$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
|
# Run dashboard UI endpoint and integrity unit tests
|
||||||
test-ui:
|
test-ui:
|
||||||
@Write-Host "Running dashboard UI endpoint and integrity unit tests..." -ForegroundColor Cyan
|
@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> {
|
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(map) = CONFIG_ENV.get() {
|
||||||
if let Some(val) = map.get(key) {
|
if let Some(val) = map.get(key) {
|
||||||
return Ok(val.clone());
|
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") {
|
} else if (category === "MEMO" || category === "MILESTONE" || category === "HANDOFF_MEMO") {
|
||||||
requestDomainRefresh("memos");
|
requestDomainRefresh("memos");
|
||||||
}
|
}
|
||||||
|
if (method === "notifications/resources/updated") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const feed = document.getElementById("activity-feed");
|
const feed = document.getElementById("activity-feed");
|
||||||
if (feed) {
|
if (feed) {
|
||||||
if (feed.querySelector(".feed-entry") === null) {
|
if (feed.querySelector(".feed-entry") === null) {
|
||||||
@@ -861,8 +864,7 @@ function setupWS() {
|
|||||||
return;
|
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") {
|
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(data);
|
||||||
handleIncomingActivity(payload);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("WebSocket message parse error", e);
|
console.error("WebSocket message parse error", e);
|
||||||
|
|||||||
@@ -1083,6 +1083,11 @@ function handleIncomingActivity(payload: any): void {
|
|||||||
) {
|
) {
|
||||||
requestDomainRefresh("memos");
|
requestDomainRefresh("memos");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (method === "notifications/resources/updated") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const feed = document.getElementById("activity-feed");
|
const feed = document.getElementById("activity-feed");
|
||||||
if (feed) {
|
if (feed) {
|
||||||
if (feed.querySelector(".feed-entry") === null) {
|
if (feed.querySelector(".feed-entry") === null) {
|
||||||
@@ -1144,8 +1149,7 @@ function setupWS(): void {
|
|||||||
data.method === "notifications/task/completed" ||
|
data.method === "notifications/task/completed" ||
|
||||||
data.method === "notifications/resources/updated"
|
data.method === "notifications/resources/updated"
|
||||||
) {
|
) {
|
||||||
const payload = data.params || data.data || data;
|
handleIncomingActivity(data);
|
||||||
handleIncomingActivity(payload);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("WebSocket message parse error", 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(),
|
.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(),
|
_ => return name.to_string(),
|
||||||
};
|
};
|
||||||
if detail.is_empty() {
|
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