@polderlabs/bizar 4.5.2 → 4.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bizar-dash/dist/assets/main-DHZmbnxQ.js +361 -0
- package/bizar-dash/dist/assets/main-DHZmbnxQ.js.map +1 -0
- package/bizar-dash/dist/assets/main-DX_Jh8Wc.css +1 -0
- package/bizar-dash/dist/assets/{mobile-lbH6szyX.js → mobile-BK8-ythT.js} +18 -18
- package/bizar-dash/dist/assets/mobile-BK8-ythT.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-BRhoDOUz.js → mobile-Chvf9u_B.js} +1 -1
- package/bizar-dash/dist/assets/{mobile-BRhoDOUz.js.map → mobile-Chvf9u_B.js.map} +1 -1
- package/bizar-dash/dist/index.html +3 -3
- package/bizar-dash/dist/mobile.html +2 -2
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/bizar-dash/skills/publishing/SKILL.md +146 -0
- package/bizar-dash/src/server/api.mjs +8 -0
- package/bizar-dash/src/server/backup-store.mjs +525 -0
- package/bizar-dash/src/server/digest-store.mjs +558 -0
- package/bizar-dash/src/server/lib/rate-limit.mjs +122 -0
- package/bizar-dash/src/server/logger.mjs +71 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +45 -6
- package/bizar-dash/src/server/metrics.mjs +193 -0
- package/bizar-dash/src/server/routes/backup.mjs +112 -0
- package/bizar-dash/src/server/routes/chat.mjs +20 -3
- package/bizar-dash/src/server/routes/digests.mjs +82 -0
- package/bizar-dash/src/server/routes/lightrag.mjs +3 -2
- package/bizar-dash/src/server/routes/memory.mjs +5 -4
- package/bizar-dash/src/server/routes/misc.mjs +2 -1
- package/bizar-dash/src/server/routes/overview.mjs +2 -1
- package/bizar-dash/src/server/routes-v2/events.mjs +14 -0
- package/bizar-dash/src/server/schedules-runner.mjs +126 -0
- package/bizar-dash/src/server/server.mjs +79 -0
- package/bizar-dash/src/web/App.tsx +8 -1
- package/bizar-dash/src/web/components/BackupRestore.tsx +330 -0
- package/bizar-dash/src/web/components/SearchModal.tsx +6 -3
- package/bizar-dash/src/web/components/VirtualList.tsx +53 -0
- package/bizar-dash/src/web/components/chat/ChatThread.tsx +17 -11
- package/bizar-dash/src/web/components/chat/Composer.tsx +2 -0
- package/bizar-dash/src/web/hooks/useI18n.ts +13 -0
- package/bizar-dash/src/web/lib/i18n.ts +25 -0
- package/bizar-dash/src/web/locales/en.json +52 -0
- package/bizar-dash/src/web/main.tsx +5 -0
- package/bizar-dash/src/web/styles/main.css +70 -8
- package/bizar-dash/src/web/views/Activity.tsx +35 -18
- package/bizar-dash/src/web/views/Agents.tsx +57 -42
- package/bizar-dash/src/web/views/Artifacts.tsx +38 -25
- package/bizar-dash/src/web/views/Chat.tsx +8 -0
- package/bizar-dash/src/web/views/History.tsx +94 -76
- package/bizar-dash/src/web/views/MiniMaxUsage.tsx +21 -4
- package/bizar-dash/src/web/views/Mods.tsx +30 -17
- package/bizar-dash/src/web/views/Overview.tsx +19 -9
- package/bizar-dash/src/web/views/Providers.tsx +16 -16
- package/bizar-dash/src/web/views/Schedules.tsx +33 -15
- package/bizar-dash/src/web/views/Settings.tsx +97 -1745
- package/bizar-dash/src/web/views/Skills.tsx +4 -1
- package/bizar-dash/src/web/views/Tasks.tsx +11 -2
- package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +8 -2
- package/bizar-dash/src/web/views/memory/LightragPanel.tsx +3 -0
- package/bizar-dash/src/web/views/memory/ObsidianPanel.tsx +12 -4
- package/bizar-dash/src/web/views/memory/SemanticSearchPanel.tsx +3 -0
- package/bizar-dash/src/web/views/settings/ActivitySection.tsx +205 -0
- package/bizar-dash/src/web/views/settings/AgentSection.tsx +294 -0
- package/bizar-dash/src/web/views/settings/AuthSection.tsx +159 -0
- package/bizar-dash/src/web/views/settings/BackupSection.tsx +16 -0
- package/bizar-dash/src/web/views/settings/EnvVarsSection.tsx +16 -0
- package/bizar-dash/src/web/views/settings/GeneralSection.tsx +105 -0
- package/bizar-dash/src/web/views/settings/HeadroomSection.tsx +39 -0
- package/bizar-dash/src/web/views/settings/MemorySection.tsx +16 -0
- package/bizar-dash/src/web/views/settings/NetworkSection.tsx +87 -0
- package/bizar-dash/src/web/views/settings/NotificationsSection.tsx +34 -0
- package/bizar-dash/src/web/views/settings/ProvidersSection.tsx +16 -0
- package/bizar-dash/src/web/views/settings/SkillsSection.tsx +16 -0
- package/bizar-dash/src/web/views/settings/SystemLlmSection.tsx +81 -0
- package/bizar-dash/src/web/views/settings/ThemeSection.tsx +168 -0
- package/bizar-dash/src/web/views/settings/UpdatesSection.tsx +256 -0
- package/bizar-dash/tests/a11y.test.tsx +206 -0
- package/bizar-dash/tests/backup-restore.test.mjs +217 -0
- package/bizar-dash/tests/backup-restore.test.tsx +123 -0
- package/bizar-dash/tests/backup-store.test.mjs +300 -0
- package/bizar-dash/tests/cli-bugfixes.test.mjs +4 -4
- package/bizar-dash/tests/cli-error-visibility.test.mjs +153 -0
- package/bizar-dash/tests/cli-refactor.test.mjs +184 -0
- package/bizar-dash/tests/components/Button.test.tsx +41 -0
- package/bizar-dash/tests/components/Card.test.tsx +42 -0
- package/bizar-dash/tests/components/Modal.test.tsx +104 -0
- package/bizar-dash/tests/components/Spinner.test.tsx +32 -0
- package/bizar-dash/tests/components/StatusBadge.test.tsx +35 -0
- package/bizar-dash/tests/components/Toast.test.tsx +108 -0
- package/bizar-dash/tests/digest-generation.test.mjs +191 -0
- package/bizar-dash/tests/digest-store.test.mjs +264 -0
- package/bizar-dash/tests/hooks/useModal.test.tsx +84 -0
- package/bizar-dash/tests/hooks/useToast.test.tsx +50 -0
- package/bizar-dash/tests/lib/i18n.test.ts +46 -0
- package/bizar-dash/tests/lib/utils.test.ts +194 -0
- package/bizar-dash/tests/logger.test.mjs +207 -0
- package/bizar-dash/tests/metrics.test.mjs +183 -0
- package/bizar-dash/tests/rate-limit.test.mjs +298 -0
- package/bizar-dash/tests/server-bugfixes.test.mjs +2 -2
- package/bizar-dash/tests/setup.ts +7 -0
- package/bizar-dash/vitest.config.ts +13 -0
- package/cli/artifact-cli.mjs +605 -0
- package/cli/artifact-render.mjs +621 -0
- package/cli/artifact-server.mjs +847 -0
- package/cli/artifact.mjs +38 -2096
- package/cli/bg.mjs +5 -13
- package/cli/bin.mjs +221 -1350
- package/cli/commands/artifact.mjs +20 -0
- package/cli/commands/dash.mjs +160 -0
- package/cli/commands/headroom.mjs +204 -0
- package/cli/commands/install.mjs +169 -0
- package/cli/commands/memory.mjs +25 -0
- package/cli/commands/minimax.mjs +285 -0
- package/cli/commands/mod.mjs +185 -0
- package/cli/commands/service.mjs +65 -0
- package/cli/commands/usage.mjs +109 -0
- package/cli/commands/util.mjs +459 -0
- package/cli/digest.mjs +149 -0
- package/cli/doctor.mjs +1 -13
- package/cli/provision.mjs +2 -13
- package/cli/service-controller.mjs +1 -11
- package/cli/service.mjs +1 -11
- package/cli/utils.mjs +41 -1
- package/package.json +6 -1
- package/bizar-dash/dist/assets/main-B4OfGAwz.js +0 -361
- package/bizar-dash/dist/assets/main-B4OfGAwz.js.map +0 -1
- package/bizar-dash/dist/assets/main-DAlLdW8I.css +0 -1
- package/bizar-dash/dist/assets/mobile-lbH6szyX.js.map +0 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
4
|
+
import { Button } from '../../src/web/components/Button';
|
|
5
|
+
|
|
6
|
+
describe('Button', () => {
|
|
7
|
+
it('renders text', () => {
|
|
8
|
+
render(<Button>Click me</Button>);
|
|
9
|
+
expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('fires onClick when clicked', async () => {
|
|
13
|
+
const onClick = vi.fn();
|
|
14
|
+
const user = userEvent.setup();
|
|
15
|
+
render(<Button onClick={onClick}>Click</Button>);
|
|
16
|
+
await user.click(screen.getByRole('button'));
|
|
17
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('is disabled when disabled prop is set', () => {
|
|
21
|
+
render(<Button disabled>Disabled</Button>);
|
|
22
|
+
expect(screen.getByRole('button')).toBeDisabled();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('shows loading state and disables button', () => {
|
|
26
|
+
render(<Button loading>Saving</Button>);
|
|
27
|
+
const btn = screen.getByRole('button');
|
|
28
|
+
expect(btn).toBeDisabled();
|
|
29
|
+
expect(btn.querySelector('.btn-spinner')).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('applies variant classes', () => {
|
|
33
|
+
const { container } = render(<Button variant="primary">Primary</Button>);
|
|
34
|
+
expect(container.firstChild).toHaveClass('btn-primary');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('applies size classes', () => {
|
|
38
|
+
const { container } = render(<Button size="lg">Large</Button>);
|
|
39
|
+
expect(container.firstChild).toHaveClass('btn-size-lg');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import { Card, CardTitle, CardMeta } from '../../src/web/components/Card';
|
|
4
|
+
|
|
5
|
+
describe('Card', () => {
|
|
6
|
+
it('renders children', () => {
|
|
7
|
+
render(<Card>Hello</Card>);
|
|
8
|
+
expect(screen.getByText('Hello')).toBeInTheDocument();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('has expected class name', () => {
|
|
12
|
+
const { container } = render(<Card>Test</Card>);
|
|
13
|
+
expect(container.firstChild).toHaveClass('card');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('applies variant classes', () => {
|
|
17
|
+
const { container, rerender } = render(<Card variant="elevated">Test</Card>);
|
|
18
|
+
expect(container.firstChild).toHaveClass('card-elevated');
|
|
19
|
+
|
|
20
|
+
rerender(<Card variant="outlined">Test</Card>);
|
|
21
|
+
expect(container.firstChild).toHaveClass('card-outlined');
|
|
22
|
+
|
|
23
|
+
rerender(<Card variant="filled">Test</Card>);
|
|
24
|
+
expect(container.firstChild).toHaveClass('card-filled');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('renders CardTitle and CardMeta', () => {
|
|
28
|
+
render(
|
|
29
|
+
<Card>
|
|
30
|
+
<CardTitle>My Title</CardTitle>
|
|
31
|
+
<CardMeta>Meta info</CardMeta>
|
|
32
|
+
</Card>,
|
|
33
|
+
);
|
|
34
|
+
expect(screen.getByText('My Title')).toBeInTheDocument();
|
|
35
|
+
expect(screen.getByText('Meta info')).toBeInTheDocument();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('sets interactive class when interactive prop is true', () => {
|
|
39
|
+
const { container } = render(<Card interactive>Clickable</Card>);
|
|
40
|
+
expect(container.firstChild).toHaveClass('card-interactive');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
4
|
+
import { ModalProvider, useModal } from '../../src/web/components/Modal';
|
|
5
|
+
|
|
6
|
+
function ModalHarness() {
|
|
7
|
+
const modal = useModal();
|
|
8
|
+
return (
|
|
9
|
+
<div>
|
|
10
|
+
<button onClick={() => modal.open({ title: 'Dialog', children: <p>Content</p> })}>
|
|
11
|
+
Open
|
|
12
|
+
</button>
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ModalHarnessWithClose() {
|
|
18
|
+
const modal = useModal();
|
|
19
|
+
return (
|
|
20
|
+
<div>
|
|
21
|
+
<button
|
|
22
|
+
onClick={() =>
|
|
23
|
+
modal.open({
|
|
24
|
+
title: 'Dialog',
|
|
25
|
+
children: <p>Content</p>,
|
|
26
|
+
footer: <button onClick={() => modal.close()}>Close</button>,
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
>
|
|
30
|
+
Open
|
|
31
|
+
</button>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('Modal', () => {
|
|
37
|
+
it('renders when open', async () => {
|
|
38
|
+
const user = userEvent.setup();
|
|
39
|
+
render(
|
|
40
|
+
<ModalProvider>
|
|
41
|
+
<ModalHarness />
|
|
42
|
+
</ModalProvider>,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
await user.click(screen.getByText('Open'));
|
|
46
|
+
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
|
47
|
+
expect(screen.getByText('Dialog')).toBeInTheDocument();
|
|
48
|
+
expect(screen.getByText('Content')).toBeInTheDocument();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('does not render dialog when closed', () => {
|
|
52
|
+
render(
|
|
53
|
+
<ModalProvider>
|
|
54
|
+
<ModalHarness />
|
|
55
|
+
</ModalProvider>,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('has role="dialog"', async () => {
|
|
62
|
+
const user = userEvent.setup();
|
|
63
|
+
render(
|
|
64
|
+
<ModalProvider>
|
|
65
|
+
<ModalHarness />
|
|
66
|
+
</ModalProvider>,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
await user.click(screen.getByText('Open'));
|
|
70
|
+
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('closes on Escape key', async () => {
|
|
74
|
+
const user = userEvent.setup();
|
|
75
|
+
render(
|
|
76
|
+
<ModalProvider>
|
|
77
|
+
<ModalHarness />
|
|
78
|
+
</ModalProvider>,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
await user.click(screen.getByText('Open'));
|
|
82
|
+
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
|
83
|
+
|
|
84
|
+
await user.keyboard('{Escape}');
|
|
85
|
+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('closes when clicking the backdrop', async () => {
|
|
89
|
+
const user = userEvent.setup();
|
|
90
|
+
render(
|
|
91
|
+
<ModalProvider>
|
|
92
|
+
<ModalHarness />
|
|
93
|
+
</ModalProvider>,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
await user.click(screen.getByText('Open'));
|
|
97
|
+
const dialog = screen.getByRole('dialog');
|
|
98
|
+
expect(dialog).toBeInTheDocument();
|
|
99
|
+
|
|
100
|
+
const backdrop = dialog.parentElement!;
|
|
101
|
+
await user.click(backdrop);
|
|
102
|
+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import { Spinner } from '../../src/web/components/Spinner';
|
|
4
|
+
|
|
5
|
+
describe('Spinner', () => {
|
|
6
|
+
it('renders with default size', () => {
|
|
7
|
+
const { container } = render(<Spinner />);
|
|
8
|
+
const spinner = container.firstChild;
|
|
9
|
+
expect(spinner).toHaveClass('spinner');
|
|
10
|
+
expect(spinner).toHaveClass('spinner-md');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('renders with custom size', () => {
|
|
14
|
+
const { container } = render(<Spinner size="lg" />);
|
|
15
|
+
expect(container.firstChild).toHaveClass('spinner-lg');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('has role="status" for accessibility', () => {
|
|
19
|
+
render(<Spinner />);
|
|
20
|
+
expect(screen.getByRole('status')).toBeInTheDocument();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('has default aria-label', () => {
|
|
24
|
+
render(<Spinner />);
|
|
25
|
+
expect(screen.getByRole('status')).toHaveAttribute('aria-label', 'Loading');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('accepts a custom aria-label', () => {
|
|
29
|
+
render(<Spinner label="Please wait" />);
|
|
30
|
+
expect(screen.getByRole('status')).toHaveAttribute('aria-label', 'Please wait');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import { StatusBadge } from '../../src/web/components/StatusBadge';
|
|
4
|
+
|
|
5
|
+
describe('StatusBadge', () => {
|
|
6
|
+
it('renders with default neutral kind', () => {
|
|
7
|
+
const { container } = render(<StatusBadge>Default</StatusBadge>);
|
|
8
|
+
expect(container.firstChild).toHaveClass('badge');
|
|
9
|
+
expect(container.firstChild).toHaveClass('badge-neutral');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('renders with each variant class', () => {
|
|
13
|
+
const variants = ['success', 'warning', 'error', 'info', 'accent'] as const;
|
|
14
|
+
for (const kind of variants) {
|
|
15
|
+
const { container, unmount } = render(<StatusBadge kind={kind}>{kind}</StatusBadge>);
|
|
16
|
+
expect(container.firstChild).toHaveClass(`badge-${kind}`);
|
|
17
|
+
unmount();
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('renders children', () => {
|
|
22
|
+
render(<StatusBadge>Active</StatusBadge>);
|
|
23
|
+
expect(screen.getByText('Active')).toBeInTheDocument();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('shows dot element when dot prop is true', () => {
|
|
27
|
+
const { container } = render(<StatusBadge dot>With dot</StatusBadge>);
|
|
28
|
+
expect(container.querySelector('.badge-dot')).toBeInTheDocument();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('does not show dot when dot prop is false', () => {
|
|
32
|
+
const { container } = render(<StatusBadge>No dot</StatusBadge>);
|
|
33
|
+
expect(container.querySelector('.badge-dot')).not.toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { render, screen, act, fireEvent } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
4
|
+
import { ToastContainer, ToastProvider, useToast } from '../../src/web/components/Toast';
|
|
5
|
+
|
|
6
|
+
function ToastHarness() {
|
|
7
|
+
const toast = useToast();
|
|
8
|
+
return (
|
|
9
|
+
<div>
|
|
10
|
+
<button onClick={() => toast.show('Info message')}>Show Info</button>
|
|
11
|
+
<button onClick={() => toast.success('Success!')}>Show Success</button>
|
|
12
|
+
<button onClick={() => toast.error('Error!')}>Show Error</button>
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('ToastContainer', () => {
|
|
18
|
+
it('renders toast with message', () => {
|
|
19
|
+
render(
|
|
20
|
+
<ToastContainer
|
|
21
|
+
toasts={[{ id: 1, kind: 'info', message: 'Test toast' }]}
|
|
22
|
+
onDismiss={() => {}}
|
|
23
|
+
/>,
|
|
24
|
+
);
|
|
25
|
+
expect(screen.getByText('Test toast')).toBeInTheDocument();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('has role="alert" and aria-live="assertive"', () => {
|
|
29
|
+
render(
|
|
30
|
+
<ToastContainer
|
|
31
|
+
toasts={[{ id: 1, kind: 'info', message: 'Accessible toast' }]}
|
|
32
|
+
onDismiss={() => {}}
|
|
33
|
+
/>,
|
|
34
|
+
);
|
|
35
|
+
const toast = screen.getByRole('alert');
|
|
36
|
+
expect(toast).toBeInTheDocument();
|
|
37
|
+
expect(toast).toHaveAttribute('aria-live', 'assertive');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('ToastProvider + useToast', () => {
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
vi.useRealTimers();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('displays a toast when show is called', async () => {
|
|
47
|
+
const user = userEvent.setup();
|
|
48
|
+
render(
|
|
49
|
+
<ToastProvider>
|
|
50
|
+
<ToastHarness />
|
|
51
|
+
</ToastProvider>,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
await user.click(screen.getByText('Show Info'));
|
|
55
|
+
expect(screen.getByText('Info message')).toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('displays multiple toasts stacked', async () => {
|
|
59
|
+
const user = userEvent.setup();
|
|
60
|
+
render(
|
|
61
|
+
<ToastProvider>
|
|
62
|
+
<ToastHarness />
|
|
63
|
+
</ToastProvider>,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
await user.click(screen.getByText('Show Success'));
|
|
67
|
+
await user.click(screen.getByText('Show Error'));
|
|
68
|
+
|
|
69
|
+
expect(screen.getByText('Success!')).toBeInTheDocument();
|
|
70
|
+
expect(screen.getByText('Error!')).toBeInTheDocument();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('auto-dismisses after the default timeout', () => {
|
|
74
|
+
// Use fireEvent (synchronous) instead of userEvent to avoid
|
|
75
|
+
// timer conflicts with vi.useFakeTimers
|
|
76
|
+
vi.useFakeTimers();
|
|
77
|
+
|
|
78
|
+
render(
|
|
79
|
+
<ToastProvider>
|
|
80
|
+
<ToastHarness />
|
|
81
|
+
</ToastProvider>,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
fireEvent.click(screen.getByText('Show Info'));
|
|
85
|
+
expect(screen.getByText('Info message')).toBeInTheDocument();
|
|
86
|
+
|
|
87
|
+
act(() => {
|
|
88
|
+
vi.advanceTimersByTime(4000);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(screen.queryByText('Info message')).not.toBeInTheDocument();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('dismisses a toast when dismiss button is clicked', async () => {
|
|
95
|
+
const user = userEvent.setup();
|
|
96
|
+
render(
|
|
97
|
+
<ToastProvider>
|
|
98
|
+
<ToastHarness />
|
|
99
|
+
</ToastProvider>,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
await user.click(screen.getByText('Show Info'));
|
|
103
|
+
expect(screen.getByText('Info message')).toBeInTheDocument();
|
|
104
|
+
|
|
105
|
+
await user.click(screen.getByLabelText('Dismiss'));
|
|
106
|
+
expect(screen.queryByText('Info message')).not.toBeInTheDocument();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tests/digest-generation.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* v4.8.0 — End-to-end tests for digest generation.
|
|
5
|
+
*
|
|
6
|
+
* Verifies:
|
|
7
|
+
* - Creating tasks then generating a digest includes task info
|
|
8
|
+
* - dryRun does not save any files
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
12
|
+
import assert from 'node:assert';
|
|
13
|
+
import { tmpdir } from 'node:os';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { mkdirSync, rmSync, existsSync, writeFileSync } from 'node:fs';
|
|
16
|
+
|
|
17
|
+
// Point stores to temp directories
|
|
18
|
+
const storeHome = join(tmpdir(), `bizar-digest-gen-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
19
|
+
process.env.BIZAR_STORE_HOME = storeHome;
|
|
20
|
+
|
|
21
|
+
const DIGEST_STORE = await import('../src/server/digest-store.mjs');
|
|
22
|
+
|
|
23
|
+
// We import tasksStore to create tasks that should appear in the digest
|
|
24
|
+
let tasksStore;
|
|
25
|
+
|
|
26
|
+
describe('digest-generation', () => {
|
|
27
|
+
beforeEach(async () => {
|
|
28
|
+
mkdirSync(storeHome, { recursive: true });
|
|
29
|
+
|
|
30
|
+
// Create the project's .obsidian directory with some notes
|
|
31
|
+
const obsidianDir = join(storeHome, '.obsidian');
|
|
32
|
+
const dailyDir = join(obsidianDir, 'daily');
|
|
33
|
+
const decisionsDir = join(obsidianDir, 'decisions');
|
|
34
|
+
mkdirSync(dailyDir, { recursive: true });
|
|
35
|
+
mkdirSync(decisionsDir, { recursive: true });
|
|
36
|
+
|
|
37
|
+
// Write some memory notes "within the week"
|
|
38
|
+
writeFileSync(join(dailyDir, '2026-07-01.md'),
|
|
39
|
+
'---\ntitle: "Daily Note Jul 1"\n---\n\nWorked on authentication flow.');
|
|
40
|
+
writeFileSync(join(decisionsDir, 'use-sqlite.md'),
|
|
41
|
+
'---\ntitle: "Use SQLite"\n---\n\nDecision to use SQLite for local storage.');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
try { rmSync(storeHome, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
46
|
+
DIGEST_STORE.__resetStoreForTests();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// ── Basic generation ──────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
describe('basic generation', () => {
|
|
52
|
+
it('generates markdown with all expected sections', async () => {
|
|
53
|
+
const result = await DIGEST_STORE.generateWeeklyDigest({
|
|
54
|
+
weekStart: '2026-06-28',
|
|
55
|
+
weekEnd: '2026-07-04',
|
|
56
|
+
projectRoot: storeHome,
|
|
57
|
+
dryRun: true,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
assert.ok(result.markdown);
|
|
61
|
+
// Check frontmatter
|
|
62
|
+
assert.ok(result.markdown.includes('type: digest'));
|
|
63
|
+
assert.ok(result.markdown.includes('weekStart: 2026-06-28'));
|
|
64
|
+
assert.ok(result.markdown.includes('weekEnd: 2026-07-04'));
|
|
65
|
+
|
|
66
|
+
// Check all section headers present
|
|
67
|
+
const sections = [
|
|
68
|
+
'Tasks completed',
|
|
69
|
+
'Tasks created',
|
|
70
|
+
'Memory notes written',
|
|
71
|
+
'Chat sessions',
|
|
72
|
+
'Schedules fired',
|
|
73
|
+
'Background agents completed',
|
|
74
|
+
'Token usage',
|
|
75
|
+
];
|
|
76
|
+
for (const s of sections) {
|
|
77
|
+
assert.ok(result.markdown.includes(`## ${s}`), `section "${s}" should have a header`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check footer
|
|
81
|
+
assert.ok(result.markdown.includes('BizarHarness v4.8.0'));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('memory-writes section is populated from vault state', async () => {
|
|
85
|
+
// Use a week range that includes "right now" so the notes written
|
|
86
|
+
// in beforeEach (which have the current mtime) are picked up.
|
|
87
|
+
const now = new Date();
|
|
88
|
+
const weekStart = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000)
|
|
89
|
+
.toISOString().slice(0, 10);
|
|
90
|
+
const weekEnd = new Date(now.getTime() + 1 * 24 * 60 * 60 * 1000)
|
|
91
|
+
.toISOString().slice(0, 10);
|
|
92
|
+
|
|
93
|
+
const result = await DIGEST_STORE.generateWeeklyDigest({
|
|
94
|
+
weekStart,
|
|
95
|
+
weekEnd,
|
|
96
|
+
projectRoot: storeHome,
|
|
97
|
+
dryRun: true,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// memory-writes section should contain our test notes (since they
|
|
101
|
+
// were just created and fall within the dynamic date range)
|
|
102
|
+
const memorySection = result.sections['memory-writes'];
|
|
103
|
+
assert.ok(Array.isArray(memorySection), 'memory-writes should be an array');
|
|
104
|
+
assert.ok(memorySection.length > 0, 'should include at least one memory note');
|
|
105
|
+
// At least one note should reference the files we created
|
|
106
|
+
const hasDaily = memorySection.some((n) => n.relPath.includes('daily'));
|
|
107
|
+
const hasDecisions = memorySection.some((n) => n.relPath.includes('decisions'));
|
|
108
|
+
assert.ok(hasDaily || hasDecisions, 'should include notes from our test vault');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// ── dryRun behavior ───────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
describe('dryRun', () => {
|
|
115
|
+
it('does not save files when dryRun is true', async () => {
|
|
116
|
+
const result = await DIGEST_STORE.generateAndSave({
|
|
117
|
+
weekStart: '2026-06-28',
|
|
118
|
+
weekEnd: '2026-07-04',
|
|
119
|
+
projectRoot: storeHome,
|
|
120
|
+
dryRun: true,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// saveResult should not be present
|
|
124
|
+
assert.strictEqual(result.saveResult, undefined, 'saveResult should be undefined in dry run');
|
|
125
|
+
|
|
126
|
+
// No digest files should exist
|
|
127
|
+
const prefix = 'weekly-';
|
|
128
|
+
const digestsDir = join(storeHome, 'digests');
|
|
129
|
+
if (existsSync(digestsDir)) {
|
|
130
|
+
const { readdirSync } = await import('node:fs');
|
|
131
|
+
const files = readdirSync(digestsDir);
|
|
132
|
+
const digestFiles = files.filter((f) => f.startsWith(prefix));
|
|
133
|
+
assert.strictEqual(digestFiles.length, 0, 'no digest files should exist after dry run');
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('saves files when dryRun is false', async () => {
|
|
138
|
+
const result = await DIGEST_STORE.generateAndSave({
|
|
139
|
+
weekStart: '2026-06-21',
|
|
140
|
+
weekEnd: '2026-06-27',
|
|
141
|
+
projectRoot: storeHome,
|
|
142
|
+
dryRun: false,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
assert.ok(result.saveResult, 'saveResult should be present');
|
|
146
|
+
assert.ok(result.saveResult.ok);
|
|
147
|
+
assert.ok(result.saveResult.paths.length >= 1);
|
|
148
|
+
|
|
149
|
+
// File should exist on disk
|
|
150
|
+
for (const p of result.saveResult.paths) {
|
|
151
|
+
assert.ok(existsSync(p), `digest file should exist: ${p}`);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// ── Markdown format ───────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
describe('markdown format', () => {
|
|
159
|
+
it('produces parseable frontmatter', async () => {
|
|
160
|
+
const result = await DIGEST_STORE.generateWeeklyDigest({
|
|
161
|
+
weekStart: '2026-06-28',
|
|
162
|
+
weekEnd: '2026-07-04',
|
|
163
|
+
projectRoot: storeHome,
|
|
164
|
+
dryRun: true,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Extract frontmatter between --- markers
|
|
168
|
+
const lines = result.markdown.split('\n');
|
|
169
|
+
assert.strictEqual(lines[0], '---', 'should start with frontmatter delimiter');
|
|
170
|
+
const endIdx = lines.indexOf('---', 1);
|
|
171
|
+
assert.ok(endIdx > 1, 'should have closing frontmatter delimiter');
|
|
172
|
+
|
|
173
|
+
const fm = lines.slice(1, endIdx);
|
|
174
|
+
const titleLine = fm.find((l) => l.startsWith('title:'));
|
|
175
|
+
assert.ok(titleLine, 'frontmatter should have title');
|
|
176
|
+
assert.ok(titleLine.includes('Weekly Digest'), 'title should mention Weekly Digest');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('has a readable human date in the header', async () => {
|
|
180
|
+
const result = await DIGEST_STORE.generateWeeklyDigest({
|
|
181
|
+
weekStart: '2026-06-28',
|
|
182
|
+
weekEnd: '2026-07-04',
|
|
183
|
+
projectRoot: storeHome,
|
|
184
|
+
dryRun: true,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// The markdown should have a **Week:** line
|
|
188
|
+
assert.ok(result.markdown.includes('**Week:**'), 'should have week header');
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
});
|