@yemi33/minions 0.1.2071 → 0.1.2072
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 +358 -0
- package/dashboard/js/state.js +2 -1
- package/dashboard/pages/qa.html +72 -0
- package/dashboard/styles.css +102 -0
- package/dashboard.js +410 -6
- package/docs/qa-runbook-lifecycle.md +232 -0
- package/engine/cleanup.js +4 -1
- package/engine/comment-classifier.js +8 -1
- package/engine/cooldown.js +6 -2
- package/engine/gh-comment.js +74 -3
- package/engine/lifecycle.js +100 -0
- package/engine/pipeline.js +9 -1
- package/engine/playbook.js +39 -0
- package/engine/qa-runners/maestro.js +152 -0
- package/engine/qa-runners/playwright.js +149 -0
- package/engine/qa-runners.js +323 -0
- package/engine/qa-sessions.js +1008 -0
- package/engine/shared.js +71 -12
- package/engine.js +140 -0
- package/package.json +1 -1
- package/playbooks/qa-session-draft.md +158 -0
- package/playbooks/qa-session-execute.md +165 -0
- package/playbooks/qa-session-setup.md +154 -0
- package/prompts/cc-system.md +43 -0
- package/routing.md +3 -0
package/dashboard/js/qa.js
CHANGED
|
@@ -503,7 +503,365 @@ function qaOpenRunAgent(workItemId, agentId) {
|
|
|
503
503
|
if (typeof currentPage !== 'undefined' && currentPage === 'qa') {
|
|
504
504
|
loadQaTargets();
|
|
505
505
|
loadQaRunbooks();
|
|
506
|
+
loadQaRunners();
|
|
507
|
+
loadQaSessions();
|
|
508
|
+
_startQaSessionsPoll();
|
|
506
509
|
_startQaRunsPoll();
|
|
507
510
|
}
|
|
508
511
|
} catch {}
|
|
509
512
|
})();
|
|
513
|
+
|
|
514
|
+
// ── P-h7e4f9b2: QA Sessions section ───────────────────────────────────────
|
|
515
|
+
//
|
|
516
|
+
// Three sub-UIs that mirror the Targets / Runbooks / Runs trio above:
|
|
517
|
+
// 1. "Start QA Session" form posts to POST /api/qa/session.
|
|
518
|
+
// 2. Sessions list (composite cards with phase chips) polled every 3s
|
|
519
|
+
// while any session is non-terminal; clearInterval once all sessions
|
|
520
|
+
// are terminal (mirrors _stopPlanPoll's "auto-stop on terminal" rule).
|
|
521
|
+
// 3. Runner dropdown auto-loaded from GET /api/qa/runners.
|
|
522
|
+
//
|
|
523
|
+
// All renderer HTML escapes user-supplied values with escHtml. The five
|
|
524
|
+
// session actions (approve / edit / cancel / dismiss / kill) call their
|
|
525
|
+
// matching /api/qa/sessions/<id>/<action> endpoint and refresh the card in
|
|
526
|
+
// place via a follow-up loadQaSessions().
|
|
527
|
+
|
|
528
|
+
const QA_SESSION_TERMINAL_STATES = new Set(['done', 'failed', 'killed']);
|
|
529
|
+
|
|
530
|
+
let _qaSessionsPollInterval = null;
|
|
531
|
+
let _qaSessionsCache = [];
|
|
532
|
+
let _qaRunnersCache = [];
|
|
533
|
+
|
|
534
|
+
function _stopQaSessionsPoll() {
|
|
535
|
+
if (_qaSessionsPollInterval) {
|
|
536
|
+
clearInterval(_qaSessionsPollInterval);
|
|
537
|
+
_qaSessionsPollInterval = null;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function _startQaSessionsPoll() {
|
|
542
|
+
_stopQaSessionsPoll();
|
|
543
|
+
// Initial fetch + 3s poll per acceptance criteria. The poll is auto-
|
|
544
|
+
// stopped once every session in _qaSessionsCache is terminal (see
|
|
545
|
+
// _qaAfterSessionsRender). The PAGE_LEAVE_HOOKS still call
|
|
546
|
+
// _stopQaSessionsPoll on every switchPage, so leaving the page is a
|
|
547
|
+
// separate stop trigger.
|
|
548
|
+
loadQaSessions();
|
|
549
|
+
_qaSessionsPollInterval = setInterval(loadQaSessions, 3000);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Show / hide the per-target-kind sub-input. Called from the kind dropdown
|
|
553
|
+
// onchange so users only see the field that maps to their selected kind.
|
|
554
|
+
function qaSessionTargetKindChanged() {
|
|
555
|
+
const kind = document.getElementById('qa-session-target-kind');
|
|
556
|
+
if (!kind) return;
|
|
557
|
+
const wraps = {
|
|
558
|
+
pr: 'qa-session-target-pr-wrap',
|
|
559
|
+
branch: 'qa-session-target-branch-wrap',
|
|
560
|
+
commit: 'qa-session-target-sha-wrap',
|
|
561
|
+
current: 'qa-session-target-worktree-wrap',
|
|
562
|
+
};
|
|
563
|
+
for (const id of Object.values(wraps)) {
|
|
564
|
+
const el = document.getElementById(id);
|
|
565
|
+
if (el) el.style.display = 'none';
|
|
566
|
+
}
|
|
567
|
+
const showId = wraps[kind.value];
|
|
568
|
+
if (showId) {
|
|
569
|
+
const el = document.getElementById(showId);
|
|
570
|
+
if (el) el.style.display = '';
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
async function loadQaRunners() {
|
|
575
|
+
const sel = document.getElementById('qa-session-runner');
|
|
576
|
+
if (!sel) return;
|
|
577
|
+
try {
|
|
578
|
+
const res = await fetch('/api/qa/runners');
|
|
579
|
+
const json = res.ok ? await res.json() : { runners: [] };
|
|
580
|
+
_qaRunnersCache = Array.isArray(json.runners) ? json.runners : [];
|
|
581
|
+
} catch { _qaRunnersCache = []; }
|
|
582
|
+
// Rebuild via DOM API so we don't touch innerHTML at all.
|
|
583
|
+
while (sel.firstChild) sel.removeChild(sel.firstChild);
|
|
584
|
+
const auto = document.createElement('option');
|
|
585
|
+
auto.value = '';
|
|
586
|
+
auto.textContent = 'Auto-detect';
|
|
587
|
+
sel.appendChild(auto);
|
|
588
|
+
for (const r of _qaRunnersCache) {
|
|
589
|
+
if (!r || !r.name) continue;
|
|
590
|
+
const opt = document.createElement('option');
|
|
591
|
+
opt.value = r.name;
|
|
592
|
+
opt.textContent = (r.label || r.name) + ' (priority ' + (r.priority != null ? r.priority : '?') + ')';
|
|
593
|
+
sel.appendChild(opt);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async function loadQaSessions() {
|
|
598
|
+
const root = document.getElementById('qa-sessions-content');
|
|
599
|
+
if (!root) return;
|
|
600
|
+
let sessions = [];
|
|
601
|
+
let err = null;
|
|
602
|
+
try {
|
|
603
|
+
const res = await fetch('/api/qa/sessions?limit=50');
|
|
604
|
+
const json = res.ok ? await res.json() : { sessions: [] };
|
|
605
|
+
sessions = Array.isArray(json.sessions) ? json.sessions : [];
|
|
606
|
+
} catch (e) { err = e; }
|
|
607
|
+
|
|
608
|
+
if (err) {
|
|
609
|
+
// eslint-disable-next-line no-unsanitized/method -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: err.message)
|
|
610
|
+
const frag = document.createRange().createContextualFragment(
|
|
611
|
+
'<p class="empty" style="color:var(--red)">Failed to load sessions: ' + escHtml(err.message || String(err)) + '</p>'
|
|
612
|
+
);
|
|
613
|
+
root.replaceChildren(frag);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
_qaSessionsCache = sessions;
|
|
618
|
+
if (!sessions.length) {
|
|
619
|
+
const frag = document.createRange().createContextualFragment(
|
|
620
|
+
'<p class="empty">No QA sessions yet. Use the form above to start one.</p>'
|
|
621
|
+
);
|
|
622
|
+
root.replaceChildren(frag);
|
|
623
|
+
_qaAfterSessionsRender();
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
let html = '';
|
|
628
|
+
for (const s of sessions) {
|
|
629
|
+
html += _qaRenderSessionCard(s);
|
|
630
|
+
}
|
|
631
|
+
// eslint-disable-next-line no-unsanitized/method -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: session id, state, target sub-fields, mode, runner, error/summary/failureClass)
|
|
632
|
+
const frag = document.createRange().createContextualFragment(html);
|
|
633
|
+
root.replaceChildren(frag);
|
|
634
|
+
_qaAfterSessionsRender();
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Auto-stop polling once every visible session has reached a terminal
|
|
638
|
+
// state. The form-submit + action handlers each call _startQaSessionsPoll
|
|
639
|
+
// to restart polling when the next non-terminal session appears.
|
|
640
|
+
function _qaAfterSessionsRender() {
|
|
641
|
+
const allTerminal = _qaSessionsCache.length > 0
|
|
642
|
+
&& _qaSessionsCache.every(s => s && QA_SESSION_TERMINAL_STATES.has(s.state));
|
|
643
|
+
if (allTerminal) _stopQaSessionsPoll();
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function _qaRenderSessionCard(s) {
|
|
647
|
+
const id = s && s.id || '';
|
|
648
|
+
const state = (s && s.state) || 'unknown';
|
|
649
|
+
const spec = (s && s.spec) || {};
|
|
650
|
+
const target = spec.target || {};
|
|
651
|
+
const targetSummary = _qaSummarizeTarget(target);
|
|
652
|
+
const flowsRaw = spec.flowsRaw || '';
|
|
653
|
+
const flowsShort = flowsRaw.length > 200 ? flowsRaw.slice(0, 200) + '…' : flowsRaw;
|
|
654
|
+
const runner = spec.runner || '(auto-detect)';
|
|
655
|
+
const mode = spec.mode || 'confirm';
|
|
656
|
+
const failureClass = s && s.failureClass || '';
|
|
657
|
+
const error = s && s.error || '';
|
|
658
|
+
const summary = s && s.summary || '';
|
|
659
|
+
const createdAt = s && s.createdAt ? String(s.createdAt).slice(0, 19).replace('T', ' ') : '';
|
|
660
|
+
const isTerminal = QA_SESSION_TERMINAL_STATES.has(state);
|
|
661
|
+
|
|
662
|
+
const phases = _qaRenderPhaseChips(state);
|
|
663
|
+
|
|
664
|
+
// Action buttons depend on session state:
|
|
665
|
+
// - awaiting-approval → [APPROVE & RUN] [EDIT] [CANCEL]
|
|
666
|
+
// - non-terminal → footer always shows [DISMISS] [KILL SPAWN]
|
|
667
|
+
// - terminal → no actions
|
|
668
|
+
let actions = '';
|
|
669
|
+
if (state === 'awaiting-approval') {
|
|
670
|
+
actions += '<button class="qa-btn-primary" onclick="qaSessionApprove(\'' + escHtml(id) + '\')">Approve & run</button>';
|
|
671
|
+
actions += '<button class="qa-btn-ghost" onclick="qaSessionEditPrompt(\'' + escHtml(id) + '\')">Edit</button>';
|
|
672
|
+
actions += '<button class="qa-btn-ghost" onclick="qaSessionCancel(\'' + escHtml(id) + '\')">Cancel</button>';
|
|
673
|
+
}
|
|
674
|
+
if (!isTerminal) {
|
|
675
|
+
actions += '<button class="qa-btn-ghost" onclick="qaSessionDismiss(\'' + escHtml(id) + '\')">Dismiss</button>';
|
|
676
|
+
actions += '<button class="qa-btn-ghost qa-btn-danger" onclick="qaSessionKill(\'' + escHtml(id) + '\')">Kill spawn</button>';
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
let meta = '';
|
|
680
|
+
meta += '<span class="qa-session-meta-item">target: <code>' + escHtml(targetSummary) + '</code></span>';
|
|
681
|
+
meta += '<span class="qa-session-meta-item">mode: <code>' + escHtml(mode) + '</code></span>';
|
|
682
|
+
meta += '<span class="qa-session-meta-item">runner: <code>' + escHtml(runner) + '</code></span>';
|
|
683
|
+
if (createdAt) meta += '<span class="qa-session-meta-item qa-session-meta-ts">' + escHtml(createdAt) + '</span>';
|
|
684
|
+
|
|
685
|
+
let body = '';
|
|
686
|
+
if (flowsShort) {
|
|
687
|
+
body += '<div class="qa-session-flows"><strong>Flows:</strong> ' + escHtml(flowsShort) + '</div>';
|
|
688
|
+
}
|
|
689
|
+
if (state === 'failed' && (failureClass || error)) {
|
|
690
|
+
body += '<div class="qa-session-error"><strong>' + escHtml(failureClass || 'failed') + ':</strong> ' + escHtml(error || summary || 'no details') + '</div>';
|
|
691
|
+
} else if (summary) {
|
|
692
|
+
body += '<div class="qa-session-summary">' + escHtml(summary) + '</div>';
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const stateClass = 'qa-session-state-' + escHtml(state);
|
|
696
|
+
return (
|
|
697
|
+
'<div class="qa-session-card ' + stateClass + '" data-session-id="' + escHtml(id) + '">' +
|
|
698
|
+
'<div class="qa-session-head">' +
|
|
699
|
+
'<span class="qa-session-id"><code>' + escHtml(id) + '</code></span>' +
|
|
700
|
+
'<span class="qa-session-state">' + escHtml(state) + '</span>' +
|
|
701
|
+
'</div>' +
|
|
702
|
+
'<div class="qa-session-phases">' + phases + '</div>' +
|
|
703
|
+
'<div class="qa-session-meta">' + meta + '</div>' +
|
|
704
|
+
body +
|
|
705
|
+
(actions ? '<div class="qa-session-actions">' + actions + '</div>' : '') +
|
|
706
|
+
'</div>'
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Map session.state → the visual phase the user is currently in (1..4).
|
|
711
|
+
// Phases earlier than `current` render as --done; later phases render as
|
|
712
|
+
// muted; the current phase is --active. Terminal states render the final
|
|
713
|
+
// phase as --done with a status badge on the state row.
|
|
714
|
+
function _qaRenderPhaseChips(state) {
|
|
715
|
+
const PHASES = [
|
|
716
|
+
{ key: 'setup', icon: '🔧', label: 'setup' },
|
|
717
|
+
{ key: 'draft', icon: '📝', label: 'draft' },
|
|
718
|
+
{ key: 'execute', icon: '▶', label: 'execute' },
|
|
719
|
+
{ key: 'done', icon: '✅', label: 'done' },
|
|
720
|
+
];
|
|
721
|
+
let activeIdx;
|
|
722
|
+
switch (state) {
|
|
723
|
+
case 'pending':
|
|
724
|
+
case 'spawning': activeIdx = 0; break;
|
|
725
|
+
case 'drafting':
|
|
726
|
+
case 'awaiting-approval': activeIdx = 1; break;
|
|
727
|
+
case 'executing': activeIdx = 2; break;
|
|
728
|
+
case 'done':
|
|
729
|
+
case 'failed':
|
|
730
|
+
case 'killed': activeIdx = 3; break;
|
|
731
|
+
default: activeIdx = 0;
|
|
732
|
+
}
|
|
733
|
+
let html = '';
|
|
734
|
+
for (let i = 0; i < PHASES.length; i++) {
|
|
735
|
+
let cls = 'qa-phase-chip';
|
|
736
|
+
if (i < activeIdx) cls += ' qa-phase-chip--done';
|
|
737
|
+
else if (i === activeIdx) cls += ' qa-phase-chip--active';
|
|
738
|
+
else cls += ' qa-phase-chip--pending';
|
|
739
|
+
if (i === 3 && state === 'failed') cls += ' qa-phase-chip--failed';
|
|
740
|
+
if (i === 3 && state === 'killed') cls += ' qa-phase-chip--killed';
|
|
741
|
+
html += '<span class="' + escHtml(cls) + '">' + escHtml(PHASES[i].icon) + ' ' + escHtml(PHASES[i].label) + '</span>';
|
|
742
|
+
if (i < PHASES.length - 1) html += '<span class="qa-phase-arrow">→</span>';
|
|
743
|
+
}
|
|
744
|
+
return html;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function _qaSummarizeTarget(target) {
|
|
748
|
+
if (!target || typeof target !== 'object') return '(unknown)';
|
|
749
|
+
switch (target.kind) {
|
|
750
|
+
case 'pr': return 'PR#' + (target.prId || '?');
|
|
751
|
+
case 'branch': return 'branch:' + (target.branch || '?');
|
|
752
|
+
case 'commit': return 'commit:' + String(target.sha || '').slice(0, 8);
|
|
753
|
+
case 'current': return 'current:' + (target.worktree || '(agent cwd)');
|
|
754
|
+
default: return target.kind || '(unknown)';
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
async function qaSubmitSessionForm() {
|
|
759
|
+
const msg = document.getElementById('qa-session-form-msg');
|
|
760
|
+
const submit = document.getElementById('qa-session-submit');
|
|
761
|
+
if (msg) msg.textContent = '';
|
|
762
|
+
if (submit) submit.disabled = true;
|
|
763
|
+
|
|
764
|
+
const kind = (document.getElementById('qa-session-target-kind') || {}).value;
|
|
765
|
+
const target = { kind };
|
|
766
|
+
if (kind === 'pr') target.prId = (document.getElementById('qa-session-target-pr-id') || {}).value || '';
|
|
767
|
+
if (kind === 'branch') target.branch = (document.getElementById('qa-session-target-branch') || {}).value || '';
|
|
768
|
+
if (kind === 'commit') target.sha = (document.getElementById('qa-session-target-sha') || {}).value || '';
|
|
769
|
+
if (kind === 'current') {
|
|
770
|
+
const wt = (document.getElementById('qa-session-target-worktree') || {}).value || '';
|
|
771
|
+
if (wt) target.worktree = wt;
|
|
772
|
+
}
|
|
773
|
+
const flowsRaw = (document.getElementById('qa-session-flows') || {}).value || '';
|
|
774
|
+
const mode = (document.getElementById('qa-session-mode') || {}).value || 'confirm';
|
|
775
|
+
const runner = (document.getElementById('qa-session-runner') || {}).value || '';
|
|
776
|
+
const project = (document.getElementById('qa-session-project') || {}).value || '';
|
|
777
|
+
const capture = {
|
|
778
|
+
video: !!(document.getElementById('qa-session-capture-video') || {}).checked,
|
|
779
|
+
screenshots: !!(document.getElementById('qa-session-capture-screenshots') || {}).checked,
|
|
780
|
+
logs: !!(document.getElementById('qa-session-capture-logs') || {}).checked,
|
|
781
|
+
};
|
|
782
|
+
const body = { target, flowsRaw, mode, capture };
|
|
783
|
+
if (runner) body.runner = runner;
|
|
784
|
+
if (project) body.project = project;
|
|
785
|
+
|
|
786
|
+
try {
|
|
787
|
+
const res = await fetch('/api/qa/session', {
|
|
788
|
+
method: 'POST',
|
|
789
|
+
headers: { 'Content-Type': 'application/json' },
|
|
790
|
+
body: JSON.stringify(body),
|
|
791
|
+
});
|
|
792
|
+
const json = await res.json().catch(() => ({}));
|
|
793
|
+
if (!res.ok) {
|
|
794
|
+
if (msg) msg.textContent = 'Error ' + res.status + ': ' + (json.error || 'unknown');
|
|
795
|
+
if (msg) msg.style.color = 'var(--red)';
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
if (msg) {
|
|
799
|
+
msg.textContent = 'Session ' + (json.sessionId || '') + ' queued.';
|
|
800
|
+
msg.style.color = 'var(--green)';
|
|
801
|
+
}
|
|
802
|
+
// Reset only the flows (the rest is sticky for fast iteration).
|
|
803
|
+
const flowsEl = document.getElementById('qa-session-flows');
|
|
804
|
+
if (flowsEl) flowsEl.value = '';
|
|
805
|
+
// Optimistic refresh + restart polling — a new non-terminal session
|
|
806
|
+
// means the auto-stop heuristic should reactivate.
|
|
807
|
+
_startQaSessionsPoll();
|
|
808
|
+
} catch (e) {
|
|
809
|
+
if (msg) {
|
|
810
|
+
msg.textContent = 'Network error: ' + (e && e.message || String(e));
|
|
811
|
+
msg.style.color = 'var(--red)';
|
|
812
|
+
}
|
|
813
|
+
} finally {
|
|
814
|
+
if (submit) submit.disabled = false;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
async function _qaSessionPost(url, label, body) {
|
|
819
|
+
try {
|
|
820
|
+
const res = await fetch(url, {
|
|
821
|
+
method: 'POST',
|
|
822
|
+
headers: { 'Content-Type': 'application/json' },
|
|
823
|
+
body: body ? JSON.stringify(body) : '{}',
|
|
824
|
+
});
|
|
825
|
+
const json = await res.json().catch(() => ({}));
|
|
826
|
+
if (!res.ok) {
|
|
827
|
+
alert(label + ' failed (' + res.status + '): ' + (json.error || 'unknown'));
|
|
828
|
+
return null;
|
|
829
|
+
}
|
|
830
|
+
_startQaSessionsPoll();
|
|
831
|
+
return json;
|
|
832
|
+
} catch (e) {
|
|
833
|
+
alert(label + ' failed: ' + (e && e.message || String(e)));
|
|
834
|
+
return null;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function qaSessionApprove(id) {
|
|
839
|
+
if (!id) return null;
|
|
840
|
+
return _qaSessionPost('/api/qa/sessions/' + encodeURIComponent(id) + '/approve', 'approve');
|
|
841
|
+
}
|
|
842
|
+
function qaSessionCancel(id) {
|
|
843
|
+
if (!id) return null;
|
|
844
|
+
if (!confirm('Cancel session ' + id + '? The managed-spawn keeps running — use Kill to also tear it down.')) return null;
|
|
845
|
+
return _qaSessionPost('/api/qa/sessions/' + encodeURIComponent(id) + '/cancel', 'cancel');
|
|
846
|
+
}
|
|
847
|
+
function qaSessionDismiss(id) {
|
|
848
|
+
if (!id) return null;
|
|
849
|
+
if (!confirm('Mark session ' + id + ' done without running EXECUTE?')) return null;
|
|
850
|
+
return _qaSessionPost('/api/qa/sessions/' + encodeURIComponent(id) + '/dismiss', 'dismiss');
|
|
851
|
+
}
|
|
852
|
+
function qaSessionKill(id) {
|
|
853
|
+
if (!id) return null;
|
|
854
|
+
if (!confirm('Kill session ' + id + ' AND its managed-spawn?')) return null;
|
|
855
|
+
return _qaSessionPost('/api/qa/sessions/' + encodeURIComponent(id) + '/kill', 'kill');
|
|
856
|
+
}
|
|
857
|
+
function qaSessionEdit(id, feedback) {
|
|
858
|
+
if (!id) return null;
|
|
859
|
+
if (!feedback || !feedback.trim()) return null;
|
|
860
|
+
return _qaSessionPost('/api/qa/sessions/' + encodeURIComponent(id) + '/edit', 'edit', { feedback });
|
|
861
|
+
}
|
|
862
|
+
// Convenience wrapper used by the card UI — prompt for feedback then POST.
|
|
863
|
+
function qaSessionEditPrompt(id) {
|
|
864
|
+
const fb = prompt('Reviewer feedback for the next DRAFT pass:');
|
|
865
|
+
if (!fb || !fb.trim()) return null;
|
|
866
|
+
return qaSessionEdit(id, fb);
|
|
867
|
+
}
|
package/dashboard/js/state.js
CHANGED
|
@@ -43,7 +43,7 @@ let currentPage = getPageFromUrl();
|
|
|
43
43
|
// _stopMeetingPoll, _stopQaRunsPoll all clearInterval on a nullable handle;
|
|
44
44
|
// closeDetail / closeManagedLog short-circuit when no panel/stream is open.
|
|
45
45
|
const PAGE_LAZY_LOADERS = {
|
|
46
|
-
qa: ['loadQaTargets', 'loadQaRunbooks', '_startQaRunsPoll'],
|
|
46
|
+
qa: ['loadQaTargets', 'loadQaRunbooks', 'loadQaRunners', 'loadQaSessions', '_startQaSessionsPoll', '_startQaRunsPoll'],
|
|
47
47
|
plans: ['refreshPlans'],
|
|
48
48
|
inbox: ['refreshKnowledgeBase'],
|
|
49
49
|
};
|
|
@@ -53,6 +53,7 @@ const PAGE_LEAVE_HOOKS = [
|
|
|
53
53
|
'_stopMeetingPoll',
|
|
54
54
|
'closeDetail',
|
|
55
55
|
'_stopQaRunsPoll',
|
|
56
|
+
'_stopQaSessionsPoll',
|
|
56
57
|
'closeManagedLog',
|
|
57
58
|
];
|
|
58
59
|
|
package/dashboard/pages/qa.html
CHANGED
|
@@ -2,6 +2,78 @@
|
|
|
2
2
|
<h2>QA</h2>
|
|
3
3
|
<p class="empty" style="margin:4px 0 12px 0">Validation runbooks dispatched against live managed instances. Targets, runbooks, and run history with artifact previews.</p>
|
|
4
4
|
</section>
|
|
5
|
+
<section id="qa-sessions-section" class="qa-section">
|
|
6
|
+
<h2>QA Sessions <span class="qa-section-subtitle">natural-language smoke tests — engine sets up a live target, drafts a runner-native test, and (with your approval) executes it</span></h2>
|
|
7
|
+
<div class="qa-session-form-wrap">
|
|
8
|
+
<form id="qa-session-form" onsubmit="event.preventDefault();qaSubmitSessionForm();">
|
|
9
|
+
<div class="qa-form-row qa-form-row--grid">
|
|
10
|
+
<div>
|
|
11
|
+
<label class="qa-form-label" for="qa-session-target-kind">Target</label>
|
|
12
|
+
<select id="qa-session-target-kind" class="qa-form-input" onchange="qaSessionTargetKindChanged()">
|
|
13
|
+
<option value="current">Current worktree</option>
|
|
14
|
+
<option value="pr">Pull request</option>
|
|
15
|
+
<option value="branch">Branch</option>
|
|
16
|
+
<option value="commit">Commit SHA</option>
|
|
17
|
+
</select>
|
|
18
|
+
</div>
|
|
19
|
+
<div>
|
|
20
|
+
<label class="qa-form-label" for="qa-session-project">Project</label>
|
|
21
|
+
<input id="qa-session-project" type="text" class="qa-form-input" placeholder="(blank = central)">
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="qa-form-row qa-session-target-fields">
|
|
25
|
+
<div id="qa-session-target-pr-wrap" class="qa-session-target-sub" style="display:none">
|
|
26
|
+
<label class="qa-form-label" for="qa-session-target-pr-id">PR id</label>
|
|
27
|
+
<input id="qa-session-target-pr-id" type="text" class="qa-form-input" placeholder="2887">
|
|
28
|
+
</div>
|
|
29
|
+
<div id="qa-session-target-branch-wrap" class="qa-session-target-sub" style="display:none">
|
|
30
|
+
<label class="qa-form-label" for="qa-session-target-branch">Branch</label>
|
|
31
|
+
<input id="qa-session-target-branch" type="text" class="qa-form-input" placeholder="work/my-feature">
|
|
32
|
+
</div>
|
|
33
|
+
<div id="qa-session-target-sha-wrap" class="qa-session-target-sub" style="display:none">
|
|
34
|
+
<label class="qa-form-label" for="qa-session-target-sha">Commit SHA</label>
|
|
35
|
+
<input id="qa-session-target-sha" type="text" class="qa-form-input" placeholder="abc1234">
|
|
36
|
+
</div>
|
|
37
|
+
<div id="qa-session-target-worktree-wrap" class="qa-session-target-sub">
|
|
38
|
+
<label class="qa-form-label" for="qa-session-target-worktree">Worktree path</label>
|
|
39
|
+
<input id="qa-session-target-worktree" type="text" class="qa-form-input" placeholder="(blank = $MINIONS_AGENT_CWD)">
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="qa-form-row">
|
|
43
|
+
<label class="qa-form-label" for="qa-session-flows">Flows</label>
|
|
44
|
+
<textarea id="qa-session-flows" class="qa-form-input qa-form-textarea" placeholder="open the homepage, click login, verify the dashboard renders" required></textarea>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="qa-form-row qa-form-row--inline">
|
|
47
|
+
<div>
|
|
48
|
+
<label class="qa-form-label" for="qa-session-mode">Mode</label>
|
|
49
|
+
<select id="qa-session-mode" class="qa-form-input">
|
|
50
|
+
<option value="confirm">Confirm (review draft first)</option>
|
|
51
|
+
<option value="auto">Auto (skip approval)</option>
|
|
52
|
+
</select>
|
|
53
|
+
</div>
|
|
54
|
+
<div>
|
|
55
|
+
<label class="qa-form-label" for="qa-session-runner">Runner</label>
|
|
56
|
+
<select id="qa-session-runner" class="qa-form-input">
|
|
57
|
+
<option value="">Auto-detect</option>
|
|
58
|
+
</select>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="qa-session-capture-group">
|
|
61
|
+
<label class="qa-form-label">Capture</label>
|
|
62
|
+
<label class="qa-session-capture-item"><input id="qa-session-capture-screenshots" type="checkbox" checked> screenshots</label>
|
|
63
|
+
<label class="qa-session-capture-item"><input id="qa-session-capture-video" type="checkbox"> video</label>
|
|
64
|
+
<label class="qa-session-capture-item"><input id="qa-session-capture-logs" type="checkbox" checked> logs</label>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
<div class="qa-form-row qa-form-actions">
|
|
68
|
+
<button type="submit" id="qa-session-submit" class="qa-btn-primary">Start QA Session</button>
|
|
69
|
+
<span id="qa-session-form-msg" class="qa-form-msg"></span>
|
|
70
|
+
</div>
|
|
71
|
+
</form>
|
|
72
|
+
</div>
|
|
73
|
+
<div id="qa-sessions-content" class="qa-sessions-list">
|
|
74
|
+
<p class="empty">Loading sessions…</p>
|
|
75
|
+
</div>
|
|
76
|
+
</section>
|
|
5
77
|
<section id="qa-targets-section" class="qa-section">
|
|
6
78
|
<h2>Targets <span class="qa-section-subtitle">live managed/keep processes available as runbook targets — full inventory lives on <a href="/engine" class="qa-engine-link">/engine</a></span></h2>
|
|
7
79
|
<div id="qa-targets-content" class="qa-targets-list">
|
package/dashboard/styles.css
CHANGED
|
@@ -1005,6 +1005,108 @@
|
|
|
1005
1005
|
}
|
|
1006
1006
|
.qa-artifact-log { max-width: 480px; }
|
|
1007
1007
|
|
|
1008
|
+
/* ── P-h7e4f9b2: QA Sessions section ───────────────────────────────── */
|
|
1009
|
+
.qa-session-form-wrap {
|
|
1010
|
+
background: var(--surface2); border: 1px solid var(--border);
|
|
1011
|
+
border-radius: var(--radius-md); padding: var(--space-5);
|
|
1012
|
+
margin-bottom: var(--space-6);
|
|
1013
|
+
}
|
|
1014
|
+
.qa-form-row--grid {
|
|
1015
|
+
display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-5);
|
|
1016
|
+
}
|
|
1017
|
+
.qa-form-row--inline {
|
|
1018
|
+
display: flex; flex-wrap: wrap; gap: var(--space-5); align-items: flex-end;
|
|
1019
|
+
}
|
|
1020
|
+
.qa-form-row--inline > div { display: flex; flex-direction: column; gap: var(--space-2); }
|
|
1021
|
+
.qa-session-target-fields { gap: var(--space-3); }
|
|
1022
|
+
.qa-session-target-sub { display: flex; flex-direction: column; gap: var(--space-2); }
|
|
1023
|
+
.qa-session-capture-group { display: flex; flex-direction: column; gap: var(--space-2); }
|
|
1024
|
+
.qa-session-capture-item {
|
|
1025
|
+
display: inline-flex; align-items: center; gap: var(--space-2);
|
|
1026
|
+
font-size: var(--text-base); color: var(--text);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
.qa-sessions-list { display: flex; flex-direction: column; gap: var(--space-4); }
|
|
1030
|
+
.qa-session-card {
|
|
1031
|
+
border: 1px solid var(--border); border-radius: var(--radius-md);
|
|
1032
|
+
background: var(--surface2); padding: var(--space-4) var(--space-5);
|
|
1033
|
+
display: flex; flex-direction: column; gap: var(--space-3);
|
|
1034
|
+
}
|
|
1035
|
+
.qa-session-state-failed { border-left: 3px solid var(--red); }
|
|
1036
|
+
.qa-session-state-killed { border-left: 3px solid var(--muted); }
|
|
1037
|
+
.qa-session-state-done { border-left: 3px solid var(--green); }
|
|
1038
|
+
.qa-session-state-awaiting-approval { border-left: 3px solid var(--yellow); }
|
|
1039
|
+
.qa-session-state-executing,
|
|
1040
|
+
.qa-session-state-drafting,
|
|
1041
|
+
.qa-session-state-spawning { border-left: 3px solid var(--blue); }
|
|
1042
|
+
.qa-session-head {
|
|
1043
|
+
display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap;
|
|
1044
|
+
font-size: var(--text-md);
|
|
1045
|
+
}
|
|
1046
|
+
.qa-session-id { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--text); }
|
|
1047
|
+
.qa-session-state {
|
|
1048
|
+
display: inline-block; font-size: var(--text-xs); font-weight: 700;
|
|
1049
|
+
padding: 2px var(--space-3); border-radius: var(--radius-sm);
|
|
1050
|
+
text-transform: uppercase; letter-spacing: 0.5px;
|
|
1051
|
+
background: var(--surface); color: var(--muted); border: 1px solid var(--border);
|
|
1052
|
+
}
|
|
1053
|
+
.qa-session-state-done .qa-session-state { color: var(--green); border-color: var(--green); }
|
|
1054
|
+
.qa-session-state-failed .qa-session-state { color: var(--red); border-color: var(--red); }
|
|
1055
|
+
.qa-session-state-killed .qa-session-state { color: var(--muted); }
|
|
1056
|
+
.qa-session-state-awaiting-approval .qa-session-state { color: var(--yellow); border-color: var(--yellow); }
|
|
1057
|
+
.qa-session-state-executing .qa-session-state,
|
|
1058
|
+
.qa-session-state-drafting .qa-session-state,
|
|
1059
|
+
.qa-session-state-spawning .qa-session-state { color: var(--blue); border-color: var(--blue); }
|
|
1060
|
+
|
|
1061
|
+
.qa-session-phases {
|
|
1062
|
+
display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
|
|
1063
|
+
font-size: var(--text-base);
|
|
1064
|
+
}
|
|
1065
|
+
.qa-phase-chip {
|
|
1066
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
1067
|
+
padding: 2px var(--space-3); border-radius: var(--radius-sm);
|
|
1068
|
+
background: var(--surface); color: var(--muted);
|
|
1069
|
+
border: 1px solid var(--border);
|
|
1070
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
1071
|
+
font-size: var(--text-xs);
|
|
1072
|
+
}
|
|
1073
|
+
.qa-phase-chip--pending { opacity: 0.5; }
|
|
1074
|
+
.qa-phase-chip--active {
|
|
1075
|
+
color: var(--blue); border-color: var(--blue); font-weight: 600;
|
|
1076
|
+
background: rgba(56, 139, 253, 0.08);
|
|
1077
|
+
}
|
|
1078
|
+
.qa-phase-chip--done { color: var(--green); border-color: var(--green); }
|
|
1079
|
+
.qa-phase-chip--failed { color: var(--red); border-color: var(--red); }
|
|
1080
|
+
.qa-phase-chip--killed { color: var(--muted); border-color: var(--muted); }
|
|
1081
|
+
.qa-phase-arrow { color: var(--muted); font-size: var(--text-xs); }
|
|
1082
|
+
|
|
1083
|
+
.qa-session-meta {
|
|
1084
|
+
display: flex; flex-wrap: wrap; gap: var(--space-4);
|
|
1085
|
+
font-size: var(--text-base); color: var(--muted);
|
|
1086
|
+
}
|
|
1087
|
+
.qa-session-meta-ts { margin-left: auto; }
|
|
1088
|
+
.qa-session-flows {
|
|
1089
|
+
font-size: var(--text-base); color: var(--text);
|
|
1090
|
+
background: var(--surface); padding: var(--space-3);
|
|
1091
|
+
border-radius: var(--radius-sm); border: 1px solid var(--border);
|
|
1092
|
+
}
|
|
1093
|
+
.qa-session-error {
|
|
1094
|
+
font-size: var(--text-base); color: var(--red);
|
|
1095
|
+
background: rgba(248, 81, 73, 0.08); padding: var(--space-3);
|
|
1096
|
+
border-radius: var(--radius-sm); border-left: 3px solid var(--red);
|
|
1097
|
+
}
|
|
1098
|
+
.qa-session-summary {
|
|
1099
|
+
font-size: var(--text-base); color: var(--text);
|
|
1100
|
+
background: var(--surface); padding: var(--space-3);
|
|
1101
|
+
border-radius: var(--radius-sm);
|
|
1102
|
+
}
|
|
1103
|
+
.qa-session-actions {
|
|
1104
|
+
display: flex; gap: var(--space-3); flex-wrap: wrap;
|
|
1105
|
+
padding-top: var(--space-3); border-top: 1px solid var(--border);
|
|
1106
|
+
}
|
|
1107
|
+
.qa-btn-danger { color: var(--red); }
|
|
1108
|
+
.qa-btn-danger:hover { background: rgba(248, 81, 73, 0.08); }
|
|
1109
|
+
|
|
1008
1110
|
/* W-mpmwxni2000c25c7-d - Command Center / doc-chat typed error bubble. */
|
|
1009
1111
|
/* Token-only styling so dark/light themes stay consistent; the inline */
|
|
1010
1112
|
/* styles emitted by command-center.js use the same vars and are kept */
|