@yemi33/minions 0.1.2108 → 0.1.2110
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/dashboard/js/qa.js +64 -2
- package/dashboard/js/settings.js +2 -0
- package/dashboard.js +41 -0
- package/docs/deprecated.json +8 -0
- package/docs/design-state-storage.md +1 -1
- package/docs/onboarding.md +2 -2
- package/docs/qa-runbook-lifecycle.md +36 -0
- package/engine/db/migrations/009-qa.js +140 -0
- package/engine/qa-runs.js +118 -32
- package/engine/qa-sessions.js +30 -14
- package/engine/shared.js +79 -1
- package/engine/small-state-store.js +320 -0
- package/engine.js +52 -6
- package/package.json +1 -1
package/dashboard/js/qa.js
CHANGED
|
@@ -344,19 +344,81 @@ function _qaRenderRunRow(run) {
|
|
|
344
344
|
} else if (agentId) {
|
|
345
345
|
liveLink = '<a href="#" class="qa-run-link" onclick="event.preventDefault();qaOpenRunAgent(\'\',this.dataset.agent);" data-agent="' + escHtml(agentId) + '">' + escHtml(linkLabel) + '</a>';
|
|
346
346
|
}
|
|
347
|
+
// W-mpxp90xs000i5732 — per-row Delete button, terminal-only. The engine
|
|
348
|
+
// also enforces terminal-only with a 409 (defense in depth); rendering
|
|
349
|
+
// the button conditionally just keeps the UI honest about what's safe
|
|
350
|
+
// to click.
|
|
351
|
+
let deleteBtn = '';
|
|
352
|
+
if (_qaIsTerminalStatus(status)) {
|
|
353
|
+
deleteBtn = '<button class="qa-btn-ghost qa-run-delete-btn" ' +
|
|
354
|
+
'data-run-id="' + escHtml(id) + '" ' +
|
|
355
|
+
'onclick="qaDeleteRun(this.dataset.runId)">Delete</button>';
|
|
356
|
+
}
|
|
357
|
+
const actionsHtml = (liveLink || deleteBtn)
|
|
358
|
+
? '<span class="qa-run-actions">' + liveLink + deleteBtn + '</span>'
|
|
359
|
+
: '';
|
|
347
360
|
const artifactsHtml = _qaRenderArtifactPreviews(id, run.artifacts || []);
|
|
348
|
-
return '<div class="qa-run-row">' +
|
|
361
|
+
return '<div class="qa-run-row" data-run-id="' + escHtml(id) + '">' +
|
|
349
362
|
'<div class="qa-run-head">' +
|
|
350
363
|
'<span class="qa-run-status ' + escHtml(statusClass) + '">' + escHtml(status) + '</span>' +
|
|
351
364
|
'<span class="qa-run-name">' + escHtml(runbook) + '</span>' +
|
|
352
365
|
(target ? '<span class="qa-run-target">→ <code>' + escHtml(target) + '</code></span>' : '') +
|
|
353
366
|
(ts ? '<span class="qa-run-ts">' + escHtml(String(ts).slice(0, 19).replace('T', ' ')) + '</span>' : '') +
|
|
354
|
-
|
|
367
|
+
actionsHtml +
|
|
355
368
|
'</div>' +
|
|
356
369
|
(artifactsHtml ? '<div class="qa-run-artifacts">' + artifactsHtml + '</div>' : '') +
|
|
357
370
|
'</div>';
|
|
358
371
|
}
|
|
359
372
|
|
|
373
|
+
// W-mpxp90xs000i5732 — terminal status set kept in sync with
|
|
374
|
+
// engine/qa-runs.js#TERMINAL_STATUSES. The Delete button only renders for
|
|
375
|
+
// runs in one of these states; the engine's deleteQaRun returns 409 for
|
|
376
|
+
// anything else (defense in depth).
|
|
377
|
+
function _qaIsTerminalStatus(status) {
|
|
378
|
+
if (!status || typeof status !== 'string') return false;
|
|
379
|
+
const s = status.toLowerCase();
|
|
380
|
+
return s === 'passed' || s === 'failed' || s === 'errored';
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// W-mpxp90xs000i5732 — per-row Delete handler. Confirms with the user,
|
|
384
|
+
// fires DELETE /api/qa/runs/<id>, optimistic-removes the row on 200, and
|
|
385
|
+
// surfaces errors via showToast(). Does NOT use alert() post-API per the
|
|
386
|
+
// WI's UX contract.
|
|
387
|
+
async function qaDeleteRun(id) {
|
|
388
|
+
if (!id) return;
|
|
389
|
+
if (!confirm('Delete QA run ' + id + ' and its artifacts? This cannot be undone.')) return;
|
|
390
|
+
let res, json;
|
|
391
|
+
try {
|
|
392
|
+
res = await fetch('/api/qa/runs/' + encodeURIComponent(id), {
|
|
393
|
+
method: 'DELETE',
|
|
394
|
+
});
|
|
395
|
+
} catch (e) {
|
|
396
|
+
if (typeof showToast === 'function') {
|
|
397
|
+
showToast('cmd-toast', 'Delete QA run failed: ' + (e && e.message || String(e)), false);
|
|
398
|
+
}
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
try { json = await res.json(); } catch { json = {}; }
|
|
402
|
+
if (res.ok && json && json.ok) {
|
|
403
|
+
const sel = (window.CSS && CSS.escape ? CSS.escape(id) : id.replace(/"/g, '\\"'));
|
|
404
|
+
const row = document.querySelector('.qa-run-row[data-run-id="' + sel + '"]');
|
|
405
|
+
if (row && row.parentNode) row.parentNode.removeChild(row);
|
|
406
|
+
if (typeof loadQaRuns === 'function') loadQaRuns();
|
|
407
|
+
if (typeof showToast === 'function') {
|
|
408
|
+
const msg = json.artifactsRemoved
|
|
409
|
+
? 'QA run deleted (artifacts removed).'
|
|
410
|
+
: 'QA run deleted.';
|
|
411
|
+
showToast('cmd-toast', msg, true);
|
|
412
|
+
}
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
const errMsg = (json && (json.error + (json.currentStatus ? ' (status=' + json.currentStatus + ')' : '')))
|
|
416
|
+
|| ('HTTP ' + (res ? res.status : '?'));
|
|
417
|
+
if (typeof showToast === 'function') {
|
|
418
|
+
showToast('cmd-toast', 'Delete QA run failed: ' + errMsg, false);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
360
422
|
function _qaRenderArtifactPreviews(runId, artifacts) {
|
|
361
423
|
if (!runId || !Array.isArray(artifacts) || !artifacts.length) return '';
|
|
362
424
|
const parts = [];
|
package/dashboard/js/settings.js
CHANGED
|
@@ -404,6 +404,7 @@ async function openSettings() {
|
|
|
404
404
|
'<div class="settings-stack">' +
|
|
405
405
|
settingsToggle('Allow Temp Agents', 'set-allowTempAgents', !!e.allowTempAgents, 'Spawn ephemeral agents when all permanent agents are busy') +
|
|
406
406
|
settingsToggle('Disable model discovery', 'set-disableModelDiscovery', !!e.disableModelDiscovery, 'Skip /api/runtimes/<name>/models REST calls fleet-wide. Settings UI falls back to free-text.') +
|
|
407
|
+
settingsToggle('QA: dual-write JSON sidecars', 'set-qaDualWriteJson', e.qaDualWriteJson !== false, 'Keep mirroring qa-runs.json and qa-sessions.json on every mutation alongside the SQLite source of truth. Default ON. Disable only once external tooling has been migrated off the JSON files — disabling drops the rollback path back to the JSON store.') +
|
|
407
408
|
'</div>';
|
|
408
409
|
|
|
409
410
|
// Section registry — order is intentional (Runtime + Auto-fix surface first
|
|
@@ -880,6 +881,7 @@ async function saveSettings() {
|
|
|
880
881
|
copilotReasoningSummaries: !!document.getElementById('set-copilotReasoningSummaries')?.checked,
|
|
881
882
|
maxBudgetUsd: (document.getElementById('set-maxBudgetUsd')?.value ?? '').trim(),
|
|
882
883
|
disableModelDiscovery: !!document.getElementById('set-disableModelDiscovery')?.checked,
|
|
884
|
+
qaDualWriteJson: !!document.getElementById('set-qaDualWriteJson')?.checked,
|
|
883
885
|
// W-mpmwxkrw000872ec — global font-size scale. Allowlist validation
|
|
884
886
|
// (and clamp messaging) lives server-side in handleSettingsUpdate.
|
|
885
887
|
fontSize: document.getElementById('set-fontSize')?.value || 'small',
|
package/dashboard.js
CHANGED
|
@@ -10345,6 +10345,46 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
10345
10345
|
}
|
|
10346
10346
|
}
|
|
10347
10347
|
|
|
10348
|
+
// W-mpxp90xs000i5732 — DELETE /api/qa/runs/<id>. Hard-delete: removes the
|
|
10349
|
+
// run from engine/qa-runs.json AND best-effort wipes the on-disk artifact
|
|
10350
|
+
// directory under engine/qa-artifacts/<runId>/. Terminal-only — refuses
|
|
10351
|
+
// pending/running runs with 409. Caller must cancel/complete the run via
|
|
10352
|
+
// the lifecycle helpers before deletion. CSRF/Origin gate is applied by
|
|
10353
|
+
// the server dispatcher for all mutating methods (see MUTATING_METHODS).
|
|
10354
|
+
function handleQaRunsDelete(req, res, match) {
|
|
10355
|
+
try {
|
|
10356
|
+
const qa = require('./engine/qa-runs');
|
|
10357
|
+
const id = decodeURIComponent(match[1] || '');
|
|
10358
|
+
if (!_qaIsSafeSegment(id)) {
|
|
10359
|
+
return jsonReply(res, 400, { error: 'invalid run id' }, req);
|
|
10360
|
+
}
|
|
10361
|
+
const result = qa.deleteQaRun(id);
|
|
10362
|
+
if (result && result.ok) {
|
|
10363
|
+
return jsonReply(res, 200, {
|
|
10364
|
+
ok: true,
|
|
10365
|
+
id,
|
|
10366
|
+
artifactsRemoved: !!result.artifactsRemoved,
|
|
10367
|
+
}, req);
|
|
10368
|
+
}
|
|
10369
|
+
const err = result && result.error;
|
|
10370
|
+
if (err === 'not_found') {
|
|
10371
|
+
return jsonReply(res, 404, { error: 'not_found' }, req);
|
|
10372
|
+
}
|
|
10373
|
+
if (err === 'not_terminal') {
|
|
10374
|
+
return jsonReply(res, 409, {
|
|
10375
|
+
error: 'not_terminal',
|
|
10376
|
+
currentStatus: result.currentStatus,
|
|
10377
|
+
}, req);
|
|
10378
|
+
}
|
|
10379
|
+
if (err === 'invalid_id') {
|
|
10380
|
+
return jsonReply(res, 400, { error: 'invalid run id' }, req);
|
|
10381
|
+
}
|
|
10382
|
+
return jsonReply(res, 500, { error: 'qa-runs delete returned unknown result' }, req);
|
|
10383
|
+
} catch (e) {
|
|
10384
|
+
return jsonReply(res, 500, { error: e.message }, req);
|
|
10385
|
+
}
|
|
10386
|
+
}
|
|
10387
|
+
|
|
10348
10388
|
function handleQaArtifact(req, res, match) {
|
|
10349
10389
|
try {
|
|
10350
10390
|
const qa = require('./engine/qa-runs');
|
|
@@ -11052,6 +11092,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
11052
11092
|
// traversal attempts return 403, missing files return 404.
|
|
11053
11093
|
{ method: 'GET', path: '/api/qa/runs', desc: 'List QA validation runs (newest first). Optional ?limit=N and ?status=pending|running|passed|failed|errored filters.', handler: handleQaRunsList },
|
|
11054
11094
|
{ method: 'GET', path: /^\/api\/qa\/runs\/([^/?]+)$/, template: '/api/qa/runs/<id>', desc: 'Fetch a single QA run record by id.', handler: handleQaRunsById },
|
|
11095
|
+
{ method: 'DELETE', path: /^\/api\/qa\/runs\/([^/?]+)$/, template: '/api/qa/runs/<id>', desc: 'Delete a QA run by id (terminal runs only). Best-effort removes artifacts under engine/qa-artifacts/<runId>/.', destructive: true, handler: handleQaRunsDelete },
|
|
11055
11096
|
{ method: 'GET', path: /^\/api\/qa\/artifacts\/([^/?]+)\/([^?]+)$/, template: '/api/qa/artifacts/<runId>/<file>', desc: 'Serve a QA artifact file (image/video/log). Sandboxed to engine/qa-artifacts/; rejects path traversal with 403.', handler: handleQaArtifact },
|
|
11056
11097
|
// QA Runbooks (W-mpeiwz6k0005bf34-a) — per-project test plans stored at
|
|
11057
11098
|
// <MINIONS_DIR>/projects/<name>/runbooks/<id>.json. Pure persistence —
|
package/docs/deprecated.json
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"id": "qa-json-sidecars",
|
|
4
|
+
"location": "engine/small-state-store.js _mirrorQaRunsJson + _mirrorQaSessionsJson; engine/shared.js _qaMutator (mirror call); dashboard/js/settings.js set-qaDualWriteJson toggle",
|
|
5
|
+
"reason": "Phase 8 migration (migrate-qa-state-to-sqlite, shipped 2026-06) moved qa_runs + qa_sessions into SQLite (engine/state.db, tables qa_runs + qa_sessions) as the source of truth. The legacy JSON sidecars engine/qa-runs.json and engine/qa-sessions.json are dual-written on every mutation while operators migrate external tooling and dashboards off the JSON files. Gated by engine.qaDualWriteJson (default true).",
|
|
6
|
+
"deprecated": "2026-06-03",
|
|
7
|
+
"targetRemovalDate": "2026-09-03",
|
|
8
|
+
"notes": "Safe to remove on or after 2026-09-03 (90 days post-ship, ~3 release windows) once: (1) operator telemetry / dashboard Settings shows engine.qaDualWriteJson flipped false on the main fleet; (2) no external readers (CI scripts, dashboards, ops runbooks) reference the JSON sidecars directly. Removal scope: delete _mirrorQaRunsJson + _mirrorQaSessionsJson + their exports in engine/small-state-store.js; drop the _qaDualWriteEnabled() + mirror call in engine/shared.js#_qaMutator; remove qaDualWriteJson from ENGINE_DEFAULTS and from the Settings toggle in dashboard/js/settings.js; remove the divergence-rehydrate branch in _resyncQaRunsIfDiverged / _resyncQaSessionsIfDiverged (the JSON files no longer exist to diverge against)."
|
|
9
|
+
},
|
|
2
10
|
{
|
|
3
11
|
"id": "config-poll-key-migration",
|
|
4
12
|
"location": "engine/queries.js:126-163",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> Author: Rebecca (Architect) | Date: 2026-04-07 | Status: **Accepted — implementation in progress**
|
|
4
4
|
|
|
5
|
-
> **Implementation status (as of 2026-
|
|
5
|
+
> **Implementation status (as of 2026-06):** The `node:sqlite` recommendation in §3 has been adopted ahead of schedule. Phases 0–8 have shipped (events, dispatches, work_items, pull_requests, logs, metrics, watches, schedule_runs + pipeline_runs + managed_processes + worktree_pool, and qa_runs + qa_sessions — see `CHANGELOG.md`). The SQLite schema lives under `engine/db/migrations/` and the singleton opens `engine/state.db` in WAL mode. Phase 8 added the first opt-out toggle for the JSON sidecars (`engine.qaDualWriteJson`, default true) — when ops trust SQL as the source of truth for QA state, flip it false to halve write I/O on hot loops. The "Phase 2: estimated Node 26 LTS" timeline in §3 is now historical context; treat sections 1–3 as design rationale rather than a forward plan.
|
|
6
6
|
|
|
7
7
|
## Executive Summary
|
|
8
8
|
|
package/docs/onboarding.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A fast, hands-on walkthrough for new contributors. Follow the eight steps below in order. Each step is independent enough that you can stop, take a break, and resume without losing state.
|
|
4
4
|
|
|
5
|
-
> **Prerequisites:** Node.js
|
|
5
|
+
> **Prerequisites:** Node.js 22.5+ (required by `package.json#engines` for the `node:sqlite` runtime backing the state DB), Git, and a working Claude Code CLI (`npm install -g @anthropic-ai/claude-code`) authenticated against your Anthropic API key or Claude Max subscription. See the top-level [README.md](../README.md#prerequisites) for the full list.
|
|
6
6
|
|
|
7
7
|
> **Screenshots:** This walkthrough is intentionally text-only on the first pass. If you want annotated dashboard screenshots, run the [`capture-demos`](../README.md#dashboard) skill after Step 5; it drives Playwright over the running dashboard and writes images you can drop alongside each section.
|
|
8
8
|
|
|
@@ -15,7 +15,7 @@ You only need to clone if you intend to modify Minions itself. End users normall
|
|
|
15
15
|
```bash
|
|
16
16
|
git clone https://github.com/yemi33/minions.git ~/minions-dev
|
|
17
17
|
cd ~/minions-dev
|
|
18
|
-
npm install # installs dev tooling (Playwright); engine
|
|
18
|
+
npm install # installs dev tooling (Playwright) + the lone runtime dep (@azure-devops/mcp); engine is otherwise built on Node built-ins
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
You should now have:
|
|
@@ -27,6 +27,41 @@ and clears the interval on page navigation via the `switchPage` wrapper in
|
|
|
27
27
|
`dashboard/js/qa.js` (matches `_stopPlanPoll`/`_stopMeetingPoll` pattern in
|
|
28
28
|
`dashboard/js/state.js`).
|
|
29
29
|
|
|
30
|
+
### Persistence (Phase 8: SQLite source of truth)
|
|
31
|
+
|
|
32
|
+
Since the Phase 8 migration (`migrate-qa-state-to-sqlite`), both
|
|
33
|
+
`qa-runs.json` and `qa-sessions.json` are dual-written sidecars and SQLite
|
|
34
|
+
(`engine/state.db`, tables `qa_runs` + `qa_sessions`) is the source of
|
|
35
|
+
truth. Every mutation goes through `shared.mutateQaRuns` /
|
|
36
|
+
`shared.mutateQaSessions`, which apply inside a SQLite transaction
|
|
37
|
+
(`BEGIN IMMEDIATE`) then best-effort mirror to JSON under a `withFileLock`
|
|
38
|
+
on the sidecar path so cross-process writers serialize. Reads go through
|
|
39
|
+
`small-state-store.readQaRuns` / `readQaSessions`, which compare the SQL
|
|
40
|
+
content-hash to the JSON sidecar and rehydrate SQL from JSON when an
|
|
41
|
+
external editor (operator hand-edit, manual rollback) has diverged.
|
|
42
|
+
|
|
43
|
+
The JSON mirror is gated by `engine.qaDualWriteJson` (default true). Once
|
|
44
|
+
operators trust SQL as the source of truth and have migrated external
|
|
45
|
+
tooling off the JSON files, flip it false via Dashboard → Settings →
|
|
46
|
+
Advanced → "QA: dual-write JSON sidecars" to halve write I/O. With the
|
|
47
|
+
toggle off, `qa-runs.json` / `qa-sessions.json` stop receiving updates
|
|
48
|
+
(stale snapshot at the moment of the flip), so don't disable until any
|
|
49
|
+
external readers have moved to the SQL tables or the dashboard `/api/qa/*`
|
|
50
|
+
endpoints. The qa-runs cap (`QA_RUNS_MAX_RECORDS=2000`) and qa-sessions
|
|
51
|
+
cap (`QA_SESSIONS_MAX_RECORDS=500`) still apply, enforced in-memory at
|
|
52
|
+
createRun/createSession time.
|
|
53
|
+
|
|
54
|
+
### Deleting runs
|
|
55
|
+
|
|
56
|
+
`DELETE /api/qa/runs/<id>` hard-deletes a single terminal-status run
|
|
57
|
+
(`passed`/`failed`/`errored`) and best-effort removes its artifact
|
|
58
|
+
directory under `engine/qa-artifacts/<runId>/`. Non-terminal runs return
|
|
59
|
+
`409 not_terminal` — operators must cancel/complete the run via the
|
|
60
|
+
lifecycle helpers first. Invalid ids return `400 invalid_id`. The /qa
|
|
61
|
+
dashboard page renders a per-row Delete button conditionally (terminal
|
|
62
|
+
only), confirms with the user, and surfaces success/error via
|
|
63
|
+
`showToast()` rather than `alert()`.
|
|
64
|
+
|
|
30
65
|
## Artifact contract
|
|
31
66
|
|
|
32
67
|
`engine/qa-artifacts/<runId>/<file>`, served via
|
|
@@ -198,6 +233,7 @@ Documented in `dashboard.js`; routes are visible at `GET /api/routes`.
|
|
|
198
233
|
| POST | `/api/qa/sessions/<id>/dismiss` | Non-terminal → `done`. Accept the draft as final; leaves spawn alive. Optional `{ summary }`. |
|
|
199
234
|
| GET | `/api/qa/runners` | List registered runner adapters (built-ins + `qa-runners.d/` plugins). Metadata only — hooks (functions) are stripped. |
|
|
200
235
|
| POST | `/api/qa/runners/reload` | Clear in-process registry, re-register built-ins, re-scan `qa-runners.d/` for plugin edits. Returns the fresh runner list. |
|
|
236
|
+
| DELETE | `/api/qa/runs/<id>` | Hard-delete a single QA run record (terminal-status runs only) and best-effort wipe its artifact directory under `engine/qa-artifacts/<runId>/`. Returns `{ ok: true, id, artifactsRemoved }` on success, `409 not_terminal` for in-flight runs, `404 not_found` for unknown ids. |
|
|
201
237
|
|
|
202
238
|
The single-session POSTs share `_qaSessionAction` in `dashboard.js`; module
|
|
203
239
|
errors are mapped to HTTP via `_qaSessionsErrorToStatus`:
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// engine/db/migrations/009-qa.js
|
|
2
|
+
//
|
|
3
|
+
// Phase 8: move the two QA state files into SQL.
|
|
4
|
+
//
|
|
5
|
+
// engine/qa-runs.json -> qa_runs (top-level array → row per id)
|
|
6
|
+
// engine/qa-sessions.json -> qa_sessions (top-level array → row per id)
|
|
7
|
+
//
|
|
8
|
+
// Mirrors the dual-write / content-hash divergence pattern established by
|
|
9
|
+
// the watches and Phase 7 small-state stores. JSON sidecars stay in place as
|
|
10
|
+
// dual-write mirrors for one release cycle (gated by `engine.qaDualWriteJson`).
|
|
11
|
+
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
|
|
15
|
+
function _resolveMinionsDir() {
|
|
16
|
+
const envHome = process.env.MINIONS_HOME || process.env.MINIONS_TEST_DIR;
|
|
17
|
+
if (envHome) return envHome;
|
|
18
|
+
try { return require('../../shared').MINIONS_DIR; } catch { return null; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function _toMs(v) {
|
|
22
|
+
if (v == null) return null;
|
|
23
|
+
if (typeof v === 'number') return Number.isFinite(v) ? v : null;
|
|
24
|
+
const parsed = Date.parse(v);
|
|
25
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function _readJsonOr(filePath, fallback) {
|
|
29
|
+
try {
|
|
30
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
31
|
+
return JSON.parse(raw);
|
|
32
|
+
} catch { return fallback; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = {
|
|
36
|
+
version: 9,
|
|
37
|
+
description: 'qa_runs + qa_sessions',
|
|
38
|
+
up(db) {
|
|
39
|
+
db.exec(`
|
|
40
|
+
CREATE TABLE qa_runs (
|
|
41
|
+
id TEXT PRIMARY KEY,
|
|
42
|
+
runbook_id TEXT NOT NULL,
|
|
43
|
+
target_name TEXT NOT NULL,
|
|
44
|
+
project TEXT,
|
|
45
|
+
work_item_id TEXT,
|
|
46
|
+
status TEXT NOT NULL,
|
|
47
|
+
started_at INTEGER,
|
|
48
|
+
completed_at INTEGER,
|
|
49
|
+
created_at INTEGER NOT NULL,
|
|
50
|
+
data TEXT NOT NULL
|
|
51
|
+
);
|
|
52
|
+
CREATE INDEX idx_qa_runs_status ON qa_runs(status);
|
|
53
|
+
CREATE INDEX idx_qa_runs_created_at ON qa_runs(created_at DESC);
|
|
54
|
+
CREATE INDEX idx_qa_runs_runbook ON qa_runs(runbook_id);
|
|
55
|
+
CREATE INDEX idx_qa_runs_target ON qa_runs(target_name);
|
|
56
|
+
CREATE INDEX idx_qa_runs_work_item ON qa_runs(work_item_id) WHERE work_item_id IS NOT NULL;
|
|
57
|
+
|
|
58
|
+
CREATE TABLE qa_sessions (
|
|
59
|
+
id TEXT PRIMARY KEY,
|
|
60
|
+
state TEXT NOT NULL,
|
|
61
|
+
primary_project TEXT,
|
|
62
|
+
qa_run_id TEXT,
|
|
63
|
+
created_at INTEGER NOT NULL,
|
|
64
|
+
updated_at INTEGER NOT NULL,
|
|
65
|
+
completed_at INTEGER,
|
|
66
|
+
data TEXT NOT NULL
|
|
67
|
+
);
|
|
68
|
+
CREATE INDEX idx_qa_sessions_state ON qa_sessions(state);
|
|
69
|
+
CREATE INDEX idx_qa_sessions_created_at ON qa_sessions(created_at DESC);
|
|
70
|
+
CREATE INDEX idx_qa_sessions_project ON qa_sessions(primary_project);
|
|
71
|
+
CREATE INDEX idx_qa_sessions_qa_run ON qa_sessions(qa_run_id) WHERE qa_run_id IS NOT NULL;
|
|
72
|
+
`);
|
|
73
|
+
|
|
74
|
+
const minionsDir = _resolveMinionsDir();
|
|
75
|
+
if (!minionsDir) return;
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
let inserted = 0;
|
|
78
|
+
|
|
79
|
+
// ── qa_runs ────────────────────────────────────────────────────────────
|
|
80
|
+
{
|
|
81
|
+
const raw = _readJsonOr(path.join(minionsDir, 'engine', 'qa-runs.json'), null);
|
|
82
|
+
if (Array.isArray(raw)) {
|
|
83
|
+
const ins = db.prepare(`
|
|
84
|
+
INSERT INTO qa_runs (id, runbook_id, target_name, project, work_item_id, status, started_at, completed_at, created_at, data)
|
|
85
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
86
|
+
ON CONFLICT(id) DO NOTHING
|
|
87
|
+
`);
|
|
88
|
+
for (const run of raw) {
|
|
89
|
+
if (!run || !run.id) continue;
|
|
90
|
+
try {
|
|
91
|
+
ins.run(
|
|
92
|
+
String(run.id),
|
|
93
|
+
String(run.runbookId || ''),
|
|
94
|
+
String(run.targetName || ''),
|
|
95
|
+
run.project || null,
|
|
96
|
+
run.workItemId || null,
|
|
97
|
+
String(run.status || 'pending'),
|
|
98
|
+
_toMs(run.startedAt),
|
|
99
|
+
_toMs(run.completedAt),
|
|
100
|
+
_toMs(run.createdAt) || now,
|
|
101
|
+
JSON.stringify(run),
|
|
102
|
+
);
|
|
103
|
+
inserted += 1;
|
|
104
|
+
} catch { /* duplicate / corrupt — skip */ }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── qa_sessions ────────────────────────────────────────────────────────
|
|
110
|
+
{
|
|
111
|
+
const raw = _readJsonOr(path.join(minionsDir, 'engine', 'qa-sessions.json'), null);
|
|
112
|
+
if (Array.isArray(raw)) {
|
|
113
|
+
const ins = db.prepare(`
|
|
114
|
+
INSERT INTO qa_sessions (id, state, primary_project, qa_run_id, created_at, updated_at, completed_at, data)
|
|
115
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
116
|
+
ON CONFLICT(id) DO NOTHING
|
|
117
|
+
`);
|
|
118
|
+
for (const session of raw) {
|
|
119
|
+
if (!session || !session.id) continue;
|
|
120
|
+
try {
|
|
121
|
+
ins.run(
|
|
122
|
+
String(session.id),
|
|
123
|
+
String(session.state || 'pending'),
|
|
124
|
+
session.primaryProject || (session.spec && session.spec.project) || null,
|
|
125
|
+
session.qaRunId || null,
|
|
126
|
+
_toMs(session.createdAt) || now,
|
|
127
|
+
_toMs(session.updatedAt) || _toMs(session.createdAt) || now,
|
|
128
|
+
_toMs(session.completedAt),
|
|
129
|
+
JSON.stringify(session),
|
|
130
|
+
);
|
|
131
|
+
inserted += 1;
|
|
132
|
+
} catch { /* duplicate / corrupt — skip */ }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// eslint-disable-next-line no-console
|
|
138
|
+
console.log(`[db-migrate] v9: backfilled ${inserted} QA rows; JSON files kept as dual-write mirrors`);
|
|
139
|
+
},
|
|
140
|
+
};
|