@lovenyberg/ove 0.7.0 → 0.9.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/bin/ove.ts +37 -1
- package/package.json +1 -1
- package/public/index.html +164 -3
- package/public/metrics.html +716 -0
- package/public/status.html +43 -0
- package/public/trace.html +127 -0
- package/src/adapters/github.test.ts +8 -0
- package/src/adapters/github.ts +3 -2
- package/src/adapters/http.test.ts +597 -1
- package/src/adapters/http.ts +222 -3
- package/src/adapters/slack.test.ts +233 -0
- package/src/adapters/types.ts +1 -1
- package/src/adapters/whatsapp.test.ts +102 -0
- package/src/adapters/wiring.test.ts +2 -0
- package/src/diagnostics.test.ts +375 -0
- package/src/handlers.test.ts +553 -0
- package/src/handlers.ts +46 -7
- package/src/index.ts +1 -0
- package/src/queue.test.ts +174 -0
- package/src/queue.ts +85 -7
- package/src/router.test.ts +151 -1
- package/src/router.ts +95 -34
- package/src/sessions.test.ts +41 -0
- package/src/sessions.ts +27 -0
- package/src/setup.ts +160 -1
- package/src/smoke.test.ts +31 -1
package/src/queue.test.ts
CHANGED
|
@@ -110,4 +110,178 @@ describe("TaskQueue", () => {
|
|
|
110
110
|
const task = queue.dequeue();
|
|
111
111
|
expect(task!.taskType).toBe("discuss");
|
|
112
112
|
});
|
|
113
|
+
|
|
114
|
+
it("default priority is 0", () => {
|
|
115
|
+
const id = queue.enqueue({
|
|
116
|
+
userId: "slack:U123",
|
|
117
|
+
repo: "my-app",
|
|
118
|
+
prompt: "normal task",
|
|
119
|
+
});
|
|
120
|
+
const task = queue.get(id);
|
|
121
|
+
expect(task!.priority).toBe(0);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("stores and retrieves priority", () => {
|
|
125
|
+
const id = queue.enqueue({
|
|
126
|
+
userId: "slack:U123",
|
|
127
|
+
repo: "my-app",
|
|
128
|
+
prompt: "urgent task",
|
|
129
|
+
priority: 2,
|
|
130
|
+
});
|
|
131
|
+
const task = queue.get(id);
|
|
132
|
+
expect(task!.priority).toBe(2);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("preserves priority through dequeue", () => {
|
|
136
|
+
queue.enqueue({
|
|
137
|
+
userId: "slack:U123",
|
|
138
|
+
repo: "my-app",
|
|
139
|
+
prompt: "high priority task",
|
|
140
|
+
priority: 1,
|
|
141
|
+
});
|
|
142
|
+
const task = queue.dequeue();
|
|
143
|
+
expect(task!.priority).toBe(1);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("dequeues higher priority tasks before lower priority", () => {
|
|
147
|
+
// Enqueue low priority first, then high priority
|
|
148
|
+
queue.enqueue({
|
|
149
|
+
userId: "slack:U123",
|
|
150
|
+
repo: "repo-a",
|
|
151
|
+
prompt: "normal task",
|
|
152
|
+
priority: 0,
|
|
153
|
+
});
|
|
154
|
+
queue.enqueue({
|
|
155
|
+
userId: "slack:U123",
|
|
156
|
+
repo: "repo-b",
|
|
157
|
+
prompt: "urgent task",
|
|
158
|
+
priority: 2,
|
|
159
|
+
});
|
|
160
|
+
queue.enqueue({
|
|
161
|
+
userId: "slack:U123",
|
|
162
|
+
repo: "repo-c",
|
|
163
|
+
prompt: "high task",
|
|
164
|
+
priority: 1,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Should dequeue urgent (2) first, then high (1), then normal (0)
|
|
168
|
+
const first = queue.dequeue();
|
|
169
|
+
expect(first!.prompt).toBe("urgent task");
|
|
170
|
+
expect(first!.priority).toBe(2);
|
|
171
|
+
|
|
172
|
+
const second = queue.dequeue();
|
|
173
|
+
expect(second!.prompt).toBe("high task");
|
|
174
|
+
expect(second!.priority).toBe(1);
|
|
175
|
+
|
|
176
|
+
const third = queue.dequeue();
|
|
177
|
+
expect(third!.prompt).toBe("normal task");
|
|
178
|
+
expect(third!.priority).toBe(0);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("dequeues by FIFO within same priority", () => {
|
|
182
|
+
queue.enqueue({
|
|
183
|
+
userId: "slack:U123",
|
|
184
|
+
repo: "repo-a",
|
|
185
|
+
prompt: "first normal",
|
|
186
|
+
priority: 0,
|
|
187
|
+
});
|
|
188
|
+
queue.enqueue({
|
|
189
|
+
userId: "slack:U123",
|
|
190
|
+
repo: "repo-b",
|
|
191
|
+
prompt: "second normal",
|
|
192
|
+
priority: 0,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const first = queue.dequeue();
|
|
196
|
+
expect(first!.prompt).toBe("first normal");
|
|
197
|
+
|
|
198
|
+
const second = queue.dequeue();
|
|
199
|
+
expect(second!.prompt).toBe("second normal");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe("metrics()", () => {
|
|
203
|
+
it("returns zeroes on empty queue", () => {
|
|
204
|
+
const m = queue.metrics();
|
|
205
|
+
expect(m.counts).toEqual({ pending: 0, running: 0, completed: 0, failed: 0 });
|
|
206
|
+
expect(m.avgDurationByRepo).toEqual([]);
|
|
207
|
+
expect(m.throughput.lastHour).toBe(0);
|
|
208
|
+
expect(m.throughput.last24h).toBe(0);
|
|
209
|
+
expect(m.errorRate).toBe(0);
|
|
210
|
+
expect(m.repoBreakdown).toEqual([]);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("returns correct counts by status", () => {
|
|
214
|
+
queue.enqueue({ userId: "u1", repo: "a", prompt: "p1" });
|
|
215
|
+
const id2 = queue.enqueue({ userId: "u1", repo: "b", prompt: "p2" });
|
|
216
|
+
queue.dequeue(); // dequeues repo "a" -> running
|
|
217
|
+
// repo "b" is still pending since "a" is running but different repo, so dequeue "b"
|
|
218
|
+
const task2 = queue.dequeue();
|
|
219
|
+
if (task2) queue.complete(task2.id, "done");
|
|
220
|
+
|
|
221
|
+
const m = queue.metrics();
|
|
222
|
+
expect(m.counts.running).toBe(1);
|
|
223
|
+
expect(m.counts.completed).toBe(1);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("computes average duration by repo", () => {
|
|
227
|
+
const id1 = queue.enqueue({ userId: "u1", repo: "app-a", prompt: "p1" });
|
|
228
|
+
queue.dequeue();
|
|
229
|
+
queue.complete(id1, "ok");
|
|
230
|
+
|
|
231
|
+
const id2 = queue.enqueue({ userId: "u1", repo: "app-a", prompt: "p2" });
|
|
232
|
+
queue.dequeue();
|
|
233
|
+
queue.complete(id2, "ok");
|
|
234
|
+
|
|
235
|
+
const m = queue.metrics();
|
|
236
|
+
expect(m.avgDurationByRepo.length).toBe(1);
|
|
237
|
+
expect(m.avgDurationByRepo[0].repo).toBe("app-a");
|
|
238
|
+
expect(m.avgDurationByRepo[0].count).toBe(2);
|
|
239
|
+
expect(m.avgDurationByRepo[0].avgMs).toBeGreaterThanOrEqual(0);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("computes throughput for recent tasks", () => {
|
|
243
|
+
const id = queue.enqueue({ userId: "u1", repo: "x", prompt: "p" });
|
|
244
|
+
queue.dequeue();
|
|
245
|
+
queue.complete(id, "done");
|
|
246
|
+
|
|
247
|
+
const m = queue.metrics();
|
|
248
|
+
expect(m.throughput.lastHour).toBe(1);
|
|
249
|
+
expect(m.throughput.last24h).toBe(1);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("computes error rate", () => {
|
|
253
|
+
const id1 = queue.enqueue({ userId: "u1", repo: "a", prompt: "p1" });
|
|
254
|
+
queue.dequeue();
|
|
255
|
+
queue.complete(id1, "ok");
|
|
256
|
+
|
|
257
|
+
const id2 = queue.enqueue({ userId: "u1", repo: "a", prompt: "p2" });
|
|
258
|
+
queue.dequeue();
|
|
259
|
+
queue.fail(id2, "broken");
|
|
260
|
+
|
|
261
|
+
const m = queue.metrics();
|
|
262
|
+
// 1 failed out of 2 finished = 0.5
|
|
263
|
+
expect(m.errorRate).toBe(0.5);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("returns per-repo breakdown", () => {
|
|
267
|
+
queue.enqueue({ userId: "u1", repo: "alpha", prompt: "p1" });
|
|
268
|
+
queue.enqueue({ userId: "u1", repo: "beta", prompt: "p2" });
|
|
269
|
+
queue.enqueue({ userId: "u2", repo: "alpha", prompt: "p3" });
|
|
270
|
+
|
|
271
|
+
const m = queue.metrics();
|
|
272
|
+
expect(m.repoBreakdown.length).toBe(2);
|
|
273
|
+
const alpha = m.repoBreakdown.find((r) => r.repo === "alpha");
|
|
274
|
+
expect(alpha).toBeDefined();
|
|
275
|
+
expect(alpha!.pending).toBe(2);
|
|
276
|
+
const beta = m.repoBreakdown.find((r) => r.repo === "beta");
|
|
277
|
+
expect(beta).toBeDefined();
|
|
278
|
+
expect(beta!.pending).toBe(1);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("error rate is zero when no finished tasks", () => {
|
|
282
|
+
queue.enqueue({ userId: "u1", repo: "x", prompt: "p" });
|
|
283
|
+
const m = queue.metrics();
|
|
284
|
+
expect(m.errorRate).toBe(0);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
113
287
|
});
|
package/src/queue.ts
CHANGED
|
@@ -5,6 +5,7 @@ export interface TaskInput {
|
|
|
5
5
|
repo: string;
|
|
6
6
|
prompt: string;
|
|
7
7
|
taskType?: string;
|
|
8
|
+
priority?: number;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export interface Task {
|
|
@@ -15,6 +16,7 @@ export interface Task {
|
|
|
15
16
|
status: "pending" | "running" | "completed" | "failed";
|
|
16
17
|
result: string | null;
|
|
17
18
|
taskType: string | null;
|
|
19
|
+
priority: number;
|
|
18
20
|
createdAt: string;
|
|
19
21
|
completedAt: string | null;
|
|
20
22
|
}
|
|
@@ -27,6 +29,7 @@ interface TaskRow {
|
|
|
27
29
|
status: string;
|
|
28
30
|
result: string | null;
|
|
29
31
|
task_type: string | null;
|
|
32
|
+
priority: number;
|
|
30
33
|
created_at: string;
|
|
31
34
|
completed_at: string | null;
|
|
32
35
|
}
|
|
@@ -45,6 +48,7 @@ export class TaskQueue {
|
|
|
45
48
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
46
49
|
result TEXT,
|
|
47
50
|
task_type TEXT,
|
|
51
|
+
priority INTEGER NOT NULL DEFAULT 0,
|
|
48
52
|
created_at TEXT NOT NULL,
|
|
49
53
|
completed_at TEXT
|
|
50
54
|
)
|
|
@@ -54,14 +58,18 @@ export class TaskQueue {
|
|
|
54
58
|
if (!columns.some(c => c.name === "task_type")) {
|
|
55
59
|
this.db.run("ALTER TABLE tasks ADD COLUMN task_type TEXT");
|
|
56
60
|
}
|
|
61
|
+
// Migration: add priority column if missing (backward compat)
|
|
62
|
+
if (!columns.some(c => c.name === "priority")) {
|
|
63
|
+
this.db.run("ALTER TABLE tasks ADD COLUMN priority INTEGER NOT NULL DEFAULT 0");
|
|
64
|
+
}
|
|
57
65
|
}
|
|
58
66
|
|
|
59
67
|
enqueue(input: TaskInput): string {
|
|
60
68
|
const id = crypto.randomUUID();
|
|
61
69
|
this.db.run(
|
|
62
|
-
`INSERT INTO tasks (id, user_id, repo, prompt, status, task_type, created_at)
|
|
63
|
-
VALUES (?, ?, ?, ?, 'pending', ?, ?)`,
|
|
64
|
-
[id, input.userId, input.repo, input.prompt, input.taskType || null, new Date().toISOString()]
|
|
70
|
+
`INSERT INTO tasks (id, user_id, repo, prompt, status, task_type, priority, created_at)
|
|
71
|
+
VALUES (?, ?, ?, ?, 'pending', ?, ?, ?)`,
|
|
72
|
+
[id, input.userId, input.repo, input.prompt, input.taskType || null, input.priority ?? 0, new Date().toISOString()]
|
|
65
73
|
);
|
|
66
74
|
return id;
|
|
67
75
|
}
|
|
@@ -72,7 +80,7 @@ export class TaskQueue {
|
|
|
72
80
|
`SELECT * FROM tasks
|
|
73
81
|
WHERE status = 'pending'
|
|
74
82
|
AND repo NOT IN (SELECT repo FROM tasks WHERE status = 'running')
|
|
75
|
-
ORDER BY created_at ASC
|
|
83
|
+
ORDER BY priority DESC, created_at ASC
|
|
76
84
|
LIMIT 1`
|
|
77
85
|
)
|
|
78
86
|
.get() as TaskRow;
|
|
@@ -106,7 +114,7 @@ export class TaskQueue {
|
|
|
106
114
|
listByUser(userId: string, limit: number = 10): Task[] {
|
|
107
115
|
const rows = this.db
|
|
108
116
|
.query(
|
|
109
|
-
`SELECT * FROM tasks WHERE user_id = ? ORDER BY created_at DESC LIMIT ?`
|
|
117
|
+
`SELECT * FROM tasks WHERE user_id = ? ORDER BY priority DESC, created_at DESC LIMIT ?`
|
|
110
118
|
)
|
|
111
119
|
.all(userId, limit) as TaskRow[];
|
|
112
120
|
return rows.map((r) => this.rowToTask(r));
|
|
@@ -126,10 +134,79 @@ export class TaskQueue {
|
|
|
126
134
|
return row;
|
|
127
135
|
}
|
|
128
136
|
|
|
137
|
+
metrics(): {
|
|
138
|
+
counts: { pending: number; running: number; completed: number; failed: number };
|
|
139
|
+
avgDurationByRepo: { repo: string; avgMs: number; count: number }[];
|
|
140
|
+
throughput: { lastHour: number; last24h: number };
|
|
141
|
+
errorRate: number;
|
|
142
|
+
repoBreakdown: { repo: string; pending: number; running: number; completed: number; failed: number }[];
|
|
143
|
+
} {
|
|
144
|
+
const counts = this.stats();
|
|
145
|
+
|
|
146
|
+
// Average task duration by repo (only completed/failed tasks with both timestamps)
|
|
147
|
+
const avgDurationByRepo = this.db
|
|
148
|
+
.query(
|
|
149
|
+
`SELECT repo,
|
|
150
|
+
AVG((julianday(completed_at) - julianday(created_at)) * 86400000) as avg_ms,
|
|
151
|
+
COUNT(*) as count
|
|
152
|
+
FROM tasks
|
|
153
|
+
WHERE completed_at IS NOT NULL AND created_at IS NOT NULL
|
|
154
|
+
AND status IN ('completed', 'failed')
|
|
155
|
+
GROUP BY repo
|
|
156
|
+
ORDER BY count DESC`
|
|
157
|
+
)
|
|
158
|
+
.all() as { repo: string; avg_ms: number; count: number }[];
|
|
159
|
+
|
|
160
|
+
// Task throughput — completed in last hour and last 24h
|
|
161
|
+
const now = new Date().toISOString();
|
|
162
|
+
const throughputRow = this.db
|
|
163
|
+
.query(
|
|
164
|
+
`SELECT
|
|
165
|
+
COUNT(*) FILTER (WHERE completed_at >= datetime(?, '-1 hour')) as last_hour,
|
|
166
|
+
COUNT(*) FILTER (WHERE completed_at >= datetime(?, '-24 hours')) as last_24h
|
|
167
|
+
FROM tasks
|
|
168
|
+
WHERE status IN ('completed', 'failed') AND completed_at IS NOT NULL`
|
|
169
|
+
)
|
|
170
|
+
.get(now, now) as { last_hour: number; last_24h: number };
|
|
171
|
+
|
|
172
|
+
// Error rate: failed / total finished
|
|
173
|
+
const total = counts.completed + counts.failed;
|
|
174
|
+
const errorRate = total > 0 ? counts.failed / total : 0;
|
|
175
|
+
|
|
176
|
+
// Per-repo breakdown
|
|
177
|
+
const repoBreakdown = this.db
|
|
178
|
+
.query(
|
|
179
|
+
`SELECT repo,
|
|
180
|
+
COUNT(*) FILTER (WHERE status = 'pending') as pending,
|
|
181
|
+
COUNT(*) FILTER (WHERE status = 'running') as running,
|
|
182
|
+
COUNT(*) FILTER (WHERE status = 'completed') as completed,
|
|
183
|
+
COUNT(*) FILTER (WHERE status = 'failed') as failed
|
|
184
|
+
FROM tasks
|
|
185
|
+
GROUP BY repo
|
|
186
|
+
ORDER BY (COUNT(*) FILTER (WHERE status = 'running') + COUNT(*) FILTER (WHERE status = 'pending')) DESC, repo ASC`
|
|
187
|
+
)
|
|
188
|
+
.all() as { repo: string; pending: number; running: number; completed: number; failed: number }[];
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
counts,
|
|
192
|
+
avgDurationByRepo: avgDurationByRepo.map((r) => ({
|
|
193
|
+
repo: r.repo,
|
|
194
|
+
avgMs: Math.round(r.avg_ms),
|
|
195
|
+
count: r.count,
|
|
196
|
+
})),
|
|
197
|
+
throughput: {
|
|
198
|
+
lastHour: throughputRow.last_hour,
|
|
199
|
+
last24h: throughputRow.last_24h,
|
|
200
|
+
},
|
|
201
|
+
errorRate: Math.round(errorRate * 10000) / 10000, // 4 decimal places
|
|
202
|
+
repoBreakdown,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
129
206
|
listActive(limit: number = 20): Task[] {
|
|
130
207
|
const rows = this.db
|
|
131
208
|
.query(
|
|
132
|
-
`SELECT * FROM tasks WHERE status IN ('running', 'pending') ORDER BY created_at ASC LIMIT ?`
|
|
209
|
+
`SELECT * FROM tasks WHERE status IN ('running', 'pending') ORDER BY priority DESC, created_at ASC LIMIT ?`
|
|
133
210
|
)
|
|
134
211
|
.all(limit) as TaskRow[];
|
|
135
212
|
return rows.map((r) => this.rowToTask(r));
|
|
@@ -159,7 +236,7 @@ export class TaskQueue {
|
|
|
159
236
|
sql += ` WHERE status = ?`;
|
|
160
237
|
params.push(status);
|
|
161
238
|
}
|
|
162
|
-
sql += ` ORDER BY created_at DESC LIMIT ?`;
|
|
239
|
+
sql += ` ORDER BY priority DESC, created_at DESC LIMIT ?`;
|
|
163
240
|
params.push(limit);
|
|
164
241
|
const rows = this.db.query(sql).all(...params) as TaskRow[];
|
|
165
242
|
return rows.map((r) => this.rowToTask(r));
|
|
@@ -182,6 +259,7 @@ export class TaskQueue {
|
|
|
182
259
|
status: row.status as "pending" | "running" | "completed" | "failed",
|
|
183
260
|
result: row.result,
|
|
184
261
|
taskType: row.task_type || null,
|
|
262
|
+
priority: row.priority ?? 0,
|
|
185
263
|
createdAt: row.created_at,
|
|
186
264
|
completedAt: row.completed_at,
|
|
187
265
|
};
|
package/src/router.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from "bun:test";
|
|
2
|
-
import { parseMessage, buildPrompt, buildCronPrompt } from "./router";
|
|
2
|
+
import { parseMessage, buildPrompt, buildCronPrompt, parsePriority } from "./router";
|
|
3
3
|
|
|
4
4
|
describe("parseMessage", () => {
|
|
5
5
|
it("parses PR review command", () => {
|
|
@@ -260,6 +260,73 @@ describe("parseMessage", () => {
|
|
|
260
260
|
});
|
|
261
261
|
});
|
|
262
262
|
|
|
263
|
+
describe("set-mode parsing", () => {
|
|
264
|
+
it("parses 'mode assistant'", () => {
|
|
265
|
+
const result = parseMessage("mode assistant");
|
|
266
|
+
expect(result.type).toBe("set-mode");
|
|
267
|
+
expect(result.args.mode).toBe("assistant");
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("parses 'mode strict'", () => {
|
|
271
|
+
const result = parseMessage("mode strict");
|
|
272
|
+
expect(result.type).toBe("set-mode");
|
|
273
|
+
expect(result.args.mode).toBe("strict");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("parses '/mode assistant'", () => {
|
|
277
|
+
const result = parseMessage("/mode assistant");
|
|
278
|
+
expect(result.type).toBe("set-mode");
|
|
279
|
+
expect(result.args.mode).toBe("assistant");
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("parses 'assistant mode'", () => {
|
|
283
|
+
const result = parseMessage("assistant mode");
|
|
284
|
+
expect(result.type).toBe("set-mode");
|
|
285
|
+
expect(result.args.mode).toBe("assistant");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("parses 'yolo mode'", () => {
|
|
289
|
+
const result = parseMessage("yolo mode");
|
|
290
|
+
expect(result.type).toBe("set-mode");
|
|
291
|
+
expect(result.args.mode).toBe("assistant");
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("parses 'be more helpful'", () => {
|
|
295
|
+
const result = parseMessage("be more helpful");
|
|
296
|
+
expect(result.type).toBe("set-mode");
|
|
297
|
+
expect(result.args.mode).toBe("assistant");
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("parses 'help me with anything'", () => {
|
|
301
|
+
const result = parseMessage("help me with anything");
|
|
302
|
+
expect(result.type).toBe("set-mode");
|
|
303
|
+
expect(result.args.mode).toBe("assistant");
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("parses 'strict mode'", () => {
|
|
307
|
+
const result = parseMessage("strict mode");
|
|
308
|
+
expect(result.type).toBe("set-mode");
|
|
309
|
+
expect(result.args.mode).toBe("strict");
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("parses 'code mode'", () => {
|
|
313
|
+
const result = parseMessage("code mode");
|
|
314
|
+
expect(result.type).toBe("set-mode");
|
|
315
|
+
expect(result.args.mode).toBe("strict");
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("parses 'back to normal'", () => {
|
|
319
|
+
const result = parseMessage("back to normal");
|
|
320
|
+
expect(result.type).toBe("set-mode");
|
|
321
|
+
expect(result.args.mode).toBe("strict");
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("does NOT match 'help me fix a bug on my-app' as set-mode", () => {
|
|
325
|
+
const result = parseMessage("help me fix a bug on my-app");
|
|
326
|
+
expect(result.type).not.toBe("set-mode");
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
263
330
|
describe("buildPrompt", () => {
|
|
264
331
|
it("builds review-pr prompt", () => {
|
|
265
332
|
const prompt = buildPrompt({ type: "review-pr", repo: "my-app", args: { prNumber: 42 }, rawText: "" });
|
|
@@ -314,3 +381,86 @@ describe("buildCronPrompt", () => {
|
|
|
314
381
|
expect(prompt).toContain(original);
|
|
315
382
|
});
|
|
316
383
|
});
|
|
384
|
+
|
|
385
|
+
describe("parsePriority", () => {
|
|
386
|
+
it("returns 0 for normal text", () => {
|
|
387
|
+
const result = parsePriority("fix the login bug");
|
|
388
|
+
expect(result.priority).toBe(0);
|
|
389
|
+
expect(result.text).toBe("fix the login bug");
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("parses 'urgent:' prefix as priority 2", () => {
|
|
393
|
+
const result = parsePriority("urgent: fix the login bug");
|
|
394
|
+
expect(result.priority).toBe(2);
|
|
395
|
+
expect(result.text).toBe("fix the login bug");
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("parses '!important' marker as priority 1", () => {
|
|
399
|
+
const result = parsePriority("fix the login bug !important");
|
|
400
|
+
expect(result.priority).toBe(1);
|
|
401
|
+
expect(result.text).toBe("fix the login bug");
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it("parses 'p1' as priority 1 (high)", () => {
|
|
405
|
+
const result = parsePriority("fix the login bug p1");
|
|
406
|
+
expect(result.priority).toBe(1);
|
|
407
|
+
expect(result.text).toBe("fix the login bug");
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it("parses 'p0' as priority 2 (urgent)", () => {
|
|
411
|
+
const result = parsePriority("p0 fix the login bug");
|
|
412
|
+
expect(result.priority).toBe(2);
|
|
413
|
+
expect(result.text).toBe("fix the login bug");
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("parses 'p2' as priority 0 (normal)", () => {
|
|
417
|
+
const result = parsePriority("fix the login bug p2");
|
|
418
|
+
expect(result.priority).toBe(0);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it("parses '--priority high' as priority 1", () => {
|
|
422
|
+
const result = parsePriority("fix the login bug --priority high");
|
|
423
|
+
expect(result.priority).toBe(1);
|
|
424
|
+
expect(result.text).toBe("fix the login bug");
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it("parses '--priority urgent' as priority 2", () => {
|
|
428
|
+
const result = parsePriority("fix the login bug --priority urgent");
|
|
429
|
+
expect(result.priority).toBe(2);
|
|
430
|
+
expect(result.text).toBe("fix the login bug");
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it("parses '--priority normal' as priority 0", () => {
|
|
434
|
+
const result = parsePriority("fix the login bug --priority normal");
|
|
435
|
+
expect(result.priority).toBe(0);
|
|
436
|
+
expect(result.text).toBe("fix the login bug");
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
describe("parseMessage priority integration", () => {
|
|
441
|
+
it("default priority is 0", () => {
|
|
442
|
+
const result = parseMessage("fix issue #15 on infra");
|
|
443
|
+
expect(result.priority).toBe(0);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("detects urgent: prefix and sets priority 2", () => {
|
|
447
|
+
const result = parseMessage("urgent: fix the login bug on my-app");
|
|
448
|
+
expect(result.priority).toBe(2);
|
|
449
|
+
expect(result.type).toBe("free-form");
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it("detects !important and sets priority 1", () => {
|
|
453
|
+
const result = parseMessage("fix issue #15 on infra !important");
|
|
454
|
+
expect(result.priority).toBe(1);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("detects p1 and sets priority 1", () => {
|
|
458
|
+
const result = parseMessage("review PR #42 on my-app p1");
|
|
459
|
+
expect(result.priority).toBe(1);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it("detects --priority urgent and sets priority 2", () => {
|
|
463
|
+
const result = parseMessage("fix the login bug on my-app --priority urgent");
|
|
464
|
+
expect(result.priority).toBe(2);
|
|
465
|
+
});
|
|
466
|
+
});
|