@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.
Files changed (123) hide show
  1. package/bizar-dash/dist/assets/main-DHZmbnxQ.js +361 -0
  2. package/bizar-dash/dist/assets/main-DHZmbnxQ.js.map +1 -0
  3. package/bizar-dash/dist/assets/main-DX_Jh8Wc.css +1 -0
  4. package/bizar-dash/dist/assets/{mobile-lbH6szyX.js → mobile-BK8-ythT.js} +18 -18
  5. package/bizar-dash/dist/assets/mobile-BK8-ythT.js.map +1 -0
  6. package/bizar-dash/dist/assets/{mobile-BRhoDOUz.js → mobile-Chvf9u_B.js} +1 -1
  7. package/bizar-dash/dist/assets/{mobile-BRhoDOUz.js.map → mobile-Chvf9u_B.js.map} +1 -1
  8. package/bizar-dash/dist/index.html +3 -3
  9. package/bizar-dash/dist/mobile.html +2 -2
  10. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  11. package/bizar-dash/skills/publishing/SKILL.md +146 -0
  12. package/bizar-dash/src/server/api.mjs +8 -0
  13. package/bizar-dash/src/server/backup-store.mjs +525 -0
  14. package/bizar-dash/src/server/digest-store.mjs +558 -0
  15. package/bizar-dash/src/server/lib/rate-limit.mjs +122 -0
  16. package/bizar-dash/src/server/logger.mjs +71 -0
  17. package/bizar-dash/src/server/memory-lightrag.mjs +45 -6
  18. package/bizar-dash/src/server/metrics.mjs +193 -0
  19. package/bizar-dash/src/server/routes/backup.mjs +112 -0
  20. package/bizar-dash/src/server/routes/chat.mjs +20 -3
  21. package/bizar-dash/src/server/routes/digests.mjs +82 -0
  22. package/bizar-dash/src/server/routes/lightrag.mjs +3 -2
  23. package/bizar-dash/src/server/routes/memory.mjs +5 -4
  24. package/bizar-dash/src/server/routes/misc.mjs +2 -1
  25. package/bizar-dash/src/server/routes/overview.mjs +2 -1
  26. package/bizar-dash/src/server/routes-v2/events.mjs +14 -0
  27. package/bizar-dash/src/server/schedules-runner.mjs +126 -0
  28. package/bizar-dash/src/server/server.mjs +79 -0
  29. package/bizar-dash/src/web/App.tsx +8 -1
  30. package/bizar-dash/src/web/components/BackupRestore.tsx +330 -0
  31. package/bizar-dash/src/web/components/SearchModal.tsx +6 -3
  32. package/bizar-dash/src/web/components/VirtualList.tsx +53 -0
  33. package/bizar-dash/src/web/components/chat/ChatThread.tsx +17 -11
  34. package/bizar-dash/src/web/components/chat/Composer.tsx +2 -0
  35. package/bizar-dash/src/web/hooks/useI18n.ts +13 -0
  36. package/bizar-dash/src/web/lib/i18n.ts +25 -0
  37. package/bizar-dash/src/web/locales/en.json +52 -0
  38. package/bizar-dash/src/web/main.tsx +5 -0
  39. package/bizar-dash/src/web/styles/main.css +70 -8
  40. package/bizar-dash/src/web/views/Activity.tsx +35 -18
  41. package/bizar-dash/src/web/views/Agents.tsx +57 -42
  42. package/bizar-dash/src/web/views/Artifacts.tsx +38 -25
  43. package/bizar-dash/src/web/views/Chat.tsx +8 -0
  44. package/bizar-dash/src/web/views/History.tsx +94 -76
  45. package/bizar-dash/src/web/views/MiniMaxUsage.tsx +21 -4
  46. package/bizar-dash/src/web/views/Mods.tsx +30 -17
  47. package/bizar-dash/src/web/views/Overview.tsx +19 -9
  48. package/bizar-dash/src/web/views/Providers.tsx +16 -16
  49. package/bizar-dash/src/web/views/Schedules.tsx +33 -15
  50. package/bizar-dash/src/web/views/Settings.tsx +97 -1745
  51. package/bizar-dash/src/web/views/Skills.tsx +4 -1
  52. package/bizar-dash/src/web/views/Tasks.tsx +11 -2
  53. package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +8 -2
  54. package/bizar-dash/src/web/views/memory/LightragPanel.tsx +3 -0
  55. package/bizar-dash/src/web/views/memory/ObsidianPanel.tsx +12 -4
  56. package/bizar-dash/src/web/views/memory/SemanticSearchPanel.tsx +3 -0
  57. package/bizar-dash/src/web/views/settings/ActivitySection.tsx +205 -0
  58. package/bizar-dash/src/web/views/settings/AgentSection.tsx +294 -0
  59. package/bizar-dash/src/web/views/settings/AuthSection.tsx +159 -0
  60. package/bizar-dash/src/web/views/settings/BackupSection.tsx +16 -0
  61. package/bizar-dash/src/web/views/settings/EnvVarsSection.tsx +16 -0
  62. package/bizar-dash/src/web/views/settings/GeneralSection.tsx +105 -0
  63. package/bizar-dash/src/web/views/settings/HeadroomSection.tsx +39 -0
  64. package/bizar-dash/src/web/views/settings/MemorySection.tsx +16 -0
  65. package/bizar-dash/src/web/views/settings/NetworkSection.tsx +87 -0
  66. package/bizar-dash/src/web/views/settings/NotificationsSection.tsx +34 -0
  67. package/bizar-dash/src/web/views/settings/ProvidersSection.tsx +16 -0
  68. package/bizar-dash/src/web/views/settings/SkillsSection.tsx +16 -0
  69. package/bizar-dash/src/web/views/settings/SystemLlmSection.tsx +81 -0
  70. package/bizar-dash/src/web/views/settings/ThemeSection.tsx +168 -0
  71. package/bizar-dash/src/web/views/settings/UpdatesSection.tsx +256 -0
  72. package/bizar-dash/tests/a11y.test.tsx +206 -0
  73. package/bizar-dash/tests/backup-restore.test.mjs +217 -0
  74. package/bizar-dash/tests/backup-restore.test.tsx +123 -0
  75. package/bizar-dash/tests/backup-store.test.mjs +300 -0
  76. package/bizar-dash/tests/cli-bugfixes.test.mjs +4 -4
  77. package/bizar-dash/tests/cli-error-visibility.test.mjs +153 -0
  78. package/bizar-dash/tests/cli-refactor.test.mjs +184 -0
  79. package/bizar-dash/tests/components/Button.test.tsx +41 -0
  80. package/bizar-dash/tests/components/Card.test.tsx +42 -0
  81. package/bizar-dash/tests/components/Modal.test.tsx +104 -0
  82. package/bizar-dash/tests/components/Spinner.test.tsx +32 -0
  83. package/bizar-dash/tests/components/StatusBadge.test.tsx +35 -0
  84. package/bizar-dash/tests/components/Toast.test.tsx +108 -0
  85. package/bizar-dash/tests/digest-generation.test.mjs +191 -0
  86. package/bizar-dash/tests/digest-store.test.mjs +264 -0
  87. package/bizar-dash/tests/hooks/useModal.test.tsx +84 -0
  88. package/bizar-dash/tests/hooks/useToast.test.tsx +50 -0
  89. package/bizar-dash/tests/lib/i18n.test.ts +46 -0
  90. package/bizar-dash/tests/lib/utils.test.ts +194 -0
  91. package/bizar-dash/tests/logger.test.mjs +207 -0
  92. package/bizar-dash/tests/metrics.test.mjs +183 -0
  93. package/bizar-dash/tests/rate-limit.test.mjs +298 -0
  94. package/bizar-dash/tests/server-bugfixes.test.mjs +2 -2
  95. package/bizar-dash/tests/setup.ts +7 -0
  96. package/bizar-dash/vitest.config.ts +13 -0
  97. package/cli/artifact-cli.mjs +605 -0
  98. package/cli/artifact-render.mjs +621 -0
  99. package/cli/artifact-server.mjs +847 -0
  100. package/cli/artifact.mjs +38 -2096
  101. package/cli/bg.mjs +5 -13
  102. package/cli/bin.mjs +221 -1350
  103. package/cli/commands/artifact.mjs +20 -0
  104. package/cli/commands/dash.mjs +160 -0
  105. package/cli/commands/headroom.mjs +204 -0
  106. package/cli/commands/install.mjs +169 -0
  107. package/cli/commands/memory.mjs +25 -0
  108. package/cli/commands/minimax.mjs +285 -0
  109. package/cli/commands/mod.mjs +185 -0
  110. package/cli/commands/service.mjs +65 -0
  111. package/cli/commands/usage.mjs +109 -0
  112. package/cli/commands/util.mjs +459 -0
  113. package/cli/digest.mjs +149 -0
  114. package/cli/doctor.mjs +1 -13
  115. package/cli/provision.mjs +2 -13
  116. package/cli/service-controller.mjs +1 -11
  117. package/cli/service.mjs +1 -11
  118. package/cli/utils.mjs +41 -1
  119. package/package.json +6 -1
  120. package/bizar-dash/dist/assets/main-B4OfGAwz.js +0 -361
  121. package/bizar-dash/dist/assets/main-B4OfGAwz.js.map +0 -1
  122. package/bizar-dash/dist/assets/main-DAlLdW8I.css +0 -1
  123. 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
+ });