@polderlabs/bizar 5.4.0 → 5.5.0
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/{EnvVarsSection-DqvB8ZcJ.js → EnvVarsSection-L413CQEI.js} +3 -3
- package/bizar-dash/dist/assets/{EnvVarsSection-DqvB8ZcJ.js.map → EnvVarsSection-L413CQEI.js.map} +1 -1
- package/bizar-dash/dist/assets/{MobileChat-DRXP2ERQ.js → MobileChat-0kGQb1S6.js} +1 -1
- package/bizar-dash/dist/assets/{MobileChat-DRXP2ERQ.js.map → MobileChat-0kGQb1S6.js.map} +1 -1
- package/bizar-dash/dist/assets/MobileSettings-Cq1oFIdO.js +1 -0
- package/bizar-dash/dist/assets/{MobileSettings-CVW9VhEo.js.map → MobileSettings-Cq1oFIdO.js.map} +1 -1
- package/bizar-dash/dist/assets/{Toast-CcZVVN4i.js → Toast-C6f1zFJ2.js} +1 -1
- package/bizar-dash/dist/assets/{Toast-CcZVVN4i.js.map → Toast-C6f1zFJ2.js.map} +1 -1
- package/bizar-dash/dist/assets/{icons-OMEGx5KA.js → icons-BsWXWkEF.js} +121 -126
- package/bizar-dash/dist/assets/icons-BsWXWkEF.js.map +1 -0
- package/bizar-dash/dist/assets/main-BklYy02e.css +1 -0
- package/bizar-dash/dist/assets/main-CBTgYSeG.js +18 -0
- package/bizar-dash/dist/assets/main-CBTgYSeG.js.map +1 -0
- package/bizar-dash/dist/assets/mobile-CsaN4CxH.js +1 -0
- package/bizar-dash/dist/assets/{mobile-XN2P7YfM.js.map → mobile-CsaN4CxH.js.map} +1 -1
- package/bizar-dash/dist/assets/{mobile-layout-D6V-7WnA.css → mobile-layout-CJnZNLy3.css} +1 -1
- package/bizar-dash/dist/assets/mobile-layout-iFBdjEE4.js +2 -0
- package/bizar-dash/dist/assets/mobile-layout-iFBdjEE4.js.map +1 -0
- package/bizar-dash/dist/assets/{useSlashCommands-iyoW95UB.js → useSlashCommands-DGJcC9iQ.js} +2 -2
- package/bizar-dash/dist/assets/{useSlashCommands-iyoW95UB.js.map → useSlashCommands-DGJcC9iQ.js.map} +1 -1
- package/bizar-dash/dist/index.html +8 -8
- package/bizar-dash/dist/mobile.html +4 -4
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/src/server/backup-store.mjs +6 -1
- package/bizar-dash/src/server/bg-spawner.mjs +574 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +242 -0
- package/bizar-dash/src/server/memory-store.mjs +43 -3
- package/bizar-dash/src/server/routes/background.mjs +143 -0
- package/bizar-dash/src/server/routes/lightrag.mjs +10 -0
- package/bizar-dash/src/server/routes/memory.mjs +8 -0
- package/bizar-dash/src/server/server.mjs +22 -0
- package/bizar-dash/src/web/App.tsx +3 -0
- package/bizar-dash/src/web/MobileApp.tsx +1 -10
- package/bizar-dash/src/web/components/InstallConfirmDialog.tsx +1 -1
- package/bizar-dash/src/web/components/SettingsNav.tsx +1 -10
- package/bizar-dash/src/web/lib/types.ts +39 -1
- package/bizar-dash/src/web/styles/main.css +43 -0
- package/bizar-dash/src/web/styles/mobile-layout.css +7 -5
- package/bizar-dash/src/web/styles/settings.css +46 -0
- package/bizar-dash/src/web/views/BackgroundAgents.tsx +399 -30
- package/bizar-dash/src/web/views/Settings.tsx +12 -12
- package/bizar-dash/src/web/views/SpawnAgentModal.tsx +221 -0
- package/bizar-dash/tests/BackgroundAgents.test.tsx +160 -0
- package/bizar-dash/tests/background-pause-resume.test.mjs +46 -0
- package/bizar-dash/tests/background-spawn-from-ui.test.mjs +36 -0
- package/bizar-dash/tests/background-steer.test.mjs +42 -0
- package/bizar-dash/tests/lightrag-startup-hook.test.mjs +206 -0
- package/bizar-dash/tests/memory-auto-reindex.test.mjs +196 -0
- package/bizar-dash/tests/memory-cli.test.mjs +1 -1
- package/bizar-dash/tests/memory-default-vault.test.mjs +90 -3
- package/bizar-dash/tests/memory-lightrag-extended.test.mjs +131 -0
- package/bizar-dash/tests/mobile-layout.test.tsx +36 -0
- package/bizar-dash/tests/settings-layout.test.tsx +3 -3
- package/bizar-dash/tests/settings-nav.test.tsx +7 -5
- package/cli/bin.mjs +16 -0
- package/cli/commands/deploy/docker.mjs +2 -1
- package/cli/commands/lightrag.mjs +171 -0
- package/cli/commands/service.mjs +7 -0
- package/cli/commands/util.mjs +8 -1
- package/cli/init.mjs +1 -1
- package/cli/post-install-smoke.mjs +230 -0
- package/cli/provision.mjs +177 -0
- package/cli/provision.test.mjs +180 -0
- package/cli/service-controller.mjs +378 -28
- package/cli/service-controller.test.mjs +100 -0
- package/cli/service-env.mjs +139 -0
- package/cli/service.mjs +23 -0
- package/config/agents/_shared/AGENT_BASELINE.md +6 -6
- package/install.sh +87 -6
- package/package.json +1 -1
- package/plugins/bizar/index.ts +147 -0
- package/plugins/bizar/src/background-state.ts +84 -5
- package/plugins/bizar/src/background.ts +388 -10
- package/plugins/bizar/src/hooks/memory-inject.ts +247 -0
- package/plugins/bizar/src/hooks/memory-write-on-end.ts +188 -0
- package/plugins/bizar/src/opencode-runner.ts +87 -1
- package/plugins/bizar/src/tools/bg-pause.ts +77 -0
- package/plugins/bizar/src/tools/bg-report-progress.ts +101 -0
- package/plugins/bizar/src/tools/bg-resume.ts +72 -0
- package/plugins/bizar/src/tools/bg-send-message.ts +105 -0
- package/plugins/bizar/src/tools/bg-spawn.ts +13 -6
- package/plugins/bizar/src/tools/bg-status.ts +10 -0
- package/plugins/bizar/src/tools/memory-list.ts +135 -0
- package/plugins/bizar/src/tools/memory-read.ts +142 -0
- package/plugins/bizar/src/tools/memory-search.ts +228 -0
- package/plugins/bizar/src/tools/memory-write.ts +183 -0
- package/plugins/bizar/tests/memory-write-on-end.test.ts +92 -0
- package/plugins/bizar/tests/tools/bg-pause.test.ts +64 -0
- package/plugins/bizar/tests/tools/bg-report-progress.test.ts +82 -0
- package/plugins/bizar/tests/tools/bg-resume.test.ts +43 -0
- package/plugins/bizar/tests/tools/bg-send-message.test.ts +46 -0
- package/bizar-dash/dist/assets/MobileSettings-CVW9VhEo.js +0 -1
- package/bizar-dash/dist/assets/icons-OMEGx5KA.js.map +0 -1
- package/bizar-dash/dist/assets/main-CMAGZouj.js +0 -16
- package/bizar-dash/dist/assets/main-CMAGZouj.js.map +0 -1
- package/bizar-dash/dist/assets/main-O2tZVdHm.css +0 -1
- package/bizar-dash/dist/assets/mobile-XN2P7YfM.js +0 -1
- package/bizar-dash/dist/assets/mobile-layout-DqTxvXZp.js +0 -2
- package/bizar-dash/dist/assets/mobile-layout-DqTxvXZp.js.map +0 -1
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// src/web/views/SpawnAgentModal.tsx — modal form to spawn a background
|
|
2
|
+
// agent directly from the dashboard. Calls POST /api/background,
|
|
3
|
+
// tracks the resulting instanceId, and on success closes the modal
|
|
4
|
+
// while the parent navigates to the new instance's detail view.
|
|
5
|
+
//
|
|
6
|
+
// Fields mirror the API body shape:
|
|
7
|
+
// agent (string, required) — agent name
|
|
8
|
+
// prompt (string, required) — initial prompt
|
|
9
|
+
// model (string, optional) — "providerID/modelID"
|
|
10
|
+
// timeoutMs (number, slider) — 1min..4h, default 5min
|
|
11
|
+
// persistent (boolean, toggle) — auto-restart on failure
|
|
12
|
+
// tags (string[], optional)
|
|
13
|
+
import { useState, type FormEvent } from 'react';
|
|
14
|
+
import { Plus, X } from 'lucide-react';
|
|
15
|
+
import { Button } from '../components/Button';
|
|
16
|
+
import { useToast } from '../components/Toast';
|
|
17
|
+
import { api } from '../lib/api';
|
|
18
|
+
|
|
19
|
+
type AgentOption = { name: string; description?: string };
|
|
20
|
+
|
|
21
|
+
type Props = {
|
|
22
|
+
open: boolean;
|
|
23
|
+
onClose: () => void;
|
|
24
|
+
onCreated?: (instanceId: string) => void;
|
|
25
|
+
/** Available agents for the dropdown. Defaults to a small static set. */
|
|
26
|
+
agents?: AgentOption[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const DEFAULT_AGENTS: AgentOption[] = [
|
|
30
|
+
{ name: 'mimir', description: 'Deep research & codebase exploration' },
|
|
31
|
+
{ name: 'frigg', description: 'Read-only codebase Q&A' },
|
|
32
|
+
{ name: 'thor', description: 'Moderate-complexity implementation' },
|
|
33
|
+
{ name: 'tyr', description: 'Complex feature implementation' },
|
|
34
|
+
{ name: 'heimdall', description: 'Quick edits & simple tasks' },
|
|
35
|
+
{ name: 'odin', description: 'Primary router agent' },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const TIMEOUT_PRESETS = [
|
|
39
|
+
{ label: '1 min', value: 60_000 },
|
|
40
|
+
{ label: '5 min', value: 300_000 },
|
|
41
|
+
{ label: '15 min', value: 900_000 },
|
|
42
|
+
{ label: '30 min', value: 1_800_000 },
|
|
43
|
+
{ label: '1 hr', value: 3_600_000 },
|
|
44
|
+
{ label: '4 hr', value: 14_400_000 },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
export function SpawnAgentModal({ open, onClose, onCreated, agents = DEFAULT_AGENTS }: Props) {
|
|
48
|
+
const toast = useToast();
|
|
49
|
+
const [agent, setAgent] = useState<string>(agents[0]?.name || 'mimir');
|
|
50
|
+
const [prompt, setPrompt] = useState<string>('');
|
|
51
|
+
const [model, setModel] = useState<string>('');
|
|
52
|
+
const [timeoutMs, setTimeoutMs] = useState<number>(300_000);
|
|
53
|
+
const [persistent, setPersistent] = useState<boolean>(false);
|
|
54
|
+
const [tagsRaw, setTagsRaw] = useState<string>('');
|
|
55
|
+
const [submitting, setSubmitting] = useState<boolean>(false);
|
|
56
|
+
const [error, setError] = useState<string | null>(null);
|
|
57
|
+
|
|
58
|
+
if (!open) return null;
|
|
59
|
+
|
|
60
|
+
const onSubmit = async (ev: FormEvent<HTMLFormElement>) => {
|
|
61
|
+
ev.preventDefault();
|
|
62
|
+
setError(null);
|
|
63
|
+
if (!prompt.trim()) {
|
|
64
|
+
setError('Prompt is required.');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
setSubmitting(true);
|
|
68
|
+
try {
|
|
69
|
+
const tags = tagsRaw
|
|
70
|
+
.split(',')
|
|
71
|
+
.map((t) => t.trim())
|
|
72
|
+
.filter((t) => t.length > 0);
|
|
73
|
+
const body = {
|
|
74
|
+
agent,
|
|
75
|
+
prompt,
|
|
76
|
+
...(model.trim() ? { model: model.trim() } : {}),
|
|
77
|
+
timeoutMs,
|
|
78
|
+
persistent,
|
|
79
|
+
maxRestarts: 3,
|
|
80
|
+
...(tags.length > 0 ? { tags } : {}),
|
|
81
|
+
};
|
|
82
|
+
const res = await api.post<{
|
|
83
|
+
instanceId: string;
|
|
84
|
+
status: string;
|
|
85
|
+
sessionId?: string;
|
|
86
|
+
error?: string;
|
|
87
|
+
message?: string;
|
|
88
|
+
}>('/background', body);
|
|
89
|
+
if (res?.error) {
|
|
90
|
+
throw new Error(res.message || res.error);
|
|
91
|
+
}
|
|
92
|
+
toast.success(`Spawned ${agent} (${res.instanceId.slice(0, 14)}…)`);
|
|
93
|
+
onCreated?.(res.instanceId);
|
|
94
|
+
// Reset the form for next time.
|
|
95
|
+
setPrompt('');
|
|
96
|
+
setModel('');
|
|
97
|
+
setPersistent(false);
|
|
98
|
+
setTagsRaw('');
|
|
99
|
+
onClose();
|
|
100
|
+
} catch (err) {
|
|
101
|
+
setError((err as Error)?.message || 'Failed to spawn.');
|
|
102
|
+
} finally {
|
|
103
|
+
setSubmitting(false);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div className="modal-backdrop" role="dialog" aria-modal="true">
|
|
109
|
+
<div className="modal-card spawn-modal">
|
|
110
|
+
<div className="spawn-modal-header">
|
|
111
|
+
<h3>
|
|
112
|
+
<Plus size={18} /> Spawn background agent
|
|
113
|
+
</h3>
|
|
114
|
+
<Button variant="ghost" size="sm" onClick={onClose} title="Close">
|
|
115
|
+
<X size={14} />
|
|
116
|
+
</Button>
|
|
117
|
+
</div>
|
|
118
|
+
<form onSubmit={onSubmit} className="spawn-modal-body">
|
|
119
|
+
<label className="field">
|
|
120
|
+
<span className="field-label">Agent</span>
|
|
121
|
+
<select
|
|
122
|
+
className="field-input"
|
|
123
|
+
value={agent}
|
|
124
|
+
onChange={(e) => setAgent(e.target.value)}
|
|
125
|
+
required
|
|
126
|
+
>
|
|
127
|
+
{agents.map((a) => (
|
|
128
|
+
<option key={a.name} value={a.name}>
|
|
129
|
+
{a.name}{a.description ? ` — ${a.description}` : ''}
|
|
130
|
+
</option>
|
|
131
|
+
))}
|
|
132
|
+
</select>
|
|
133
|
+
</label>
|
|
134
|
+
|
|
135
|
+
<label className="field">
|
|
136
|
+
<span className="field-label">Initial prompt</span>
|
|
137
|
+
<textarea
|
|
138
|
+
className="field-input spawn-modal-textarea"
|
|
139
|
+
rows={5}
|
|
140
|
+
value={prompt}
|
|
141
|
+
onChange={(e) => setPrompt(e.target.value)}
|
|
142
|
+
placeholder="What do you want this agent to do?"
|
|
143
|
+
required
|
|
144
|
+
/>
|
|
145
|
+
</label>
|
|
146
|
+
|
|
147
|
+
<label className="field">
|
|
148
|
+
<span className="field-label">Model (optional)</span>
|
|
149
|
+
<input
|
|
150
|
+
type="text"
|
|
151
|
+
className="field-input"
|
|
152
|
+
value={model}
|
|
153
|
+
onChange={(e) => setModel(e.target.value)}
|
|
154
|
+
placeholder="providerID/modelID (leave blank for agent default)"
|
|
155
|
+
/>
|
|
156
|
+
</label>
|
|
157
|
+
|
|
158
|
+
<div className="field">
|
|
159
|
+
<span className="field-label">Timeout</span>
|
|
160
|
+
<div className="spawn-modal-timeouts">
|
|
161
|
+
{TIMEOUT_PRESETS.map((p) => (
|
|
162
|
+
<button
|
|
163
|
+
key={p.value}
|
|
164
|
+
type="button"
|
|
165
|
+
className={
|
|
166
|
+
'spawn-modal-timeout-pill' +
|
|
167
|
+
(p.value === timeoutMs ? ' spawn-modal-timeout-pill-active' : '')
|
|
168
|
+
}
|
|
169
|
+
onClick={() => setTimeoutMs(p.value)}
|
|
170
|
+
>
|
|
171
|
+
{p.label}
|
|
172
|
+
</button>
|
|
173
|
+
))}
|
|
174
|
+
</div>
|
|
175
|
+
<input
|
|
176
|
+
type="range"
|
|
177
|
+
min={60_000}
|
|
178
|
+
max={14_400_000}
|
|
179
|
+
step={60_000}
|
|
180
|
+
value={timeoutMs}
|
|
181
|
+
onChange={(e) => setTimeoutMs(Number(e.target.value))}
|
|
182
|
+
className="spawn-modal-slider"
|
|
183
|
+
/>
|
|
184
|
+
<span className="muted">{(timeoutMs / 60_000).toFixed(0)} min</span>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<label className="field-checkbox">
|
|
188
|
+
<input
|
|
189
|
+
type="checkbox"
|
|
190
|
+
checked={persistent}
|
|
191
|
+
onChange={(e) => setPersistent(e.target.checked)}
|
|
192
|
+
/>
|
|
193
|
+
<span>Persistent — auto-restart on terminal failure (max 3 attempts)</span>
|
|
194
|
+
</label>
|
|
195
|
+
|
|
196
|
+
<label className="field">
|
|
197
|
+
<span className="field-label">Tags (optional, comma-separated)</span>
|
|
198
|
+
<input
|
|
199
|
+
type="text"
|
|
200
|
+
className="field-input"
|
|
201
|
+
value={tagsRaw}
|
|
202
|
+
onChange={(e) => setTagsRaw(e.target.value)}
|
|
203
|
+
placeholder="e.g. refactor, urgent"
|
|
204
|
+
/>
|
|
205
|
+
</label>
|
|
206
|
+
|
|
207
|
+
{error && <div className="spawn-modal-error">{error}</div>}
|
|
208
|
+
|
|
209
|
+
<div className="spawn-modal-actions">
|
|
210
|
+
<Button variant="ghost" size="sm" type="button" onClick={onClose}>
|
|
211
|
+
Cancel
|
|
212
|
+
</Button>
|
|
213
|
+
<Button variant="primary" size="sm" type="submit" disabled={submitting}>
|
|
214
|
+
{submitting ? 'Spawning…' : 'Spawn agent'}
|
|
215
|
+
</Button>
|
|
216
|
+
</div>
|
|
217
|
+
</form>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// tests/BackgroundAgents.test.tsx — v5.x component tests for the
|
|
2
|
+
// rewired BackgroundAgents view and SpawnAgentModal. Uses
|
|
3
|
+
// @testing-library/react and the dashboard's vitest setup.
|
|
4
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
5
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
6
|
+
import userEvent from '@testing-library/user-event';
|
|
7
|
+
import { BackgroundAgents } from '../src/web/views/BackgroundAgents';
|
|
8
|
+
import { ToastProvider } from '../src/web/components/Toast';
|
|
9
|
+
|
|
10
|
+
// --- API mock ------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
const mockApiGet = vi.fn();
|
|
13
|
+
const mockApiPost = vi.fn();
|
|
14
|
+
|
|
15
|
+
vi.mock('../src/web/lib/api', () => ({
|
|
16
|
+
api: {
|
|
17
|
+
get: (...args: unknown[]) => mockApiGet(...args),
|
|
18
|
+
post: (...args: unknown[]) => mockApiPost(...args),
|
|
19
|
+
},
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
vi.mock('../src/web/lib/ws', () => ({
|
|
23
|
+
Ws: class {
|
|
24
|
+
constructor() {}
|
|
25
|
+
on() { return () => {}; }
|
|
26
|
+
onStatus() { return () => {}; }
|
|
27
|
+
close() {}
|
|
28
|
+
},
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
// --- Helpers -------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
const dummySnapshot = {} as any;
|
|
34
|
+
const dummySettings = {} as any;
|
|
35
|
+
|
|
36
|
+
function wrap(node: React.ReactNode) {
|
|
37
|
+
return <ToastProvider>{node}</ToastProvider>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
mockApiGet.mockReset();
|
|
42
|
+
mockApiPost.mockReset();
|
|
43
|
+
// Default: list returns empty.
|
|
44
|
+
mockApiGet.mockImplementation(async (path: string) => {
|
|
45
|
+
if (path === '/background') return { instances: [], status: { dir: '/tmp', exists: false, count: 0 } };
|
|
46
|
+
if (path.includes('/tool-calls')) return { toolCalls: [] };
|
|
47
|
+
if (path.includes('/output')) return { output: '', available: false };
|
|
48
|
+
if (path.includes('/tmux')) return { session: 'bgr_x', exists: false, attachCommand: 'tmux attach -t bgr_x', attachUrl: 'tmux://bgr_x' };
|
|
49
|
+
return {};
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// --- Tests ---------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
describe('BackgroundAgents — v5.x integration', () => {
|
|
56
|
+
it('renders empty state with a Spawn action', async () => {
|
|
57
|
+
render(wrap(<BackgroundAgents snapshot={dummySnapshot} settings={dummySettings} activeTab="background" setActiveTab={() => {}} refreshSnapshot={async () => {}} />));
|
|
58
|
+
await waitFor(() =>
|
|
59
|
+
expect(screen.getByText(/No active background agents/i)).toBeInTheDocument(),
|
|
60
|
+
);
|
|
61
|
+
expect(screen.getAllByRole('button', { name: /Spawn/i }).length).toBeGreaterThan(0);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('renders an active card with pause + steer buttons', async () => {
|
|
65
|
+
mockApiGet.mockImplementation(async (path: string) => {
|
|
66
|
+
if (path === '/background') {
|
|
67
|
+
return {
|
|
68
|
+
instances: [
|
|
69
|
+
{
|
|
70
|
+
instanceId: 'bgr_running_x',
|
|
71
|
+
status: 'running',
|
|
72
|
+
agent: 'mimir',
|
|
73
|
+
prompt: 'say hi',
|
|
74
|
+
startedAt: Date.now() - 5000,
|
|
75
|
+
toolCallCount: 0,
|
|
76
|
+
tmuxSession: 'bgr_running_x',
|
|
77
|
+
tmuxActive: false,
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
status: { dir: '/tmp', exists: true, count: 1 },
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return {};
|
|
84
|
+
});
|
|
85
|
+
render(wrap(<BackgroundAgents snapshot={dummySnapshot} settings={dummySettings} activeTab="background" setActiveTab={() => {}} refreshSnapshot={async () => {}} />));
|
|
86
|
+
await waitFor(() => expect(screen.getByText('mimir')).toBeInTheDocument());
|
|
87
|
+
expect(screen.getByRole('button', { name: /Pause/i })).toBeInTheDocument();
|
|
88
|
+
expect(screen.getByRole('button', { name: /Steer/i })).toBeInTheDocument();
|
|
89
|
+
expect(screen.getByRole('button', { name: /Kill/i })).toBeInTheDocument();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('renders a paused card with resume instead of pause', async () => {
|
|
93
|
+
mockApiGet.mockImplementation(async (path: string) => {
|
|
94
|
+
if (path === '/background') {
|
|
95
|
+
return {
|
|
96
|
+
instances: [
|
|
97
|
+
{
|
|
98
|
+
instanceId: 'bgr_paused_x',
|
|
99
|
+
status: 'paused',
|
|
100
|
+
agent: 'mimir',
|
|
101
|
+
prompt: 'paused run',
|
|
102
|
+
startedAt: Date.now() - 5000,
|
|
103
|
+
toolCallCount: 0,
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
status: { dir: '/tmp', exists: true, count: 1 },
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return {};
|
|
110
|
+
});
|
|
111
|
+
render(wrap(<BackgroundAgents snapshot={dummySnapshot} settings={dummySettings} activeTab="background" setActiveTab={() => {}} refreshSnapshot={async () => {}} />));
|
|
112
|
+
await waitFor(() => expect(screen.getByRole('button', { name: /Resume/i })).toBeInTheDocument());
|
|
113
|
+
expect(screen.queryByRole('button', { name: /Pause/i })).not.toBeInTheDocument();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('progress bar shows when instance.progress is set', async () => {
|
|
117
|
+
mockApiGet.mockImplementation(async (path: string) => {
|
|
118
|
+
if (path === '/background') {
|
|
119
|
+
return {
|
|
120
|
+
instances: [
|
|
121
|
+
{
|
|
122
|
+
instanceId: 'bgr_progress_x',
|
|
123
|
+
status: 'running',
|
|
124
|
+
agent: 'mimir',
|
|
125
|
+
prompt: 'step',
|
|
126
|
+
startedAt: Date.now(),
|
|
127
|
+
toolCallCount: 1,
|
|
128
|
+
progress: 42,
|
|
129
|
+
progressMessage: 'halfway through',
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
status: { dir: '/tmp', exists: true, count: 1 },
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return {};
|
|
136
|
+
});
|
|
137
|
+
render(wrap(<BackgroundAgents snapshot={dummySnapshot} settings={dummySettings} activeTab="background" setActiveTab={() => {}} refreshSnapshot={async () => {}} />));
|
|
138
|
+
await waitFor(() => expect(screen.getByText(/42%/)).toBeInTheDocument());
|
|
139
|
+
expect(screen.getByText(/halfway through/i)).toBeInTheDocument();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('pause button calls pause endpoint', async () => {
|
|
143
|
+
mockApiGet.mockImplementation(async (path: string) => {
|
|
144
|
+
if (path === '/background') {
|
|
145
|
+
return {
|
|
146
|
+
instances: [{ instanceId: 'bgr_x', status: 'running', agent: 'mimir', prompt: 'p', startedAt: Date.now(), toolCallCount: 0 }],
|
|
147
|
+
status: { dir: '/tmp', exists: true, count: 1 },
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return {};
|
|
151
|
+
});
|
|
152
|
+
mockApiPost.mockResolvedValue({ ok: true });
|
|
153
|
+
render(wrap(<BackgroundAgents snapshot={dummySnapshot} settings={dummySettings} activeTab="background" setActiveTab={() => {}} refreshSnapshot={async () => {}} />));
|
|
154
|
+
const btn = await waitFor(() => screen.getByRole('button', { name: /Pause/i }));
|
|
155
|
+
await userEvent.click(btn);
|
|
156
|
+
await waitFor(() => {
|
|
157
|
+
expect(mockApiPost).toHaveBeenCalledWith('/background/bgr_x/pause', {});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tests/background-pause-resume.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* v5.x — Tests for POST /api/background/:id/pause and .../resume.
|
|
5
|
+
*
|
|
6
|
+
* Validates the spawner's pure-logic surface WITHOUT spawning a real
|
|
7
|
+
* opencode subprocess (which would block on `opencode run` exiting).
|
|
8
|
+
* The pause/resume registry helpers are tested via error paths:
|
|
9
|
+
* - unknown instance → "instance_not_tracked"
|
|
10
|
+
* - paused state survives round-trip after a real spawn (skipped — see notes)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { test } from 'node:test';
|
|
14
|
+
import assert from 'node:assert/strict';
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
pauseBgAgent,
|
|
18
|
+
resumeBgAgent,
|
|
19
|
+
status,
|
|
20
|
+
configureSpawner,
|
|
21
|
+
} from '../src/server/bg-spawner.mjs';
|
|
22
|
+
|
|
23
|
+
test('configureSpawner registers a broadcast hook without throwing', () => {
|
|
24
|
+
let received = null;
|
|
25
|
+
configureSpawner({ broadcast: (msg) => { received = msg; } });
|
|
26
|
+
assert.equal(typeof received, 'object');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('pauseBgAgent on unknown instance returns instance_not_tracked', () => {
|
|
30
|
+
const r = pauseBgAgent('bgr_definitely_missing_id');
|
|
31
|
+
assert.equal(r.ok, false);
|
|
32
|
+
assert.equal(r.error, 'instance_not_tracked');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('resumeBgAgent on unknown instance returns instance_not_tracked', () => {
|
|
36
|
+
const r = resumeBgAgent('bgr_definitely_missing_id');
|
|
37
|
+
assert.equal(r.ok, false);
|
|
38
|
+
assert.equal(r.error, 'instance_not_tracked');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('status() returns count + ids diagnostics without throwing', () => {
|
|
42
|
+
const s = status();
|
|
43
|
+
assert.equal(typeof s.count, 'number');
|
|
44
|
+
assert.ok(Array.isArray(s.ids));
|
|
45
|
+
assert.ok(s.count >= 0);
|
|
46
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tests/background-spawn-from-ui.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* v5.x — Validates the spawner's input-validation surface only —
|
|
5
|
+
* doesn't actually launch `opencode run` (that would block).
|
|
6
|
+
*
|
|
7
|
+
* The full happy-path test lives in a manual run-script (see
|
|
8
|
+
* scripts/bg-spawn-smoke.mjs, future work) because the dashboard
|
|
9
|
+
* sandbox can't guarantee opencode is installed + auth'd.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { test } from 'node:test';
|
|
13
|
+
import assert from 'node:assert/strict';
|
|
14
|
+
|
|
15
|
+
import { spawnBgAgent } from '../src/server/bg-spawner.mjs';
|
|
16
|
+
|
|
17
|
+
test('spawnBgAgent rejects empty args', async () => {
|
|
18
|
+
const r = await spawnBgAgent({});
|
|
19
|
+
assert.equal(r.error, 'missing_required_fields');
|
|
20
|
+
assert.equal(r.instanceId, '');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('spawnBgAgent rejects missing prompt', async () => {
|
|
24
|
+
const r = await spawnBgAgent({ agent: 'mimir', worktree: '/tmp' });
|
|
25
|
+
assert.equal(r.error, 'missing_required_fields');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('spawnBgAgent rejects missing agent', async () => {
|
|
29
|
+
const r = await spawnBgAgent({ prompt: 'hi', worktree: '/tmp' });
|
|
30
|
+
assert.equal(r.error, 'missing_required_fields');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('spawnBgAgent rejects missing worktree', async () => {
|
|
34
|
+
const r = await spawnBgAgent({ agent: 'mimir', prompt: 'hi' });
|
|
35
|
+
assert.equal(r.error, 'missing_required_fields');
|
|
36
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tests/background-steer.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* v5.x — Tests for the steer-broker helper. Validates that the
|
|
5
|
+
* helper accepts a non-empty message and rejects empty input. We do
|
|
6
|
+
* NOT exercise the kill+respawn path here because it requires a real
|
|
7
|
+
* `opencode` binary on PATH; see background-spawn-from-ui.test.mjs
|
|
8
|
+
* for that path.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { test } from 'node:test';
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import { mkdtempSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { tmpdir } from 'node:os';
|
|
16
|
+
|
|
17
|
+
import { steerBgAgent } from '../src/server/bg-spawner.mjs';
|
|
18
|
+
|
|
19
|
+
test('steerBgAgent rejects empty message', async () => {
|
|
20
|
+
const r = await steerBgAgent('bgr_nope', '');
|
|
21
|
+
assert.equal(r.ok, false);
|
|
22
|
+
assert.equal(r.error, 'message_empty');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('steerBgAgent rejects whitespace-only message', async () => {
|
|
26
|
+
const r = await steerBgAgent('bgr_nope', ' ');
|
|
27
|
+
assert.equal(r.ok, false);
|
|
28
|
+
assert.equal(r.error, 'message_empty');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('steerBgAgent with missing instance returns not_found', async () => {
|
|
32
|
+
const tmp = mkdtempSync(join(tmpdir(), 'bgspr-'));
|
|
33
|
+
const originalHome = process.env.HOME;
|
|
34
|
+
process.env.HOME = tmp;
|
|
35
|
+
try {
|
|
36
|
+
const r = await steerBgAgent('bgr_definitely_missing', 'go faster');
|
|
37
|
+
assert.equal(r.ok, false);
|
|
38
|
+
assert.match(r.error || '', /instance_not_found|file/);
|
|
39
|
+
} finally {
|
|
40
|
+
process.env.HOME = originalHome;
|
|
41
|
+
}
|
|
42
|
+
});
|