From 70bd26814332632e433acc1494334157cd1e3ace Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 10:56:57 +0000 Subject: [PATCH 1/2] ci: lint, typecheck, test and build the ui/ app (#452) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The graph debugger in ui/ had no CI: ci.yml runs only the Python toolchain, so a change there could break the build and merge green. It already had — main carried six eslint errors and two TypeScript errors nothing reported. New workflow ui.yml, path-filtered to ui/** and itself: bun install --frozen-lockfile (fails on a stale bun.lock), eslint, tsc --noEmit (vite transpiles without type-checking, so build alone would pass a type error), vitest via test:run (the canonical runner per ui/AGENTS.md; `bun test` lacks vi.mock hoisting), and vite build. Actions pinned by SHA like the other workflows; bun pinned to 1.3.14. Fixes to bring main green under it, none changing behaviour: - applyHeatmap.test.ts: makeNode returns a typed Node, dropping six `as any`. - ControlPanel.test.tsx: defaults gain the required colorMode and onToggleColorMode props. - useGraphStore.ts: the namespace filter's predicate names NodeNamespace, the type it narrows, instead of an unrelated string. Not included: prettier --check fails on 31 files. Reformatting them would bury this change in whitespace, and the issue scopes lint, typecheck, tests and build. Closes #452 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Uv6LUrB5HCQcqBvmcixYn3 --- .github/workflows/ui.yml | 58 +++++++++++++++++++++++++ ui/src/components/ControlPanel.test.tsx | 2 + ui/src/store/useGraphStore.ts | 4 +- ui/src/utils/applyHeatmap.test.ts | 15 ++++--- 4 files changed, 70 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/ui.yml diff --git a/.github/workflows/ui.yml b/.github/workflows/ui.yml new file mode 100644 index 00000000..a4040633 --- /dev/null +++ b/.github/workflows/ui.yml @@ -0,0 +1,58 @@ +name: UI + +# The graph debugger in ui/ had no CI at all (#452): ci.yml runs only the Python +# toolchain, so a change there could break the build and still merge green. It +# had already happened — when this job was added, main carried six eslint errors +# and two TypeScript errors that nothing had reported. +# +# Path-filtered so Python-only changes do not pay for a bun install, and the +# workflow file itself is in the filter so an edit to it runs it. +on: + push: + branches: [ main, dev, "feat/**", "fix/**", "docs/**" ] + paths: [ "ui/**", ".github/workflows/ui.yml" ] + pull_request: + branches: [ main ] + paths: [ "ui/**", ".github/workflows/ui.yml" ] + +permissions: + contents: read + +jobs: + ui-ci: + name: UI Verification + runs-on: ubuntu-latest + defaults: + run: + working-directory: ui + steps: + - name: Checkout Code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + # Pinned rather than "latest": the lockfile format moves with bun, and + # a runner picking up a new release must not be what turns this red. + bun-version: 1.3.14 + + - name: Install Dependencies + # --frozen-lockfile fails when bun.lock is out of step with package.json, + # the job's equivalent of `uv lock --check` in ci.yml. + run: bun install --frozen-lockfile + + - name: ESLint + run: bun run lint + + - name: TypeScript + # Vite transpiles TypeScript without type-checking it, so `build` below + # passes a type error. This is the only step that reads the types. + run: bunx tsc --noEmit -p . + + - name: Tests + # vitest, not `bun test`: the native runner has no `vi.mock` hoisting, and + # component tests that use it fail under it (ui/AGENTS.md). + run: bun run test:run + + - name: Build + run: bun run build diff --git a/ui/src/components/ControlPanel.test.tsx b/ui/src/components/ControlPanel.test.tsx index b86d38ff..6c1ed555 100644 --- a/ui/src/components/ControlPanel.test.tsx +++ b/ui/src/components/ControlPanel.test.tsx @@ -26,6 +26,8 @@ function renderControlPanel(props: Partial[0]> = onBackToRoot: vi.fn(), searchQuery: "", setSearchQuery: vi.fn(), + colorMode: "type" as const, + onToggleColorMode: vi.fn(), }; return render(); } diff --git a/ui/src/store/useGraphStore.ts b/ui/src/store/useGraphStore.ts index 862ccd01..2f99ecf1 100644 --- a/ui/src/store/useGraphStore.ts +++ b/ui/src/store/useGraphStore.ts @@ -1,6 +1,6 @@ import { create } from 'zustand' import type { Node, Edge } from '@xyflow/react' -import type { GraphNode, GraphEdge } from '../types' +import type { GraphNode, GraphEdge, NodeNamespace } from '../types' type EdgeTypeName = 'CALLS' | 'IMPORTS' | 'EXTENDS' | 'CONTAINS' | 'DECLARES' type ViewMode = 'full' | 'flow' | 'ego' @@ -67,7 +67,7 @@ export const useGraphStore = create((set) => ({ ...new Set( graphNodes .map((n) => n.namespace) - .filter((ns): ns is string => typeof ns === 'string' && ns.length > 0) + .filter((ns): ns is NodeNamespace => typeof ns === 'string' && ns.length > 0) ), ] set((state) => ({ diff --git a/ui/src/utils/applyHeatmap.test.ts b/ui/src/utils/applyHeatmap.test.ts index c2fd22be..ba7d1a8b 100644 --- a/ui/src/utils/applyHeatmap.test.ts +++ b/ui/src/utils/applyHeatmap.test.ts @@ -1,7 +1,8 @@ import { describe, it, expect } from 'vitest' +import type { Node } from '@xyflow/react' import { applyHeatmapColors } from './applyHeatmap' -function makeNode(id: string, metadata: Record = {}) { +function makeNode(id: string, metadata: Record = {}): Node { return { id, type: 'default', @@ -14,38 +15,38 @@ function makeNode(id: string, metadata: Record = {}) { describe('applyHeatmapColors', () => { it('returns same array reference when disabled', () => { const nodes = [makeNode('a')] - const result = applyHeatmapColors(nodes as any, false) + const result = applyHeatmapColors(nodes, false) expect(result).toBe(nodes) }) it('overrides background/border for healthy node (fan_out=1)', () => { const nodes = [makeNode('a', { fan_out: 1, in_cycle: false })] - const result = applyHeatmapColors(nodes as any, true) + const result = applyHeatmapColors(nodes, true) expect(result[0].style?.background).toBe('#1b3a1b') expect(result[0].style?.borderColor).toBe('#66bb6a') }) it('uses critical color for in_cycle=true regardless of fan_out', () => { const nodes = [makeNode('a', { fan_out: 0, in_cycle: true })] - const result = applyHeatmapColors(nodes as any, true) + const result = applyHeatmapColors(nodes, true) expect(result[0].style?.background).toBe('#3b0000') }) it('uses warning color for fan_out=7', () => { const nodes = [makeNode('a', { fan_out: 7, in_cycle: false })] - const result = applyHeatmapColors(nodes as any, true) + const result = applyHeatmapColors(nodes, true) expect(result[0].style?.background).toBe('#3b2a00') }) it('preserves other style properties', () => { const nodes = [makeNode('a', { fan_out: 1, in_cycle: false })] - const result = applyHeatmapColors(nodes as any, true) + const result = applyHeatmapColors(nodes, true) expect(result[0].style).toBeDefined() }) it('nodes without health metadata get healthy color', () => { const nodes = [makeNode('a', {})] - const result = applyHeatmapColors(nodes as any, true) + const result = applyHeatmapColors(nodes, true) expect(result[0].style?.background).toBe('#1b3a1b') }) }) From a3bbe3e4c8475a39b17f8c10bb556e658aad0021 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 11:01:03 +0000 Subject: [PATCH 2/2] ci(ui): install without lifecycle scripts, typecheck through a package script SonarCloud failed the quality gate on #517 with two vulnerabilities in the new workflow: - githubactions:S6505: `bun install` without --ignore-scripts lets package lifecycle scripts run on the runner. No dependency here needs one: every step passes from a clean install with --ignore-scripts. - githubactions:S8543: `bunx tsc` could fetch an unpinned typescript. The check now runs as `bun run typecheck`, a package script using the typescript pinned in bun.lock. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Uv6LUrB5HCQcqBvmcixYn3 --- .github/workflows/ui.yml | 11 ++++++++--- ui/package.json | 1 + 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ui.yml b/.github/workflows/ui.yml index a4040633..eb61d2e1 100644 --- a/.github/workflows/ui.yml +++ b/.github/workflows/ui.yml @@ -38,8 +38,11 @@ jobs: - name: Install Dependencies # --frozen-lockfile fails when bun.lock is out of step with package.json, - # the job's equivalent of `uv lock --check` in ci.yml. - run: bun install --frozen-lockfile + # the job's equivalent of `uv lock --check` in ci.yml. --ignore-scripts + # because no dependency here needs an install hook — verified by running + # every step below from a clean install with it — so none gets to run + # code on the runner (Sonar githubactions:S6505). + run: bun install --frozen-lockfile --ignore-scripts - name: ESLint run: bun run lint @@ -47,7 +50,9 @@ jobs: - name: TypeScript # Vite transpiles TypeScript without type-checking it, so `build` below # passes a type error. This is the only step that reads the types. - run: bunx tsc --noEmit -p . + # Through a package script, so it runs the `typescript` pinned in + # bun.lock; `bunx tsc` could fetch an unpinned one (Sonar S8543). + run: bun run typecheck - name: Tests # vitest, not `bun test`: the native runner has no `vi.mock` hoisting, and diff --git a/ui/package.json b/ui/package.json index 8123c005..fc182369 100644 --- a/ui/package.json +++ b/ui/package.json @@ -20,6 +20,7 @@ "test:run": "vitest run", "preview": "vite preview", "lint": "eslint src/", + "typecheck": "tsc --noEmit -p .", "lint:fix": "eslint src/ --fix", "format": "prettier --write src/", "format:check": "prettier --check src/",