feat(#37-41): Add domains, ideas, relationships, agents, widgets schema

Schema additions for issues #37-41:

New models:
- Domain (#37): Life domains (work, marriage, homelab, etc.)
- Idea (#38): Brain dumps with pgvector embeddings
- Relationship (#39): Generic entity linking (blocks, depends_on)
- Agent (#40): ClawdBot agent tracking with metrics
- AgentSession (#40): Conversation session tracking
- WidgetDefinition (#41): HUD widget registry
- UserLayout (#41): Per-user dashboard configuration

Updated models:
- Task, Event, Project: Added domainId foreign key
- User, Workspace: Added new relations

New enums:
- IdeaStatus: CAPTURED, PROCESSING, ACTIONABLE, ARCHIVED, DISCARDED
- RelationshipType: BLOCKS, BLOCKED_BY, DEPENDS_ON, etc.
- AgentStatus: IDLE, WORKING, WAITING, ERROR, TERMINATED
- EntityType: Added IDEA, DOMAIN

Migration: 20260129182803_add_domains_ideas_agents_widgets
This commit is contained in:
Jason Woltje
2026-01-29 12:29:21 -06:00
parent a220c2dc0a
commit 973502f26e
308 changed files with 18374 additions and 113 deletions

View File

@@ -0,0 +1,157 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import { AuthProvider, useAuth } from "./auth-context";
import type { AuthUser } from "@mosaic/shared";
// Mock the API client
vi.mock("../api/client", () => ({
apiGet: vi.fn(),
apiPost: vi.fn(),
}));
const { apiGet, apiPost } = await import("../api/client");
// Test component that uses the auth context
function TestComponent() {
const { user, isLoading, isAuthenticated, signOut } = useAuth();
if (isLoading) {
return <div>Loading...</div>;
}
return (
<div>
<div data-testid="auth-status">
{isAuthenticated ? "Authenticated" : "Not Authenticated"}
</div>
{user && (
<div>
<div data-testid="user-email">{user.email}</div>
<div data-testid="user-name">{user.name}</div>
</div>
)}
<button onClick={signOut}>Sign Out</button>
</div>
);
}
describe("AuthContext", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("should provide loading state initially", () => {
vi.mocked(apiGet).mockImplementation(
() => new Promise(() => {}) // Never resolves
);
render(
<AuthProvider>
<TestComponent />
</AuthProvider>
);
expect(screen.getByText("Loading...")).toBeInTheDocument();
});
it("should provide authenticated user when session exists", async () => {
const mockUser: AuthUser = {
id: "user-1",
email: "test@example.com",
name: "Test User",
};
vi.mocked(apiGet).mockResolvedValueOnce({
user: mockUser,
session: { id: "session-1", token: "token123", expiresAt: new Date() },
});
render(
<AuthProvider>
<TestComponent />
</AuthProvider>
);
await waitFor(() => {
expect(screen.getByTestId("auth-status")).toHaveTextContent(
"Authenticated"
);
});
expect(screen.getByTestId("user-email")).toHaveTextContent(
"test@example.com"
);
expect(screen.getByTestId("user-name")).toHaveTextContent("Test User");
});
it("should handle unauthenticated state when session check fails", async () => {
vi.mocked(apiGet).mockRejectedValueOnce(new Error("Unauthorized"));
render(
<AuthProvider>
<TestComponent />
</AuthProvider>
);
await waitFor(() => {
expect(screen.getByTestId("auth-status")).toHaveTextContent(
"Not Authenticated"
);
});
expect(screen.queryByTestId("user-email")).not.toBeInTheDocument();
});
it("should clear user on sign out", async () => {
const mockUser: AuthUser = {
id: "user-1",
email: "test@example.com",
name: "Test User",
};
vi.mocked(apiGet).mockResolvedValueOnce({
user: mockUser,
session: { id: "session-1", token: "token123", expiresAt: new Date() },
});
vi.mocked(apiPost).mockResolvedValueOnce({ success: true });
render(
<AuthProvider>
<TestComponent />
</AuthProvider>
);
// Wait for authenticated state
await waitFor(() => {
expect(screen.getByTestId("auth-status")).toHaveTextContent(
"Authenticated"
);
});
// Click sign out
const signOutButton = screen.getByRole("button", { name: "Sign Out" });
signOutButton.click();
await waitFor(() => {
expect(screen.getByTestId("auth-status")).toHaveTextContent(
"Not Authenticated"
);
});
expect(apiPost).toHaveBeenCalledWith("/auth/sign-out");
});
it("should throw error when useAuth is used outside AuthProvider", () => {
// Suppress console.error for this test
const consoleErrorSpy = vi
.spyOn(console, "error")
.mockImplementation(() => {});
expect(() => {
render(<TestComponent />);
}).toThrow("useAuth must be used within AuthProvider");
consoleErrorSpy.mockRestore();
});
});

View File

@@ -0,0 +1,74 @@
"use client";
import {
createContext,
useContext,
useState,
useEffect,
useCallback,
type ReactNode,
} from "react";
import type { AuthUser, AuthSession } from "@mosaic/shared";
import { apiGet, apiPost } from "../api/client";
interface AuthContextValue {
user: AuthUser | null;
isLoading: boolean;
isAuthenticated: boolean;
signOut: () => Promise<void>;
refreshSession: () => Promise<void>;
}
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<AuthUser | null>(null);
const [isLoading, setIsLoading] = useState(true);
const checkSession = useCallback(async () => {
try {
const session = await apiGet<AuthSession>("/auth/session");
setUser(session.user);
} catch (error) {
setUser(null);
} finally {
setIsLoading(false);
}
}, []);
const signOut = useCallback(async () => {
try {
await apiPost("/auth/sign-out");
} catch (error) {
console.error("Sign out error:", error);
} finally {
setUser(null);
}
}, []);
const refreshSession = useCallback(async () => {
await checkSession();
}, [checkSession]);
useEffect(() => {
checkSession();
}, [checkSession]);
const value: AuthContextValue = {
user,
isLoading,
isAuthenticated: user !== null,
signOut,
refreshSession,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth(): AuthContextValue {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error("useAuth must be used within AuthProvider");
}
return context;
}