lynkr 9.9.0 → 9.10.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/README.md +92 -23
- package/bin/cli.js +13 -7
- package/bin/lynkr-init.js +34 -12
- package/bin/lynkr-reset.js +71 -0
- package/config/model-tiers.json +199 -52
- package/package.json +3 -3
- package/scripts/build-eval-set.js +256 -0
- package/scripts/mine-difficulty-anchors.js +288 -0
- package/scripts/validate-difficulty-classifier.js +144 -0
- package/scripts/validate-intent-anchors.js +186 -0
- package/src/api/providers-handler.js +0 -1
- package/src/api/router.js +292 -160
- package/src/clients/databricks.js +95 -6
- package/src/config/index.js +3 -43
- package/src/context/tool-result-compressor.js +883 -40
- package/src/orchestrator/index.js +117 -1235
- package/src/orchestrator/passthrough-stream.js +382 -0
- package/src/orchestrator/sse-transformer.js +408 -0
- package/src/routing/affinity-store.js +17 -3
- package/src/routing/classifier-setup.js +207 -0
- package/src/routing/complexity-analyzer.js +40 -4
- package/src/routing/difficulty-classifier.js +261 -0
- package/src/routing/index.js +70 -7
- package/src/routing/intent-score.js +132 -12
- package/src/routing/session-affinity.js +8 -1
- package/src/routing/side-channel-detector.js +103 -0
- package/src/server.js +20 -46
- package/src/agents/context-manager.js +0 -236
- package/src/agents/decomposition/dispatcher.js +0 -185
- package/src/agents/decomposition/gate.js +0 -136
- package/src/agents/decomposition/index.js +0 -183
- package/src/agents/decomposition/model-call.js +0 -75
- package/src/agents/decomposition/planner.js +0 -223
- package/src/agents/decomposition/synthesizer.js +0 -89
- package/src/agents/decomposition/telemetry.js +0 -55
- package/src/agents/definitions/loader.js +0 -653
- package/src/agents/executor.js +0 -457
- package/src/agents/index.js +0 -165
- package/src/agents/parallel-coordinator.js +0 -68
- package/src/agents/reflector.js +0 -331
- package/src/agents/skillbook.js +0 -331
- package/src/agents/store.js +0 -259
- package/src/edits/index.js +0 -171
- package/src/indexer/babel-parser.js +0 -213
- package/src/indexer/index.js +0 -1629
- package/src/indexer/navigation/index.js +0 -32
- package/src/indexer/navigation/providers/treeSitter.js +0 -36
- package/src/indexer/parser.js +0 -443
- package/src/tasks/store.js +0 -349
- package/src/tests/coverage.js +0 -173
- package/src/tests/index.js +0 -171
- package/src/tests/store.js +0 -213
- package/src/tools/agent-task.js +0 -145
- package/src/tools/code-mode.js +0 -304
- package/src/tools/decompose.js +0 -91
- package/src/tools/edits.js +0 -94
- package/src/tools/execution.js +0 -171
- package/src/tools/git.js +0 -1346
- package/src/tools/index.js +0 -306
- package/src/tools/indexer.js +0 -360
- package/src/tools/lazy-loader.js +0 -366
- package/src/tools/mcp-remote.js +0 -88
- package/src/tools/mcp.js +0 -116
- package/src/tools/process.js +0 -167
- package/src/tools/smart-selection.js +0 -180
- package/src/tools/stubs.js +0 -55
- package/src/tools/tasks.js +0 -260
- package/src/tools/tests.js +0 -132
- package/src/tools/tinyfish.js +0 -358
- package/src/tools/truncate.js +0 -106
- package/src/tools/web-client.js +0 -71
- package/src/tools/web.js +0 -415
- package/src/tools/workspace.js +0 -204
package/src/tasks/store.js
DELETED
|
@@ -1,349 +0,0 @@
|
|
|
1
|
-
const db = require("../db");
|
|
2
|
-
const logger = require("../logger");
|
|
3
|
-
|
|
4
|
-
const ORDER_FIELDS = {
|
|
5
|
-
default: "status ASC, priority DESC, updated_at DESC",
|
|
6
|
-
status: "status ASC, priority DESC, updated_at DESC",
|
|
7
|
-
priority: "priority DESC, updated_at DESC",
|
|
8
|
-
updated: "updated_at DESC",
|
|
9
|
-
created: "created_at DESC",
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
const insertTaskStmt = db.prepare(
|
|
13
|
-
`INSERT INTO tasks (
|
|
14
|
-
title,
|
|
15
|
-
status,
|
|
16
|
-
priority,
|
|
17
|
-
tags,
|
|
18
|
-
linked_file,
|
|
19
|
-
created_at,
|
|
20
|
-
updated_at,
|
|
21
|
-
created_by,
|
|
22
|
-
updated_by,
|
|
23
|
-
metadata
|
|
24
|
-
) VALUES (
|
|
25
|
-
@title,
|
|
26
|
-
@status,
|
|
27
|
-
@priority,
|
|
28
|
-
@tags,
|
|
29
|
-
@linked_file,
|
|
30
|
-
@created_at,
|
|
31
|
-
@updated_at,
|
|
32
|
-
@created_by,
|
|
33
|
-
@updated_by,
|
|
34
|
-
@metadata
|
|
35
|
-
)`,
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
const selectTaskByIdStmt = db.prepare(
|
|
39
|
-
`SELECT
|
|
40
|
-
id,
|
|
41
|
-
title,
|
|
42
|
-
status,
|
|
43
|
-
priority,
|
|
44
|
-
tags,
|
|
45
|
-
linked_file,
|
|
46
|
-
created_at,
|
|
47
|
-
updated_at,
|
|
48
|
-
created_by,
|
|
49
|
-
updated_by,
|
|
50
|
-
metadata
|
|
51
|
-
FROM tasks
|
|
52
|
-
WHERE id = ?`,
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
const deleteTaskStmt = db.prepare("DELETE FROM tasks WHERE id = ?");
|
|
56
|
-
const countTasksByStatusStmt = db.prepare(
|
|
57
|
-
`SELECT status, COUNT(1) AS total
|
|
58
|
-
FROM tasks
|
|
59
|
-
GROUP BY status`,
|
|
60
|
-
);
|
|
61
|
-
const listRecentTasksStmt = db.prepare(
|
|
62
|
-
`SELECT
|
|
63
|
-
id,
|
|
64
|
-
title,
|
|
65
|
-
status,
|
|
66
|
-
priority,
|
|
67
|
-
tags,
|
|
68
|
-
linked_file,
|
|
69
|
-
created_at,
|
|
70
|
-
updated_at,
|
|
71
|
-
created_by,
|
|
72
|
-
updated_by,
|
|
73
|
-
metadata
|
|
74
|
-
FROM tasks
|
|
75
|
-
ORDER BY updated_at DESC
|
|
76
|
-
LIMIT ?`,
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
function resolveOrder(orderBy) {
|
|
80
|
-
if (typeof orderBy === "string") {
|
|
81
|
-
const key = orderBy.toLowerCase().trim();
|
|
82
|
-
if (ORDER_FIELDS[key]) {
|
|
83
|
-
return ORDER_FIELDS[key];
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return ORDER_FIELDS.default;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function buildListQuery({ status, linkedFile, search, limit, orderBy }) {
|
|
90
|
-
let sql = `SELECT
|
|
91
|
-
id,
|
|
92
|
-
title,
|
|
93
|
-
status,
|
|
94
|
-
priority,
|
|
95
|
-
tags,
|
|
96
|
-
linked_file,
|
|
97
|
-
created_at,
|
|
98
|
-
updated_at,
|
|
99
|
-
created_by,
|
|
100
|
-
updated_by,
|
|
101
|
-
metadata
|
|
102
|
-
FROM tasks`;
|
|
103
|
-
const conditions = [];
|
|
104
|
-
const params = [];
|
|
105
|
-
|
|
106
|
-
if (status) {
|
|
107
|
-
conditions.push("status = ?");
|
|
108
|
-
params.push(status);
|
|
109
|
-
}
|
|
110
|
-
if (linkedFile) {
|
|
111
|
-
conditions.push("linked_file = ?");
|
|
112
|
-
params.push(linkedFile);
|
|
113
|
-
}
|
|
114
|
-
if (search) {
|
|
115
|
-
conditions.push("LOWER(title) LIKE ?");
|
|
116
|
-
params.push(`%${search.toLowerCase()}%`);
|
|
117
|
-
}
|
|
118
|
-
if (conditions.length) {
|
|
119
|
-
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
sql += ` ORDER BY ${resolveOrder(orderBy)}`;
|
|
123
|
-
|
|
124
|
-
if (Number.isInteger(limit) && limit > 0) {
|
|
125
|
-
sql += " LIMIT ?";
|
|
126
|
-
params.push(limit);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return { sql, params };
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function normaliseJson(value, fallback) {
|
|
133
|
-
if (value === null || value === undefined) return fallback;
|
|
134
|
-
if (typeof value === "string") {
|
|
135
|
-
try {
|
|
136
|
-
return JSON.parse(value);
|
|
137
|
-
} catch (err) {
|
|
138
|
-
logger.debug({ err }, "Failed to parse JSON column");
|
|
139
|
-
return fallback;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return value;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function normaliseTaskRow(row) {
|
|
146
|
-
if (!row) return null;
|
|
147
|
-
return {
|
|
148
|
-
id: row.id,
|
|
149
|
-
title: row.title,
|
|
150
|
-
status: row.status,
|
|
151
|
-
priority: typeof row.priority === "number" ? row.priority : Number(row.priority ?? 0),
|
|
152
|
-
tags: normaliseJson(row.tags, []),
|
|
153
|
-
linkedFile: row.linked_file ?? null,
|
|
154
|
-
createdAt: row.created_at,
|
|
155
|
-
updatedAt: row.updated_at,
|
|
156
|
-
createdBy: row.created_by ?? null,
|
|
157
|
-
updatedBy: row.updated_by ?? null,
|
|
158
|
-
metadata: normaliseJson(row.metadata, null),
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function serializeTags(tags) {
|
|
163
|
-
if (!tags) return null;
|
|
164
|
-
if (Array.isArray(tags) && tags.length === 0) return "[]";
|
|
165
|
-
try {
|
|
166
|
-
return JSON.stringify(tags);
|
|
167
|
-
} catch (err) {
|
|
168
|
-
logger.debug({ err }, "Failed to serialise tags");
|
|
169
|
-
return null;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function serializeMetadata(metadata) {
|
|
174
|
-
if (!metadata) return null;
|
|
175
|
-
try {
|
|
176
|
-
return JSON.stringify(metadata);
|
|
177
|
-
} catch (err) {
|
|
178
|
-
logger.debug({ err }, "Failed to serialise metadata");
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function getTaskById(id) {
|
|
184
|
-
const row = selectTaskByIdStmt.get(id);
|
|
185
|
-
return normaliseTaskRow(row);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function createTask({
|
|
189
|
-
title,
|
|
190
|
-
status = "todo",
|
|
191
|
-
priority = 0,
|
|
192
|
-
tags = [],
|
|
193
|
-
linkedFile = null,
|
|
194
|
-
createdBy = null,
|
|
195
|
-
metadata = null,
|
|
196
|
-
}) {
|
|
197
|
-
if (typeof title !== "string" || title.trim().length === 0) {
|
|
198
|
-
throw new Error("Task title is required.");
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const now = Date.now();
|
|
202
|
-
const params = {
|
|
203
|
-
title: title.trim(),
|
|
204
|
-
status: typeof status === "string" && status.trim().length ? status.trim() : "todo",
|
|
205
|
-
priority: Number.isFinite(priority) ? Math.trunc(priority) : 0,
|
|
206
|
-
tags: serializeTags(tags),
|
|
207
|
-
linked_file: linkedFile ?? null,
|
|
208
|
-
created_at: now,
|
|
209
|
-
updated_at: now,
|
|
210
|
-
created_by: createdBy ?? null,
|
|
211
|
-
updated_by: createdBy ?? null,
|
|
212
|
-
metadata: serializeMetadata(metadata),
|
|
213
|
-
};
|
|
214
|
-
const result = insertTaskStmt.run(params);
|
|
215
|
-
return getTaskById(result.lastInsertRowid);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function listTasks(options = {}) {
|
|
219
|
-
const { sql, params } = buildListQuery({
|
|
220
|
-
status: options.status,
|
|
221
|
-
linkedFile: options.linkedFile ?? options.path ?? options.file,
|
|
222
|
-
search: options.search ?? options.query ?? null,
|
|
223
|
-
limit: options.limit,
|
|
224
|
-
orderBy: options.orderBy,
|
|
225
|
-
});
|
|
226
|
-
return db
|
|
227
|
-
.prepare(sql)
|
|
228
|
-
.all(...params)
|
|
229
|
-
.map(normaliseTaskRow);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function updateTask(id, updates = {}) {
|
|
233
|
-
if (!id) {
|
|
234
|
-
throw new Error("Task id is required for update.");
|
|
235
|
-
}
|
|
236
|
-
const fields = [];
|
|
237
|
-
const params = {};
|
|
238
|
-
|
|
239
|
-
if (updates.title !== undefined) {
|
|
240
|
-
if (typeof updates.title !== "string" || !updates.title.trim()) {
|
|
241
|
-
throw new Error("Task title must be a non-empty string.");
|
|
242
|
-
}
|
|
243
|
-
fields.push("title = @title");
|
|
244
|
-
params.title = updates.title.trim();
|
|
245
|
-
}
|
|
246
|
-
if (updates.status !== undefined) {
|
|
247
|
-
if (typeof updates.status !== "string" || !updates.status.trim()) {
|
|
248
|
-
throw new Error("Task status must be a non-empty string.");
|
|
249
|
-
}
|
|
250
|
-
fields.push("status = @status");
|
|
251
|
-
params.status = updates.status.trim();
|
|
252
|
-
}
|
|
253
|
-
if (updates.priority !== undefined) {
|
|
254
|
-
fields.push("priority = @priority");
|
|
255
|
-
params.priority = Number.isFinite(updates.priority)
|
|
256
|
-
? Math.trunc(updates.priority)
|
|
257
|
-
: 0;
|
|
258
|
-
}
|
|
259
|
-
if (updates.tags !== undefined) {
|
|
260
|
-
fields.push("tags = @tags");
|
|
261
|
-
params.tags = serializeTags(updates.tags);
|
|
262
|
-
}
|
|
263
|
-
if (updates.linkedFile !== undefined) {
|
|
264
|
-
fields.push("linked_file = @linked_file");
|
|
265
|
-
params.linked_file =
|
|
266
|
-
updates.linkedFile === null || updates.linkedFile === undefined
|
|
267
|
-
? null
|
|
268
|
-
: String(updates.linkedFile);
|
|
269
|
-
}
|
|
270
|
-
if (updates.metadata !== undefined) {
|
|
271
|
-
fields.push("metadata = @metadata");
|
|
272
|
-
params.metadata = serializeMetadata(updates.metadata);
|
|
273
|
-
}
|
|
274
|
-
if (updates.updatedBy !== undefined) {
|
|
275
|
-
fields.push("updated_by = @updated_by");
|
|
276
|
-
params.updated_by =
|
|
277
|
-
updates.updatedBy === null || updates.updatedBy === undefined
|
|
278
|
-
? null
|
|
279
|
-
: String(updates.updatedBy);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (!fields.length) {
|
|
283
|
-
return getTaskById(id);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
params.updated_at = Date.now();
|
|
287
|
-
params.id = id;
|
|
288
|
-
fields.push("updated_at = @updated_at");
|
|
289
|
-
|
|
290
|
-
const sql = `UPDATE tasks SET ${fields.join(", ")} WHERE id = @id`;
|
|
291
|
-
const stmt = db.prepare(sql);
|
|
292
|
-
const result = stmt.run(params);
|
|
293
|
-
if (result.changes === 0) {
|
|
294
|
-
throw new Error(`Task ${id} not found.`);
|
|
295
|
-
}
|
|
296
|
-
return getTaskById(id);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function setTaskStatus(id, status, updatedBy) {
|
|
300
|
-
return updateTask(id, { status, updatedBy });
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function deleteTask(id) {
|
|
304
|
-
if (!id) {
|
|
305
|
-
throw new Error("Task id is required for deletion.");
|
|
306
|
-
}
|
|
307
|
-
const result = deleteTaskStmt.run(id);
|
|
308
|
-
return result.changes > 0;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function normaliseLimit(limit, fallback = 5) {
|
|
312
|
-
if (limit === undefined || limit === null) return fallback;
|
|
313
|
-
const num = Number(limit);
|
|
314
|
-
if (!Number.isFinite(num)) return fallback;
|
|
315
|
-
const clamped = Math.trunc(num);
|
|
316
|
-
if (clamped <= 0) return 0;
|
|
317
|
-
return Math.min(clamped, 50);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function getTaskSummary(options = {}) {
|
|
321
|
-
const counts = countTasksByStatusStmt.all();
|
|
322
|
-
const byStatus = counts.map((row) => ({
|
|
323
|
-
status: row.status,
|
|
324
|
-
total: row.total,
|
|
325
|
-
}));
|
|
326
|
-
const total = byStatus.reduce((acc, item) => acc + item.total, 0);
|
|
327
|
-
const limit = normaliseLimit(options.limit, 5);
|
|
328
|
-
let recent = [];
|
|
329
|
-
if (limit > 0) {
|
|
330
|
-
recent = listRecentTasksStmt
|
|
331
|
-
.all(limit)
|
|
332
|
-
.map(normaliseTaskRow);
|
|
333
|
-
}
|
|
334
|
-
return {
|
|
335
|
-
total,
|
|
336
|
-
byStatus,
|
|
337
|
-
recent,
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
module.exports = {
|
|
342
|
-
createTask,
|
|
343
|
-
listTasks,
|
|
344
|
-
getTaskById,
|
|
345
|
-
updateTask,
|
|
346
|
-
setTaskStatus,
|
|
347
|
-
deleteTask,
|
|
348
|
-
getTaskSummary,
|
|
349
|
-
};
|
package/src/tests/coverage.js
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
const fsp = require("fs/promises");
|
|
2
|
-
const path = require("path");
|
|
3
|
-
const fg = require("fast-glob");
|
|
4
|
-
const logger = require("../logger");
|
|
5
|
-
const { workspaceRoot } = require("../workspace");
|
|
6
|
-
|
|
7
|
-
const COVERAGE_METRICS = ["lines", "statements", "branches", "functions"];
|
|
8
|
-
const OUTPUT_LIMIT = 256;
|
|
9
|
-
|
|
10
|
-
function hasWildcard(pattern) {
|
|
11
|
-
return /[*?[\]{}]/.test(pattern);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function ensureWorkspaceAbsolute(targetPath) {
|
|
15
|
-
const absolute = path.isAbsolute(targetPath)
|
|
16
|
-
? targetPath
|
|
17
|
-
: path.resolve(workspaceRoot, targetPath);
|
|
18
|
-
if (!absolute.startsWith(workspaceRoot)) {
|
|
19
|
-
logger.debug(
|
|
20
|
-
{ targetPath },
|
|
21
|
-
"Ignoring coverage path outside workspace",
|
|
22
|
-
);
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
return absolute;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function expandCoveragePatterns(patterns) {
|
|
29
|
-
if (!Array.isArray(patterns) || patterns.length === 0) return [];
|
|
30
|
-
const results = new Set();
|
|
31
|
-
for (const rawPattern of patterns) {
|
|
32
|
-
if (typeof rawPattern !== "string") continue;
|
|
33
|
-
const pattern = rawPattern.trim();
|
|
34
|
-
if (!pattern) continue;
|
|
35
|
-
if (hasWildcard(pattern)) {
|
|
36
|
-
const matches = fg.sync(pattern, {
|
|
37
|
-
cwd: workspaceRoot,
|
|
38
|
-
dot: true,
|
|
39
|
-
absolute: true,
|
|
40
|
-
onlyFiles: true,
|
|
41
|
-
unique: true,
|
|
42
|
-
});
|
|
43
|
-
matches.forEach((match) => {
|
|
44
|
-
const absolute = ensureWorkspaceAbsolute(match);
|
|
45
|
-
if (absolute) results.add(absolute);
|
|
46
|
-
});
|
|
47
|
-
} else {
|
|
48
|
-
const absolute = ensureWorkspaceAbsolute(pattern);
|
|
49
|
-
if (absolute) results.add(absolute);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return Array.from(results);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function normaliseMetric(rawMetric) {
|
|
56
|
-
if (!rawMetric || typeof rawMetric !== "object") return null;
|
|
57
|
-
const total = Number(rawMetric.total ?? rawMetric.statements ?? rawMetric.lines ?? rawMetric.functions ?? 0);
|
|
58
|
-
const covered = Number(rawMetric.covered ?? rawMetric.hit ?? 0);
|
|
59
|
-
const skipped = Number(rawMetric.skipped ?? 0);
|
|
60
|
-
let pct = rawMetric.pct;
|
|
61
|
-
if (!Number.isFinite(pct)) {
|
|
62
|
-
pct = total > 0 ? (covered / total) * 100 : 0;
|
|
63
|
-
}
|
|
64
|
-
return {
|
|
65
|
-
total: Number.isFinite(total) ? total : null,
|
|
66
|
-
covered: Number.isFinite(covered) ? covered : null,
|
|
67
|
-
skipped: Number.isFinite(skipped) ? skipped : null,
|
|
68
|
-
pct: Number.isFinite(pct) ? Number(pct.toFixed(2)) : null,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async function parseIstanbulCoverage(filePath) {
|
|
73
|
-
try {
|
|
74
|
-
const stats = await fsp.stat(filePath);
|
|
75
|
-
if (!stats.isFile()) return null;
|
|
76
|
-
const raw = await fsp.readFile(filePath, "utf8");
|
|
77
|
-
const data = JSON.parse(raw);
|
|
78
|
-
const totals = data.total ?? data.totals ?? null;
|
|
79
|
-
if (!totals) return null;
|
|
80
|
-
const summary = {};
|
|
81
|
-
let hasMetric = false;
|
|
82
|
-
for (const metric of COVERAGE_METRICS) {
|
|
83
|
-
const value = normaliseMetric(totals[metric]);
|
|
84
|
-
if (value) {
|
|
85
|
-
summary[metric] = value;
|
|
86
|
-
hasMetric = true;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
if (!hasMetric) return null;
|
|
90
|
-
return {
|
|
91
|
-
path: path.relative(workspaceRoot, filePath) || path.basename(filePath),
|
|
92
|
-
absolutePath: filePath,
|
|
93
|
-
type: "istanbul",
|
|
94
|
-
totals: summary,
|
|
95
|
-
generatedAt: new Date(stats.mtimeMs).toISOString(),
|
|
96
|
-
};
|
|
97
|
-
} catch (err) {
|
|
98
|
-
const sample =
|
|
99
|
-
typeof err?.message === "string" && err.message.length > OUTPUT_LIMIT
|
|
100
|
-
? `${err.message.slice(0, OUTPUT_LIMIT)}…`
|
|
101
|
-
: err?.message ?? String(err);
|
|
102
|
-
logger.debug({ err: sample, filePath }, "Failed to parse coverage file");
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
async function parseCoverageFile(filePath) {
|
|
108
|
-
const extension = path.extname(filePath).toLowerCase();
|
|
109
|
-
if (extension === ".json") {
|
|
110
|
-
return parseIstanbulCoverage(filePath);
|
|
111
|
-
}
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function aggregateMetric(values) {
|
|
116
|
-
if (!values.length) return null;
|
|
117
|
-
let total = 0;
|
|
118
|
-
let covered = 0;
|
|
119
|
-
let skipped = 0;
|
|
120
|
-
for (const value of values) {
|
|
121
|
-
total += Number(value.total ?? 0);
|
|
122
|
-
covered += Number(value.covered ?? 0);
|
|
123
|
-
skipped += Number(value.skipped ?? 0);
|
|
124
|
-
}
|
|
125
|
-
const pct = total > 0 ? Number(((covered / total) * 100).toFixed(2)) : null;
|
|
126
|
-
return {
|
|
127
|
-
total: total || null,
|
|
128
|
-
covered,
|
|
129
|
-
skipped,
|
|
130
|
-
pct,
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function synthesiseSummary(entries) {
|
|
135
|
-
if (!Array.isArray(entries) || entries.length === 0) return null;
|
|
136
|
-
const summary = {};
|
|
137
|
-
let hasMetric = false;
|
|
138
|
-
for (const metric of COVERAGE_METRICS) {
|
|
139
|
-
const values = entries
|
|
140
|
-
.map((entry) => entry?.totals?.[metric])
|
|
141
|
-
.filter(Boolean);
|
|
142
|
-
if (values.length === 0) continue;
|
|
143
|
-
const aggregated = aggregateMetric(values);
|
|
144
|
-
if (aggregated) {
|
|
145
|
-
summary[metric] = aggregated;
|
|
146
|
-
hasMetric = true;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return hasMetric ? summary : null;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async function collectCoverageSummary(patterns = []) {
|
|
153
|
-
if (!Array.isArray(patterns) || patterns.length === 0) {
|
|
154
|
-
return { sources: [], summary: null };
|
|
155
|
-
}
|
|
156
|
-
const files = expandCoveragePatterns(patterns);
|
|
157
|
-
if (files.length === 0) {
|
|
158
|
-
return { sources: [], summary: null };
|
|
159
|
-
}
|
|
160
|
-
const sources = [];
|
|
161
|
-
for (const filePath of files) {
|
|
162
|
-
const parsed = await parseCoverageFile(filePath);
|
|
163
|
-
if (parsed) {
|
|
164
|
-
sources.push(parsed);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
const summary = synthesiseSummary(sources);
|
|
168
|
-
return { sources, summary };
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
module.exports = {
|
|
172
|
-
collectCoverageSummary,
|
|
173
|
-
};
|
package/src/tests/index.js
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
const config = require("../config");
|
|
2
|
-
const logger = require("../logger");
|
|
3
|
-
const { workspaceRoot } = require("../workspace");
|
|
4
|
-
const { runProcess, MAX_TIMEOUT_MS } = require("../tools/process");
|
|
5
|
-
const { collectCoverageSummary } = require("./coverage");
|
|
6
|
-
const { createTestRun, listTestRuns, getTestSummary } = require("./store");
|
|
7
|
-
|
|
8
|
-
function resolveProfile(name) {
|
|
9
|
-
if (!name || typeof name !== "string") return null;
|
|
10
|
-
const profiles = config.tests?.profiles;
|
|
11
|
-
if (!Array.isArray(profiles) || profiles.length === 0) return null;
|
|
12
|
-
const normalized = name.trim().toLowerCase();
|
|
13
|
-
return (
|
|
14
|
-
profiles.find(
|
|
15
|
-
(profile) =>
|
|
16
|
-
typeof profile?.name === "string" && profile.name.trim().toLowerCase() === normalized,
|
|
17
|
-
) ?? null
|
|
18
|
-
);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function deriveCommand({ profile, args }) {
|
|
22
|
-
if (profile && typeof profile.command === "string" && profile.command.trim().length > 0) {
|
|
23
|
-
const command = profile.command.trim();
|
|
24
|
-
const commandArgs = Array.isArray(profile.args) ? profile.args.map(String) : [];
|
|
25
|
-
const cwd =
|
|
26
|
-
typeof profile.cwd === "string" && profile.cwd.trim().length > 0
|
|
27
|
-
? profile.cwd.trim()
|
|
28
|
-
: ".";
|
|
29
|
-
const sandbox = typeof profile.sandbox === "string" ? profile.sandbox.toLowerCase() : null;
|
|
30
|
-
return { command, args: commandArgs, cwd, sandbox };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const defaultCommand = config.tests?.defaultCommand;
|
|
34
|
-
if (typeof defaultCommand === "string" && defaultCommand.trim().length > 0) {
|
|
35
|
-
return {
|
|
36
|
-
command: defaultCommand.trim(),
|
|
37
|
-
args: Array.isArray(args) && args.length ? args.map(String) : config.tests?.defaultArgs ?? [],
|
|
38
|
-
cwd: ".",
|
|
39
|
-
sandbox: config.tests?.sandbox ?? "auto",
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
throw new Error(
|
|
44
|
-
"No default test command configured. Set WORKSPACE_TEST_COMMAND or define profiles.",
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async function executeTestCommand({
|
|
49
|
-
profile,
|
|
50
|
-
command,
|
|
51
|
-
args = [],
|
|
52
|
-
cwd = ".",
|
|
53
|
-
env = {},
|
|
54
|
-
timeoutMs,
|
|
55
|
-
sandbox,
|
|
56
|
-
sessionId,
|
|
57
|
-
}) {
|
|
58
|
-
const timeout =
|
|
59
|
-
Number.isFinite(timeoutMs) && timeoutMs > 0
|
|
60
|
-
? Math.min(timeoutMs, MAX_TIMEOUT_MS)
|
|
61
|
-
: config.tests?.timeoutMs ?? 600000;
|
|
62
|
-
|
|
63
|
-
const runEnv = {
|
|
64
|
-
...env,
|
|
65
|
-
WORKSPACE_TEST_PROFILE: profile ?? "",
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
return runProcess({
|
|
69
|
-
command,
|
|
70
|
-
args,
|
|
71
|
-
cwd,
|
|
72
|
-
env: runEnv,
|
|
73
|
-
timeoutMs: timeout,
|
|
74
|
-
sandbox: sandbox ?? config.tests?.sandbox ?? "auto",
|
|
75
|
-
sessionId,
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async function runWorkspaceTests({
|
|
80
|
-
profileName,
|
|
81
|
-
args,
|
|
82
|
-
cwd,
|
|
83
|
-
env,
|
|
84
|
-
timeoutMs,
|
|
85
|
-
sandbox,
|
|
86
|
-
sessionId,
|
|
87
|
-
collectCoverage = true,
|
|
88
|
-
}) {
|
|
89
|
-
const profile = resolveProfile(profileName);
|
|
90
|
-
const derived = deriveCommand({ profile, args });
|
|
91
|
-
|
|
92
|
-
const command = typeof derived.command === "string" ? derived.command : null;
|
|
93
|
-
const commandArgs = Array.isArray(args) && args.length ? args.map(String) : derived.args ?? [];
|
|
94
|
-
const effectiveCwd = typeof cwd === "string" ? cwd : derived.cwd ?? ".";
|
|
95
|
-
const effectiveSandbox = sandbox ?? derived.sandbox ?? config.tests?.sandbox ?? "auto";
|
|
96
|
-
|
|
97
|
-
const start = Date.now();
|
|
98
|
-
const result = await executeTestCommand({
|
|
99
|
-
profile: profile?.name ?? profileName ?? null,
|
|
100
|
-
command,
|
|
101
|
-
args: commandArgs,
|
|
102
|
-
cwd: effectiveCwd,
|
|
103
|
-
env,
|
|
104
|
-
timeoutMs,
|
|
105
|
-
sandbox: effectiveSandbox,
|
|
106
|
-
sessionId,
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
const durationMs = Date.now() - start;
|
|
110
|
-
const status =
|
|
111
|
-
result.exitCode === 0 && !result.timedOut
|
|
112
|
-
? "passed"
|
|
113
|
-
: result.timedOut
|
|
114
|
-
? "timed_out"
|
|
115
|
-
: "failed";
|
|
116
|
-
|
|
117
|
-
let coverage = null;
|
|
118
|
-
if (collectCoverage) {
|
|
119
|
-
try {
|
|
120
|
-
const patterns =
|
|
121
|
-
Array.isArray(config.tests?.coverage?.files) && config.tests.coverage.files.length > 0
|
|
122
|
-
? config.tests.coverage.files
|
|
123
|
-
: [];
|
|
124
|
-
if (patterns.length > 0) {
|
|
125
|
-
const summary = await collectCoverageSummary(patterns);
|
|
126
|
-
coverage = summary;
|
|
127
|
-
}
|
|
128
|
-
} catch (err) {
|
|
129
|
-
logger.warn({ err }, "Failed to collect coverage summary");
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const record = createTestRun({
|
|
134
|
-
profile: profile?.name ?? profileName ?? null,
|
|
135
|
-
status,
|
|
136
|
-
command,
|
|
137
|
-
args: commandArgs,
|
|
138
|
-
cwd: effectiveCwd === "." ? workspaceRoot : effectiveCwd,
|
|
139
|
-
exitCode: result.exitCode,
|
|
140
|
-
timedOut: result.timedOut,
|
|
141
|
-
durationMs,
|
|
142
|
-
sandbox: effectiveSandbox,
|
|
143
|
-
stdout: result.stdout,
|
|
144
|
-
stderr: result.stderr,
|
|
145
|
-
coverage,
|
|
146
|
-
createdAt: Date.now(),
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
return {
|
|
150
|
-
run: {
|
|
151
|
-
id: record.id,
|
|
152
|
-
status,
|
|
153
|
-
exitCode: result.exitCode,
|
|
154
|
-
timedOut: result.timedOut,
|
|
155
|
-
durationMs,
|
|
156
|
-
sandbox: effectiveSandbox,
|
|
157
|
-
profile: record.profile,
|
|
158
|
-
stdout: record.stdout,
|
|
159
|
-
stdoutTruncated: record.stdoutTruncated,
|
|
160
|
-
stderr: record.stderr,
|
|
161
|
-
stderrTruncated: record.stderrTruncated,
|
|
162
|
-
},
|
|
163
|
-
coverage,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
module.exports = {
|
|
168
|
-
runWorkspaceTests,
|
|
169
|
-
listTestRuns,
|
|
170
|
-
getTestSummary,
|
|
171
|
-
};
|