agent-dealer 1.1.5 → 1.1.6
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/bundle/server/dist/repository/issues.js +58 -2
- package/bundle/server/dist/repository/issues.test.js +93 -2
- package/bundle/server/dist/routes/issues.js +32 -6
- package/bundle/server/dist/routes/issues.test.js +106 -0
- package/bundle/server/package.json +2 -2
- package/bundle/server/static-ui/assets/index-BTqSHauT.js +60 -0
- package/bundle/server/static-ui/assets/index-BrC-jEt_.css +1 -0
- package/bundle/server/static-ui/index.html +2 -2
- package/bundle/shared/dist/execution-report.d.ts +7 -0
- package/bundle/shared/dist/execution-report.js +17 -0
- package/bundle/shared/dist/execution-report.test.js +11 -1
- package/bundle/shared/package.json +1 -1
- package/package.json +1 -1
- package/bundle/server/static-ui/assets/index-D9CPv8TJ.css +0 -1
- package/bundle/server/static-ui/assets/index-DnsbK_sC.js +0 -60
|
@@ -105,16 +105,72 @@ export function getIssue(id) {
|
|
|
105
105
|
export function listIssues(status) {
|
|
106
106
|
const db = getDb();
|
|
107
107
|
if (!status) {
|
|
108
|
-
const rows = db
|
|
108
|
+
const rows = db
|
|
109
|
+
.prepare("SELECT * FROM issues ORDER BY updated_at DESC, rowid DESC")
|
|
110
|
+
.all();
|
|
109
111
|
return rows.map(rowToIssue);
|
|
110
112
|
}
|
|
111
113
|
const statuses = Array.isArray(status) ? status : [status];
|
|
112
114
|
const placeholders = statuses.map(() => "?").join(",");
|
|
113
115
|
const rows = db
|
|
114
|
-
.prepare(`SELECT * FROM issues WHERE status IN (${placeholders}) ORDER BY updated_at DESC`)
|
|
116
|
+
.prepare(`SELECT * FROM issues WHERE status IN (${placeholders}) ORDER BY updated_at DESC, rowid DESC`)
|
|
115
117
|
.all(...statuses);
|
|
116
118
|
return rows.map(rowToIssue);
|
|
117
119
|
}
|
|
120
|
+
/** NOT-228: page-size bounds for the Issues list (matches the report default/max). */
|
|
121
|
+
export const ISSUES_LIST_DEFAULT_LIMIT = 25;
|
|
122
|
+
export const ISSUES_LIST_MAX_LIMIT = 100;
|
|
123
|
+
/** Escape the LIKE metacharacters in a user query so `%`/`_` match literally. */
|
|
124
|
+
function escapeLikePattern(raw) {
|
|
125
|
+
return raw.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* NOT-228: server-side filtering + stable pagination for the Issues list.
|
|
129
|
+
* Ordering is `updated_at DESC, rowid DESC` so pages are repeatable when many
|
|
130
|
+
* rows share a timestamp; the filter applies before pagination so `total` and
|
|
131
|
+
* `totalPages` describe the full matching cohort.
|
|
132
|
+
*/
|
|
133
|
+
export function queryIssues(query = {}) {
|
|
134
|
+
const db = getDb();
|
|
135
|
+
const conditions = [];
|
|
136
|
+
const params = [];
|
|
137
|
+
const rawStatuses = query.status === undefined ? [] : Array.isArray(query.status) ? query.status : [query.status];
|
|
138
|
+
const statuses = rawStatuses.map((s) => String(s).trim()).filter(Boolean);
|
|
139
|
+
if (statuses.length > 0) {
|
|
140
|
+
conditions.push(`status IN (${statuses.map(() => "?").join(",")})`);
|
|
141
|
+
params.push(...statuses);
|
|
142
|
+
}
|
|
143
|
+
const search = query.search?.trim();
|
|
144
|
+
if (search) {
|
|
145
|
+
const pattern = `%${escapeLikePattern(search).toLowerCase()}%`;
|
|
146
|
+
conditions.push(`(LOWER(title) LIKE ? ESCAPE '\\' OR LOWER(COALESCE(external_label, '')) LIKE ? ESCAPE '\\')`);
|
|
147
|
+
params.push(pattern, pattern);
|
|
148
|
+
}
|
|
149
|
+
const repo = query.repo?.trim();
|
|
150
|
+
if (repo) {
|
|
151
|
+
conditions.push(`repo = ?`);
|
|
152
|
+
params.push(repo);
|
|
153
|
+
}
|
|
154
|
+
if (query.needsAttention) {
|
|
155
|
+
conditions.push(`EXISTS (SELECT 1 FROM human_actions ha WHERE ha.issue_id = issues.id AND ha.status = 'open')`);
|
|
156
|
+
}
|
|
157
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
158
|
+
const rawLimit = query.limit === undefined ? ISSUES_LIST_DEFAULT_LIMIT : Math.floor(query.limit);
|
|
159
|
+
const limit = !Number.isFinite(rawLimit) || rawLimit < 1
|
|
160
|
+
? ISSUES_LIST_DEFAULT_LIMIT
|
|
161
|
+
: Math.min(rawLimit, ISSUES_LIST_MAX_LIMIT);
|
|
162
|
+
const rawPage = query.page === undefined ? 1 : Math.floor(query.page);
|
|
163
|
+
const page = !Number.isFinite(rawPage) || rawPage < 1 ? 1 : rawPage;
|
|
164
|
+
const { total } = db
|
|
165
|
+
.prepare(`SELECT COUNT(*) AS total FROM issues ${where}`)
|
|
166
|
+
.get(...params);
|
|
167
|
+
const totalPages = total === 0 ? 0 : Math.ceil(total / limit);
|
|
168
|
+
const offset = (page - 1) * limit;
|
|
169
|
+
const rows = db
|
|
170
|
+
.prepare(`SELECT * FROM issues ${where} ORDER BY updated_at DESC, rowid DESC LIMIT ? OFFSET ?`)
|
|
171
|
+
.all(...params, limit, offset);
|
|
172
|
+
return { rows: rows.map(rowToIssue), page, limit, total, totalPages };
|
|
173
|
+
}
|
|
118
174
|
/**
|
|
119
175
|
* Every issue ever imported for this external id, newest first. `(source, external_id)` is
|
|
120
176
|
* deliberately non-unique (`idx_issues_external` is a plain index): one ticket may need a
|
|
@@ -4,9 +4,10 @@ import fs from "node:fs";
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
process.env.AGENT_DEALER_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-issues-"));
|
|
7
|
-
const { migrate } = await import("../db/index.js");
|
|
7
|
+
const { migrate, getDb } = await import("../db/index.js");
|
|
8
8
|
const { BUILTIN_AGENT_CLAUDE_ID, BUILTIN_AGENT_CURSOR_ID } = await import("@agent-dealer/shared");
|
|
9
|
-
const { createIssue, getIssue, listIssues, transitionIssue, incrementIssueRound, findActiveIssueByExternalId, listIssuesByExternalId } = await import("./issues.js");
|
|
9
|
+
const { createIssue, getIssue, listIssues, transitionIssue, incrementIssueRound, findActiveIssueByExternalId, listIssuesByExternalId, queryIssues, ISSUES_LIST_DEFAULT_LIMIT, ISSUES_LIST_MAX_LIMIT } = await import("./issues.js");
|
|
10
|
+
const { createHumanAction, resolveHumanAction } = await import("./human-actions.js");
|
|
10
11
|
before(() => {
|
|
11
12
|
migrate();
|
|
12
13
|
});
|
|
@@ -67,3 +68,93 @@ test("terminal-scoped lookup is per (source, externalId) pair", () => {
|
|
|
67
68
|
assert.equal(findActiveIssueByExternalId("linear", "SHARED-1")?.id, linear.id);
|
|
68
69
|
assert.equal(findActiveIssueByExternalId("manual", "SHARED-1"), null);
|
|
69
70
|
});
|
|
71
|
+
test("NOT-228: search matches title case-insensitively and ignores unrelated rows", () => {
|
|
72
|
+
const hit = createIssue(makeInput("NOT228 Quokka migration wobble"));
|
|
73
|
+
createIssue(makeInput("NOT228 unrelated zebra"));
|
|
74
|
+
const res = queryIssues({ search: " WOBBLE " });
|
|
75
|
+
assert.ok(res.rows.some((i) => i.id === hit.id), "title match survives case/whitespace");
|
|
76
|
+
assert.ok(res.rows.every((i) => !i.title.includes("unrelated zebra")));
|
|
77
|
+
assert.equal(res.page, 1);
|
|
78
|
+
assert.equal(res.limit, ISSUES_LIST_DEFAULT_LIMIT);
|
|
79
|
+
});
|
|
80
|
+
test("NOT-228: search matches the external label case-insensitively", () => {
|
|
81
|
+
const labeled = createIssue({
|
|
82
|
+
...makeInput("A plain title with no ticket in it"),
|
|
83
|
+
source: "linear",
|
|
84
|
+
externalId: "ext-228-label",
|
|
85
|
+
externalLabel: "NOT-175",
|
|
86
|
+
});
|
|
87
|
+
const res = queryIssues({ search: "not-175" });
|
|
88
|
+
assert.ok(res.rows.some((i) => i.id === labeled.id), "external label match");
|
|
89
|
+
assert.ok(res.rows.every((i) => i.id === labeled.id || (i.title + (i.externalLabel ?? "")).toLowerCase().includes("not-175")), "unrelated rows are excluded");
|
|
90
|
+
});
|
|
91
|
+
test("NOT-228: search treats LIKE metacharacters literally", () => {
|
|
92
|
+
const literal = createIssue(makeInput("NOT228 100% literal percent"));
|
|
93
|
+
createIssue(makeInput("NOT228 1000 literal thousand"));
|
|
94
|
+
const res = queryIssues({ search: "100%" });
|
|
95
|
+
assert.ok(res.rows.some((i) => i.id === literal.id));
|
|
96
|
+
assert.ok(res.rows.every((i) => (i.title + (i.externalLabel ?? "")).toLowerCase().includes("100%")));
|
|
97
|
+
});
|
|
98
|
+
test("NOT-228: status, repo, and search combine before pagination; totals describe the cohort", () => {
|
|
99
|
+
const repo = "github.com/not228/combo";
|
|
100
|
+
const a = createIssue({ ...makeInput("NOT228 combo alpha"), repo });
|
|
101
|
+
const b = createIssue({ ...makeInput("NOT228 combo beta"), repo });
|
|
102
|
+
createIssue({ ...makeInput("NOT228 combo other-repo"), repo: "github.com/not228/other" });
|
|
103
|
+
transitionIssue(b.id, "developing");
|
|
104
|
+
const res = queryIssues({ search: "combo", status: "ready", repo, limit: 1, page: 1 });
|
|
105
|
+
assert.equal(res.total, 1);
|
|
106
|
+
assert.equal(res.totalPages, 1);
|
|
107
|
+
assert.deepEqual(res.rows.map((i) => i.id), [a.id]);
|
|
108
|
+
const both = queryIssues({ search: "combo", repo });
|
|
109
|
+
assert.equal(both.total, 2, "status omitted: both statuses match");
|
|
110
|
+
});
|
|
111
|
+
test("NOT-228: equal updated_at rows paginate repeatably with rowid DESC as tiebreaker", () => {
|
|
112
|
+
const stamp = "2026-09-01T00:00:00.000Z";
|
|
113
|
+
const ids = ["NOT228 tie one", "NOT228 tie two", "NOT228 tie three"].map((t) => createIssue(makeInput(t)).id);
|
|
114
|
+
for (const id of ids) {
|
|
115
|
+
getDb().prepare("UPDATE issues SET updated_at = ? WHERE id = ?").run(stamp, id);
|
|
116
|
+
}
|
|
117
|
+
const first = queryIssues({ search: "NOT228 tie", limit: 2, page: 1 });
|
|
118
|
+
const second = queryIssues({ search: "NOT228 tie", limit: 2, page: 2 });
|
|
119
|
+
assert.equal(first.total, 3);
|
|
120
|
+
assert.equal(first.totalPages, 2);
|
|
121
|
+
// Newest rowid first: creation order was one, two, three.
|
|
122
|
+
assert.deepEqual(first.rows.map((i) => i.id), [ids[2], ids[1]]);
|
|
123
|
+
assert.deepEqual(second.rows.map((i) => i.id), [ids[0]]);
|
|
124
|
+
const again = queryIssues({ search: "NOT228 tie", limit: 2, page: 1 });
|
|
125
|
+
assert.deepEqual(again.rows.map((i) => i.id), first.rows.map((i) => i.id), "repeatable");
|
|
126
|
+
});
|
|
127
|
+
test("NOT-228: page/limit bounds clamp instead of erroring", () => {
|
|
128
|
+
const res = queryIssues({ search: "NOT228 tie", page: 0, limit: 0 });
|
|
129
|
+
assert.equal(res.page, 1);
|
|
130
|
+
assert.equal(res.limit, ISSUES_LIST_DEFAULT_LIMIT);
|
|
131
|
+
const capped = queryIssues({ search: "NOT228 tie", limit: 10_000 });
|
|
132
|
+
assert.equal(capped.limit, ISSUES_LIST_MAX_LIMIT);
|
|
133
|
+
const pastEnd = queryIssues({ search: "NOT228 tie", limit: 2, page: 99 });
|
|
134
|
+
assert.deepEqual(pastEnd.rows, []);
|
|
135
|
+
assert.equal(pastEnd.total, 3, "an out-of-range page keeps the cohort total");
|
|
136
|
+
assert.equal(pastEnd.page, 99);
|
|
137
|
+
const empty = queryIssues({ search: "NOT228 no-such-title-xyz" });
|
|
138
|
+
assert.deepEqual(empty.rows, []);
|
|
139
|
+
assert.equal(empty.total, 0);
|
|
140
|
+
assert.equal(empty.totalPages, 0);
|
|
141
|
+
});
|
|
142
|
+
test("NOT-228: needsAttention returns only issues with an open human action", () => {
|
|
143
|
+
const flagged = createIssue(makeInput("NOT228 flagged attention"));
|
|
144
|
+
const quiet = createIssue(makeInput("NOT228 quiet no-action"));
|
|
145
|
+
const action = createHumanAction({
|
|
146
|
+
issueId: flagged.id,
|
|
147
|
+
actionType: "final_review",
|
|
148
|
+
reason: "Reviewer approved",
|
|
149
|
+
question: "Accept?",
|
|
150
|
+
responseOptions: ["complete", "repair", "close"],
|
|
151
|
+
});
|
|
152
|
+
let res = queryIssues({ search: "NOT228 flagged attention", needsAttention: true });
|
|
153
|
+
assert.deepEqual(res.rows.map((i) => i.id), [flagged.id]);
|
|
154
|
+
res = queryIssues({ search: "NOT228 quiet no-action", needsAttention: true });
|
|
155
|
+
assert.deepEqual(res.rows, [], "issues without open actions are excluded");
|
|
156
|
+
resolveHumanAction(action.id, "tester", { choice: "complete" });
|
|
157
|
+
res = queryIssues({ search: "NOT228 flagged attention", needsAttention: true });
|
|
158
|
+
assert.deepEqual(res.rows, [], "resolved actions no longer flag the issue");
|
|
159
|
+
assert.ok(queryIssues({ search: "NOT228 quiet no-action" }).rows.some((i) => i.id === quiet.id));
|
|
160
|
+
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import { CreateIssueInput, UpdateIssueInput, } from "@agent-dealer/shared";
|
|
4
4
|
import { getDb } from "../db/index.js";
|
|
5
|
-
import { createIssue, getIssue, listIssues, findActiveIssueByExternalId, listIssuesByExternalId, updateIssue, listRecentRepos } from "../repository/issues.js";
|
|
5
|
+
import { createIssue, getIssue, listIssues, findActiveIssueByExternalId, listIssuesByExternalId, queryIssues, updateIssue, listRecentRepos } from "../repository/issues.js";
|
|
6
6
|
import { listWorkerSessionsForIssue, getActiveWorkerSessionForIssue } from "../repository/worker-sessions.js";
|
|
7
7
|
import { getIssueArtifact, listArtifactsForIssue } from "../repository/artifacts-for-issue.js";
|
|
8
8
|
import { listUsageEventsForIssue, summarizeIssueUsage } from "../repository/usage-events.js";
|
|
@@ -52,10 +52,8 @@ function readTraceTail(filePath, maxChars) {
|
|
|
52
52
|
}
|
|
53
53
|
export async function registerIssueRoutes(app) {
|
|
54
54
|
app.get("/api/issues", async (req) => {
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
const openActionIssueIds = new Set(listOpenHumanActions().map((a) => a.issueId));
|
|
58
|
-
return issues.map((issue) => ({
|
|
55
|
+
const query = req.query;
|
|
56
|
+
const toRow = (openActionIssueIds) => (issue) => ({
|
|
59
57
|
id: issue.id,
|
|
60
58
|
title: issue.title,
|
|
61
59
|
status: issue.status,
|
|
@@ -63,7 +61,35 @@ export async function registerIssueRoutes(app) {
|
|
|
63
61
|
currentIntent: issue.currentIntent,
|
|
64
62
|
updatedAt: issue.updatedAt,
|
|
65
63
|
hasOpenHumanAction: openActionIssueIds.has(issue.id),
|
|
66
|
-
})
|
|
64
|
+
});
|
|
65
|
+
// NOT-228: paginated web requests pass `page` and/or `limit` and get rows
|
|
66
|
+
// plus `{ page, limit, total, totalPages }`. Legacy callers (notably the
|
|
67
|
+
// CLI status query) omit both and keep the existing unpaginated array.
|
|
68
|
+
if (query.page !== undefined || query.limit !== undefined) {
|
|
69
|
+
const statuses = query.status
|
|
70
|
+
? query.status.split(",").map((s) => s.trim()).filter(Boolean)
|
|
71
|
+
: undefined;
|
|
72
|
+
const result = queryIssues({
|
|
73
|
+
search: query.q,
|
|
74
|
+
status: statuses,
|
|
75
|
+
repo: query.repo,
|
|
76
|
+
needsAttention: query.needsAttention === "1" || query.needsAttention === "true",
|
|
77
|
+
page: query.page !== undefined ? Number(query.page) : undefined,
|
|
78
|
+
limit: query.limit !== undefined ? Number(query.limit) : undefined,
|
|
79
|
+
});
|
|
80
|
+
const openActionIssueIds = new Set(listOpenHumanActions().map((a) => a.issueId));
|
|
81
|
+
return {
|
|
82
|
+
issues: result.rows.map(toRow(openActionIssueIds)),
|
|
83
|
+
page: result.page,
|
|
84
|
+
limit: result.limit,
|
|
85
|
+
total: result.total,
|
|
86
|
+
totalPages: result.totalPages,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const status = query.status;
|
|
90
|
+
const issues = status ? listIssues(status.split(",")) : listIssues();
|
|
91
|
+
const openActionIssueIds = new Set(listOpenHumanActions().map((a) => a.issueId));
|
|
92
|
+
return issues.map(toRow(openActionIssueIds));
|
|
67
93
|
});
|
|
68
94
|
/** Recent portable GitHub repo identities (legacy local paths excluded from create UI). */
|
|
69
95
|
app.get("/api/issues/recent-repos", async () => {
|
|
@@ -16,6 +16,7 @@ const { migrate, getDb } = await import("../db/index.js");
|
|
|
16
16
|
const { BUILTIN_AGENT_CLAUDE_ID, BUILTIN_AGENT_CURSOR_ID } = await import("@agent-dealer/shared");
|
|
17
17
|
const { registerIssueRoutes } = await import("./issues.js");
|
|
18
18
|
const { transitionIssue, listIssuesByExternalId, getIssue } = await import("../repository/issues.js");
|
|
19
|
+
const { createHumanAction } = await import("../repository/human-actions.js");
|
|
19
20
|
const { createIssueArtifact } = await import("../repository/artifacts.js");
|
|
20
21
|
const { claimWorkItem } = await import("../repository/work-items.js");
|
|
21
22
|
const { listHumanActionsForIssue } = await import("../repository/human-actions.js");
|
|
@@ -564,6 +565,41 @@ test("NOT-148: GET /api/issues/:id surfaces branchTipStatus with restart risk af
|
|
|
564
565
|
assert.equal(idle.json().branchTipStatus, null);
|
|
565
566
|
await app.close();
|
|
566
567
|
});
|
|
568
|
+
// NOT-228: helpers + focused tests for Issues list filtering/pagination. Every
|
|
569
|
+
// test wipes `issues` in beforeEach, so the `R228` title marker scopes each
|
|
570
|
+
// cohort without cross-test interference.
|
|
571
|
+
const R228_PAYLOAD = {
|
|
572
|
+
repo: "acme/app",
|
|
573
|
+
baseBranch: "main",
|
|
574
|
+
developerAgentId: BUILTIN_AGENT_CLAUDE_ID,
|
|
575
|
+
reviewerAgentId: BUILTIN_AGENT_CURSOR_ID,
|
|
576
|
+
};
|
|
577
|
+
async function r228Seed(app, title, extra = {}) {
|
|
578
|
+
const res = await app.inject({
|
|
579
|
+
method: "POST",
|
|
580
|
+
url: "/api/issues",
|
|
581
|
+
payload: { ...R228_PAYLOAD, ...extra, title },
|
|
582
|
+
});
|
|
583
|
+
assert.equal(res.statusCode, 200);
|
|
584
|
+
return res.json();
|
|
585
|
+
}
|
|
586
|
+
async function r228Get(app, qs) {
|
|
587
|
+
const res = await app.inject({ method: "GET", url: `/api/issues${qs}` });
|
|
588
|
+
assert.equal(res.statusCode, 200);
|
|
589
|
+
return res.json();
|
|
590
|
+
}
|
|
591
|
+
test("NOT-228: omitting page/limit keeps the legacy unpaginated array (CLI contract)", async () => {
|
|
592
|
+
const app = await buildApp();
|
|
593
|
+
const created = await r228Seed(app, "R228 legacy contract");
|
|
594
|
+
for (const url of ["/api/issues", "/api/issues?status=ready", "/api/issues?q=R228&repo=github.com%2Facme%2Fapp"]) {
|
|
595
|
+
const res = await app.inject({ method: "GET", url });
|
|
596
|
+
assert.equal(res.statusCode, 200);
|
|
597
|
+
const body = res.json();
|
|
598
|
+
assert.ok(Array.isArray(body), `${url} still answers an array`);
|
|
599
|
+
assert.ok(body.some((i) => i.id === created.id));
|
|
600
|
+
}
|
|
601
|
+
await app.close();
|
|
602
|
+
});
|
|
567
603
|
test("NOT-217: PATCH agent assignments on a queued issue preserves position, refreshes the wait reason, and records issue.reassigned", async () => {
|
|
568
604
|
const app = await buildApp();
|
|
569
605
|
try {
|
|
@@ -623,6 +659,76 @@ test("NOT-217: PATCH agent assignments on a queued issue preserves position, ref
|
|
|
623
659
|
}
|
|
624
660
|
await app.close();
|
|
625
661
|
});
|
|
662
|
+
test("NOT-228: page/limit paginate with a default of 25 and totals for the full cohort", async () => {
|
|
663
|
+
const app = await buildApp();
|
|
664
|
+
for (let n = 0; n < 30; n++)
|
|
665
|
+
await r228Seed(app, `R228 pageable ${n}`);
|
|
666
|
+
const q = encodeURIComponent("R228 pageable");
|
|
667
|
+
const first = await r228Get(app, `?q=${q}&limit=25`);
|
|
668
|
+
assert.equal(first.issues.length, 25);
|
|
669
|
+
assert.equal(first.page, 1);
|
|
670
|
+
assert.equal(first.limit, 25);
|
|
671
|
+
assert.equal(first.total, 30);
|
|
672
|
+
assert.equal(first.totalPages, 2);
|
|
673
|
+
const second = await r228Get(app, `?q=${q}&limit=25&page=2`);
|
|
674
|
+
assert.equal(second.issues.length, 5);
|
|
675
|
+
assert.equal(second.total, 30);
|
|
676
|
+
assert.deepEqual(new Set([...first.issues, ...second.issues].map((i) => i.id)).size, 30, "pages neither duplicate nor skip rows");
|
|
677
|
+
// `page` alone still paginates (default limit); `limit` alone starts at page 1.
|
|
678
|
+
const defaulted = await r228Get(app, `?q=${q}&page=2`);
|
|
679
|
+
assert.equal(defaulted.limit, 25);
|
|
680
|
+
assert.equal(defaulted.issues.length, 5);
|
|
681
|
+
await app.close();
|
|
682
|
+
});
|
|
683
|
+
test("NOT-228: search, status, repo, and attention filters apply before pagination", async () => {
|
|
684
|
+
const app = await buildApp();
|
|
685
|
+
const repo = "github.com/r228/filtered";
|
|
686
|
+
const keep = await r228Seed(app, "R228 filter keep me", { repo });
|
|
687
|
+
const moved = await r228Seed(app, "R228 filter moved on", { repo });
|
|
688
|
+
await r228Seed(app, "R228 filter other repo", { repo: "github.com/r228/elsewhere" });
|
|
689
|
+
transitionIssue(moved.id, "developing");
|
|
690
|
+
createHumanAction({
|
|
691
|
+
issueId: moved.id,
|
|
692
|
+
actionType: "final_review",
|
|
693
|
+
reason: "Reviewer approved",
|
|
694
|
+
question: "Accept?",
|
|
695
|
+
responseOptions: ["complete"],
|
|
696
|
+
});
|
|
697
|
+
const q = encodeURIComponent("R228 filter");
|
|
698
|
+
const byStatus = await r228Get(app, `?q=${q}&status=developing&page=1&limit=10`);
|
|
699
|
+
assert.equal(byStatus.total, 1);
|
|
700
|
+
assert.deepEqual(byStatus.issues.map((i) => i.id), [moved.id]);
|
|
701
|
+
const byRepo = await r228Get(app, `?q=${q}&repo=${encodeURIComponent(repo)}&page=1&limit=10`);
|
|
702
|
+
assert.equal(byRepo.total, 2);
|
|
703
|
+
const attention = await r228Get(app, `?q=${q}&needsAttention=1&page=1&limit=10`);
|
|
704
|
+
assert.equal(attention.total, 1);
|
|
705
|
+
assert.deepEqual(attention.issues.map((i) => i.id), [moved.id]);
|
|
706
|
+
assert.equal(attention.issues[0].hasOpenHumanAction, true);
|
|
707
|
+
// Search matches the external label, not just the title: no "R228 filter"
|
|
708
|
+
// title contains "not-228", so only the labeled row matches.
|
|
709
|
+
const labeled = await r228Seed(app, "R228 filter label-only title", {
|
|
710
|
+
source: "linear",
|
|
711
|
+
externalId: "R228-LBL-1",
|
|
712
|
+
externalLabel: "NOT-228",
|
|
713
|
+
});
|
|
714
|
+
const byLabel = await r228Get(app, `?q=${encodeURIComponent("not-228")}&page=1&limit=10`);
|
|
715
|
+
assert.equal(byLabel.total, 1);
|
|
716
|
+
assert.deepEqual(byLabel.issues.map((i) => i.id), [labeled.id]);
|
|
717
|
+
void keep;
|
|
718
|
+
await app.close();
|
|
719
|
+
});
|
|
720
|
+
test("NOT-228: limit clamps to 100 and out-of-range pages keep totals", async () => {
|
|
721
|
+
const app = await buildApp();
|
|
722
|
+
await r228Seed(app, "R228 bounds one");
|
|
723
|
+
const capped = await r228Get(app, `?q=${encodeURIComponent("R228 bounds")}&page=1&limit=500`);
|
|
724
|
+
assert.equal(capped.limit, 100);
|
|
725
|
+
assert.equal(capped.total, 1);
|
|
726
|
+
const pastEnd = await r228Get(app, `?q=${encodeURIComponent("R228 bounds")}&page=9&limit=10`);
|
|
727
|
+
assert.deepEqual(pastEnd.issues, []);
|
|
728
|
+
assert.equal(pastEnd.total, 1);
|
|
729
|
+
assert.equal(pastEnd.page, 9);
|
|
730
|
+
await app.close();
|
|
731
|
+
});
|
|
626
732
|
test("NOT-217: PATCH reviewer-only edit records before/after reviewers without touching the developer", async () => {
|
|
627
733
|
const app = await buildApp();
|
|
628
734
|
const created = (await app.inject({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-dealer/server",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"typecheck": "tsc --noEmit"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@agent-dealer/shared": "1.1.
|
|
25
|
+
"@agent-dealer/shared": "1.1.6",
|
|
26
26
|
"@fastify/cors": "^11.0.1",
|
|
27
27
|
"@fastify/static": "^8.2.0",
|
|
28
28
|
"@modelcontextprotocol/sdk": "^1.29.0",
|