Skip to content
This repository was archived by the owner on Jun 8, 2026. It is now read-only.

Commit 273d839

Browse files
committed
Merge remote-tracking branch 'origin/main' into symphony/pln-745
2 parents 7ae22a2 + c1b165b commit 273d839

71 files changed

Lines changed: 11410 additions & 128 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,10 @@ apps/desktop/.generated/
1919
# Playwright test artifacts (apps/desktop/test-e2e)
2020
apps/desktop/test-results/
2121
apps/desktop/playwright-report/
22+
23+
# UI Numbers Audit generated reports (FEA-1415 / PLN-738)
24+
# These embed a timestamp; regenerate locally via `pnpm --filter desktop audit:report`
25+
# or `node apps/desktop/test-e2e/agent-monitor/inventory/scan-tiles.mjs`.
26+
apps/desktop/test-e2e/agent-monitor/inventory/REPORT-*.md
27+
apps/desktop/test-e2e/agent-monitor/inventory/INVENTORY-DRAFT.md
28+
apps/desktop/test-e2e/agent-monitor/inventory/manifest.scanned.json

apps/desktop/ci/README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Desktop CI workflow staging
2+
3+
## `audit-gate.yml` — UI Numbers Audit Gate (FEA-1437 Phase 6)
4+
5+
This is the Phase 6 CI gate for the UI Numbers Audit. It lives here, **not** in
6+
`.github/workflows/`, only because the automation token that opened this PR
7+
lacks the GitHub `workflow` OAuth scope (pushing a file under
8+
`.github/workflows/` is rejected without it).
9+
10+
**To activate** (one step, needs a token/UI with `workflow` scope):
11+
12+
```bash
13+
git mv apps/desktop/ci/audit-gate.yml .github/workflows/audit-gate.yml
14+
git commit -m "ci: activate UI Numbers Audit gate"
15+
```
16+
17+
Until moved, the gate does not run automatically, but every check it performs is
18+
runnable locally and is identical to the existing `test:audit` suite:
19+
20+
```bash
21+
pnpm -C apps/desktop audit:coverage # static coverage gate (fast, no build)
22+
pnpm -C apps/desktop test:audit # full node-side audit
23+
pnpm -C apps/desktop test:audit:ui # headless Playwright tile audit
24+
```
25+
26+
The workflow has two jobs:
27+
28+
- **coverage** — runs `audit:coverage`: regenerates the tile scan + coverage
29+
classification, asserts every scanner detection is classified (no
30+
`needs_review`), asserts every harness parser has a `*-parser.contract.test.mjs`,
31+
and posts the `by_status` breakdown to the job summary. This is the merge gate
32+
that fires when a new route / tile / parser lands without manifest coverage.
33+
- **ui-audit** — builds the agent-monitor sidecar and runs the headless
34+
Playwright tile audits (rendered DOM == oracle).
35+
36+
Real-Electron and visual-regression suites are intentionally excluded (they run
37+
elsewhere as non-blocking, flaky-by-nature per PLN-760).
38+
39+
### Branch-protection guidance (read before marking required)
40+
41+
Mark **only the `coverage` job** as a required status check. Leave `ui-audit`
42+
optional (it's heavier — builds the sidecar + Playwright — and shouldn't block on
43+
infra flake). The workflow has **no `paths:` filter** by design: a required check
44+
combined with a paths filter deadlocks any PR that doesn't touch `apps/desktop/**`
45+
(the job never triggers, so the required status sits pending forever). Triggering
46+
on every PR keeps the required `coverage` check from hanging; the job is fast.

apps/desktop/ci/audit-gate.yml

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
name: UI Numbers Audit Gate
2+
3+
# FEA-1437 Phase 6 — gate PRs on the UI Numbers Audit. Two jobs:
4+
# coverage — fast, static: every scanner detection is classified (no
5+
# needs_review), every harness parser has a contract test. This
6+
# is the merge gate that fires when a new route / tile / parser
7+
# lands without manifest coverage.
8+
# ui-audit — heavier: builds the agent-monitor sidecar and runs the headless
9+
# Playwright tile audits (rendered DOM == oracle).
10+
# Real-Electron + visual-regression suites are intentionally NOT here; they run
11+
# elsewhere as non-blocking (flaky-by-nature) per PLN-760.
12+
#
13+
# NO `paths:` filter on purpose. The `coverage` job is intended to be a REQUIRED
14+
# status check, and "required check + paths filter" is a classic deadlock: a PR
15+
# that doesn't touch apps/desktop/** never triggers the job, so the required
16+
# status sits pending forever and the PR can't merge. Triggering on every PR
17+
# keeps the required check from hanging. The `coverage` job is fast; only mark
18+
# `coverage` as required (leave `ui-audit` optional — see apps/desktop/ci/README.md).
19+
20+
on:
21+
pull_request:
22+
types: [opened, synchronize, reopened]
23+
push:
24+
branches: [main]
25+
26+
concurrency:
27+
group: audit-gate-${{ github.event.pull_request.number || github.ref }}
28+
cancel-in-progress: true
29+
30+
jobs:
31+
coverage:
32+
name: Coverage gate (static)
33+
runs-on: ubuntu-latest
34+
timeout-minutes: 10
35+
permissions:
36+
contents: read
37+
packages: read
38+
steps:
39+
- uses: actions/checkout@v4
40+
- uses: pnpm/action-setup@v4
41+
- uses: actions/setup-node@v4
42+
with:
43+
node-version: 22
44+
cache: pnpm
45+
- name: Install dependencies
46+
run: pnpm install --frozen-lockfile
47+
env:
48+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
49+
- name: Generate build info
50+
run: pnpm -r prebuild
51+
- name: Audit coverage gate
52+
run: pnpm -C apps/desktop audit:coverage
53+
- name: Coverage summary -> job summary
54+
if: always()
55+
# Reporting only — never let a summary glitch fail the gate job.
56+
continue-on-error: true
57+
run: |
58+
COV=apps/desktop/test-e2e/agent-monitor/inventory/coverage.json
59+
if [ -f "$COV" ]; then
60+
{
61+
echo "### UI Numbers Audit — coverage"
62+
echo ""
63+
echo "| status | count |"
64+
echo "|---|---|"
65+
# Resolve to an absolute path: require() treats a bare relative
66+
# string as a node_modules specifier and would throw.
67+
node -e '
68+
const c = require(require("path").resolve(process.env.COV));
69+
for (const [k, v] of Object.entries(c.by_status)) console.log(`| ${k} | ${v} |`);
70+
console.log(`| **total** | **${c.total_detections}** |`);
71+
'
72+
} >> "$GITHUB_STEP_SUMMARY"
73+
fi
74+
env:
75+
COV: apps/desktop/test-e2e/agent-monitor/inventory/coverage.json
76+
77+
ui-audit:
78+
name: UI tile audit (headless Playwright)
79+
runs-on: ubuntu-latest
80+
timeout-minutes: 25
81+
permissions:
82+
contents: read
83+
packages: read
84+
steps:
85+
- uses: actions/checkout@v4
86+
- uses: pnpm/action-setup@v4
87+
- uses: actions/setup-node@v4
88+
with:
89+
node-version: 22
90+
cache: pnpm
91+
- name: Install dependencies
92+
run: pnpm install --frozen-lockfile
93+
env:
94+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
95+
- name: Generate build info
96+
run: pnpm -r prebuild
97+
- name: Install Playwright Chromium
98+
run: pnpm -C apps/desktop exec playwright install --with-deps chromium
99+
- name: Run headless tile audit
100+
run: pnpm -C apps/desktop test:audit:ui
101+
- name: Upload Playwright report on failure
102+
if: failure()
103+
uses: actions/upload-artifact@v4
104+
with:
105+
name: playwright-audit-report
106+
path: apps/desktop/test-results/
107+
retention-days: 7
108+
if-no-files-found: ignore

apps/desktop/package.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "desktop",
3-
"version": "0.15.92",
3+
"version": "0.15.101",
44
"description": "ClosedLoop Desktop",
55
"author": "ClosedLoop AI <support@closedloop.ai>",
66
"private": true,
@@ -24,6 +24,15 @@
2424
"test:contract": "node --test \"test-e2e/agent-monitor/specs/api-contract/*.test.mjs\"",
2525
"pretest:e2e": "pnpm build:agent-monitor",
2626
"test:e2e": "playwright test --config test-e2e/agent-monitor/playwright.config.ts",
27+
"pretest:audit": "pnpm build:agent-monitor && pnpm audit:scan && pnpm audit:classify",
28+
"audit:scan": "node test-e2e/agent-monitor/inventory/scan-tiles.mjs",
29+
"audit:classify": "node test-e2e/agent-monitor/inventory/coverage-classifier.mjs",
30+
"audit:coverage": "bash scripts/check-audit-coverage.sh",
31+
"test:audit": "node --test \"test-e2e/agent-monitor/specs/audit/*.test.mjs\"",
32+
"pretest:audit:ui": "pnpm build:agent-monitor",
33+
"test:audit:ui": "playwright test --config test-e2e/agent-monitor/playwright.audit.config.ts",
34+
"preaudit:report": "pnpm build:agent-monitor",
35+
"audit:report": "node test-e2e/agent-monitor/inventory/run-report.mjs",
2736
"package": "pnpm clean:package && pnpm build && pnpm stage:package && node scripts/run-electron-builder.mjs",
2837
"release": "pnpm clean:package && pnpm build && pnpm stage:package && node scripts/run-electron-builder.mjs --publish always"
2938
},

apps/desktop/scripts/agent-monitor-client/Dashboard.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -549,22 +549,32 @@ function StatPill({
549549
sub,
550550
icon: Icon,
551551
color = "text-accent",
552+
testid,
553+
subTestid,
552554
}: {
553555
label: string;
554556
value: string | number;
555557
raw?: string;
556558
sub?: string;
557559
icon: React.ElementType;
558560
color?: string;
561+
testid?: string;
562+
subTestid?: string;
559563
}) {
560564
return (
561565
<div className="card p-5 flex flex-col gap-2">
562566
<div className="flex items-center justify-between">
563567
<span className="text-xs text-gray-500 uppercase tracking-wider">{label}</span>
564568
<Icon className={`w-4 h-4 ${color}`} />
565569
</div>
566-
<p className={`text-2xl font-bold ${color}`}>{raw ? <Tip raw={raw}>{value}</Tip> : value}</p>
567-
{sub && <p className="text-[11px] text-gray-500">{sub}</p>}
570+
<p className={`text-2xl font-bold ${color}`} data-testid={testid}>
571+
{raw ? <Tip raw={raw}>{value}</Tip> : value}
572+
</p>
573+
{sub && (
574+
<p className="text-[11px] text-gray-500" data-testid={subTestid}>
575+
{sub}
576+
</p>
577+
)}
568578
</div>
569579
);
570580
}
@@ -1806,6 +1816,8 @@ export function Dashboard() {
18061816
sub={`${analyticsData?.overview.active_sessions ?? stats?.active_sessions ?? 0} active`}
18071817
icon={FolderOpen}
18081818
color="text-blue-400"
1819+
testid="audit-dashboard-monitor-total-sessions"
1820+
subTestid="audit-dashboard-monitor-total-sessions-trend-active"
18091821
/>
18101822
<StatPill
18111823
label="Total Agents"
@@ -1814,6 +1826,7 @@ export function Dashboard() {
18141826
sub={`${analyticsData?.overview.active_agents ?? stats?.active_agents ?? 0} active`}
18151827
icon={Bot}
18161828
color="text-emerald-400"
1829+
subTestid="audit-dashboard-monitor-active-agents"
18171830
/>
18181831
<StatPill
18191832
label="Total Tokens"
@@ -1834,6 +1847,7 @@ export function Dashboard() {
18341847
}
18351848
icon={DollarSign}
18361849
color="text-emerald-400"
1850+
testid="audit-dashboard-monitor-total-cost"
18371851
/>
18381852
<StatPill
18391853
label="Total Events"
@@ -1846,6 +1860,7 @@ export function Dashboard() {
18461860
sub={`~${analyticsData?.avg_events_per_session ?? 0} per session`}
18471861
icon={Zap}
18481862
color="text-yellow-400"
1863+
testid="audit-dashboard-monitor-total-events"
18491864
/>
18501865
</div>
18511866

apps/desktop/scripts/agent-monitor-client/Sessions.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ export function Sessions() {
191191
</span>
192192
)}
193193
</div>
194-
<p className="text-xs text-gray-500">
194+
<p className="text-xs text-gray-500" data-testid="audit-sessions-list-total">
195195
{t("sessionCount", { count: total })}
196196
{filter ? ` ${filter}` : ""}
197197
</p>

apps/desktop/scripts/agent-monitor-packs/client/PacksInstalled.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ export function PacksInstalled() {
141141
return (
142142
<button
143143
key={p.pack_id}
144+
data-testid="audit-pack-row"
144145
onClick={() => setSelectedId(p.pack_id)}
145146
className={`block w-full text-left rounded-lg border px-3 py-2 transition-colors ${
146147
active

apps/desktop/scripts/agent-monitor-packs/client/Skills.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ export function Skills() {
145145
return (
146146
<button
147147
key={s.skill_id}
148+
data-testid="audit-skill-row"
148149
onClick={() => setSelected(s)}
149150
className={`block w-full text-left rounded-lg border px-3 py-2 transition-colors ${
150151
active

apps/desktop/scripts/agent-monitor-packs/client/Tools.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export function Tools() {
125125
return (
126126
<button
127127
key={t.tool_name}
128+
data-testid="audit-tool-row"
128129
onClick={() => setSelected(t.tool_name)}
129130
className={`block w-full text-left rounded-lg border px-3 py-2 transition-colors ${
130131
active

apps/desktop/scripts/agent-monitor-plans/client/Plans.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ export function Plans() {
166166
return (
167167
<button
168168
key={p.id}
169+
data-testid="audit-plan-row"
169170
onClick={() => setSelectedId(p.id)}
170171
className={`block w-full text-left rounded-lg border px-3 py-2 transition-colors ${
171172
active

0 commit comments

Comments
 (0)