feat(KB-086): complete Step 3 — wire up event listeners for activity logging

This commit is contained in:
gsxdsm
2026-03-30 18:21:15 -07:00
parent 54d92dc62d
commit c47fcec1fc
2 changed files with 137 additions and 0 deletions

View File

@@ -2862,4 +2862,53 @@ describe("TaskStore", () => {
expect(logs[0].taskId).toBe("KB-001");
});
});
// ── Activity Log Event Listener Tests ────────────────────────────
describe("activity log event listeners", () => {
it("records activity on task:created", async () => {
const task = await store.createTask({ description: "Test created event" });
// Wait for async activity recording
await new Promise((r) => setTimeout(r, 10));
const logs = await store.getActivityLog({ type: "task:created" });
expect(logs.length).toBeGreaterThanOrEqual(1);
expect(logs[0].taskId).toBe(task.id);
expect(logs[0].type).toBe("task:created");
});
it("records activity on task:moved", async () => {
const task = await store.createTask({ description: "Test moved event" });
await store.moveTask(task.id, "todo");
// Wait for async activity recording
await new Promise((r) => setTimeout(r, 10));
const logs = await store.getActivityLog({ type: "task:moved" });
expect(logs.length).toBeGreaterThanOrEqual(1);
expect(logs[0].taskId).toBe(task.id);
expect(logs[0].type).toBe("task:moved");
expect(logs[0].metadata).toHaveProperty("from");
expect(logs[0].metadata).toHaveProperty("to");
});
it("records activity when task status becomes failed", async () => {
const task = await store.createTask({ description: "Test failure event" });
await store.moveTask(task.id, "todo");
await store.moveTask(task.id, "in-progress");
await store.updateTask(task.id, { status: "failed", error: "Something went wrong" });
// Wait for async activity recording
await new Promise((r) => setTimeout(r, 10));
const logs = await store.getActivityLog({ type: "task:failed" });
expect(logs.length).toBeGreaterThanOrEqual(1);
expect(logs[0].taskId).toBe(task.id);
expect(logs[0].type).toBe("task:failed");
});
it("records activity on settings:updated for important changes", async () => {
await store.updateSettings({ ntfyEnabled: true, ntfyTopic: "test-topic" });
// Wait for async activity recording
await new Promise((r) => setTimeout(r, 10));
const logs = await store.getActivityLog({ type: "settings:updated" });
expect(logs.length).toBeGreaterThanOrEqual(1);
expect(logs[0].type).toBe("settings:updated");
});
});
});

View File

@@ -53,6 +53,94 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
if (!existsSync(this.configPath)) {
await this.writeConfig({ nextId: 1 });
}
this.setupActivityLogListeners();
}
/**
* Set up event listeners for activity logging.
* Call after init() to record task lifecycle events.
*/
private setupActivityLogListeners(): void {
// Task created
this.on("task:created", (task) => {
this.recordActivity({
type: "task:created",
taskId: task.id,
taskTitle: task.title,
details: `Task ${task.id} created${task.title ? `: ${task.title}` : ""}`,
}).catch(() => {
// Best-effort: ignore recording errors
});
});
// Task moved
this.on("task:moved", (data) => {
this.recordActivity({
type: "task:moved",
taskId: data.task.id,
taskTitle: data.task.title,
details: `Task ${data.task.id} moved: ${data.from}${data.to}`,
metadata: { from: data.from, to: data.to },
}).catch(() => {
// Best-effort: ignore recording errors
});
});
// Task merged
this.on("task:merged", (result) => {
const status = result.merged ? "successfully merged" : "merge attempted";
this.recordActivity({
type: "task:merged",
taskId: result.task.id,
taskTitle: result.task.title,
details: `Task ${result.task.id} ${status} to main`,
metadata: { merged: result.merged, branch: result.branch },
}).catch(() => {
// Best-effort: ignore recording errors
});
});
// Task updated (check for failures)
this.on("task:updated", (task) => {
if (task.status === "failed") {
this.recordActivity({
type: "task:failed",
taskId: task.id,
taskTitle: task.title,
details: `Task ${task.id} failed${task.error ? `: ${task.error}` : ""}`,
metadata: task.error ? { error: task.error } : undefined,
}).catch(() => {
// Best-effort: ignore recording errors
});
}
});
// Settings updated (log important changes)
this.on("settings:updated", (data) => {
const importantChanges: string[] = [];
if (data.settings.ntfyEnabled !== data.previous.ntfyEnabled) {
importantChanges.push(`ntfy ${data.settings.ntfyEnabled ? "enabled" : "disabled"}`);
}
if (data.settings.ntfyTopic !== data.previous.ntfyTopic) {
importantChanges.push(`ntfy topic changed to ${data.settings.ntfyTopic}`);
}
if (data.settings.globalPause !== data.previous.globalPause) {
importantChanges.push(`global pause ${data.settings.globalPause ? "enabled" : "disabled"}`);
}
if (data.settings.enginePaused !== data.previous.enginePaused) {
importantChanges.push(`engine pause ${data.settings.enginePaused ? "enabled" : "disabled"}`);
}
if (importantChanges.length > 0) {
this.recordActivity({
type: "settings:updated",
details: `Settings updated: ${importantChanges.join(", ")}`,
metadata: { changes: importantChanges },
}).catch(() => {
// Best-effort: ignore recording errors
});
}
});
}
/**