Files
claw3d/tests/unit/standupStore.test.ts
T
Luke The Dev a997f13601 feat(kanban): Interactive Kanban board with real-time task tracking (#83)
* feat(kanban): add Kanban board with task-manager skill, modal UI, and desk clutter

Implement a full Kanban board system for tracking agent tasks:
- Add task-manager skill with shared JSON task store for persistence
- Render board as a floating modal over the live 3D office (not immersive)
- Auto-create tasks from actionable user messages with heuristic filtering
- Sync task status through OpenClaw agent lifecycle events
- Collapse task details panel by default, expand on card click
- Add dynamic desk clutter (papers, folders, etc.) reflecting active task count
- Exclude done tasks from desk clutter count
- Extract KANBAN_CLUTTER_OFFSET for easy positioning adjustment
- Add install flow with progress bar for the task-manager skill
- Include unit and e2e test coverage

Made-with: Cursor

* feat(kanban): production-harden task board with AI-free classification, resilient persistence, and modal UX

- Harden shared task store with atomic writes, payload size limits, and server-side enum validation
- Add client resilience: request timeouts (AbortController), exponential backoff retries, poll deduplication
- Implement optimistic UI with rollback on all card mutations (update, move, archive)
- Add modal accessibility: focus trap, Escape to close, aria-modal, keyboard card navigation
- Trust OpenClaw agent lifecycle phase=start as task classification signal instead of regex heuristics
- Keep regex heuristic only as lightweight filter for direct chat events (conversational noise)
- Expand verb recognition with typo tolerance and broader action vocabulary
- Create tasks from agent runs even when no chat event is received (external channel support)
- Merge dual header bars into single bar; reposition close button outside modal corner
- Exclude done tasks from desk clutter count; make clutter position configurable via KANBAN_CLUTTER_OFFSET
- Update default furniture layout to match user configuration
- Ensure kanban_board furniture persists in local storage across sessions
- Add comprehensive test coverage for store, API route, and controller logic

Made-with: Cursor

---------

Co-authored-by: iamlukethedev <lucas.guilherme@smartwayslfl.com>
2026-03-30 22:58:18 -05:00

113 lines
3.3 KiB
TypeScript

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { loadActiveStandupMeeting } from "@/lib/office/standup/store";
import type { StandupMeetingStore } from "@/lib/office/standup/types";
const makeTempDir = (name: string) => fs.mkdtempSync(path.join(os.tmpdir(), `${name}-`));
const writeStandupStore = (stateDir: string, store: StandupMeetingStore) => {
const storeDir = path.join(stateDir, "claw3d");
fs.mkdirSync(storeDir, { recursive: true });
fs.writeFileSync(
path.join(storeDir, "standup-store.json"),
JSON.stringify(store, null, 2),
"utf8"
);
};
describe("standup store", () => {
const priorStateDir = process.env.OPENCLAW_STATE_DIR;
let tempDir: string | null = null;
afterEach(() => {
process.env.OPENCLAW_STATE_DIR = priorStateDir;
if (tempDir) {
fs.rmSync(tempDir, { recursive: true, force: true });
tempDir = null;
}
});
it("drops stale active gathering meetings on load", () => {
tempDir = makeTempDir("standup-store-stale");
process.env.OPENCLAW_STATE_DIR = tempDir;
const staleIso = new Date(Date.now() - 60 * 60 * 1000).toISOString();
writeStandupStore(tempDir, {
activeMeeting: {
id: "meeting-stale",
trigger: "manual",
phase: "gathering",
scheduledFor: null,
startedAt: staleIso,
updatedAt: staleIso,
completedAt: null,
currentSpeakerAgentId: null,
speakerStartedAt: null,
speakerDurationMs: 8000,
participantOrder: ["main"],
arrivedAgentIds: ["main"],
cards: [],
},
lastMeeting: null,
});
expect(loadActiveStandupMeeting()).toBeNull();
});
it("keeps fresh active gatherings on load", () => {
tempDir = makeTempDir("standup-store-fresh");
process.env.OPENCLAW_STATE_DIR = tempDir;
const freshIso = new Date().toISOString();
writeStandupStore(tempDir, {
activeMeeting: {
id: "meeting-fresh",
trigger: "manual",
phase: "gathering",
scheduledFor: null,
startedAt: freshIso,
updatedAt: freshIso,
completedAt: null,
currentSpeakerAgentId: null,
speakerStartedAt: null,
speakerDurationMs: 8000,
participantOrder: ["main"],
arrivedAgentIds: [],
cards: [],
},
lastMeeting: null,
});
expect(loadActiveStandupMeeting()?.id).toBe("meeting-fresh");
});
it("drops stale gathering meetings even if arrivals refreshed updatedAt", () => {
tempDir = makeTempDir("standup-store-gathering-updated");
process.env.OPENCLAW_STATE_DIR = tempDir;
const staleStartedIso = new Date(Date.now() - 10 * 60 * 1000).toISOString();
const freshUpdatedIso = new Date().toISOString();
writeStandupStore(tempDir, {
activeMeeting: {
id: "meeting-gathering-stale",
trigger: "manual",
phase: "gathering",
scheduledFor: null,
startedAt: staleStartedIso,
updatedAt: freshUpdatedIso,
completedAt: null,
currentSpeakerAgentId: null,
speakerStartedAt: null,
speakerDurationMs: 8000,
participantOrder: ["main"],
arrivedAgentIds: ["main"],
cards: [],
},
lastMeeting: null,
});
expect(loadActiveStandupMeeting()).toBeNull();
});
});