diff --git a/.serena/project.yml b/.serena/project.yml index 9551299c..79692ddc 100644 --- a/.serena/project.yml +++ b/.serena/project.yml @@ -1,31 +1,40 @@ # list of languages for which language servers are started; choose from: -# al bash clojure cpp csharp csharp_omnisharp -# dart elixir elm erlang fortran go -# haskell java julia kotlin lua markdown -# nix perl php python python_jedi r -# rego ruby ruby_solargraph rust scala swift -# terraform typescript typescript_vts yaml zig +# al bash clojure cpp csharp +# csharp_omnisharp dart elixir elm erlang +# fortran fsharp go groovy haskell +# java julia kotlin lua markdown +# matlab nix pascal perl php +# php_phpactor powershell python python_jedi r +# rego ruby ruby_solargraph rust scala +# swift terraform toml typescript typescript_vts +# vue yaml zig +# (This list may be outdated. For the current list, see values of Language enum here: +# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py +# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.) # Note: # - For C, use cpp # - For JavaScript, use typescript +# - For Free Pascal/Lazarus, use pascal # Special requirements: -# - csharp: Requires the presence of a .sln file in the project folder. +# Some languages require additional setup/installations. +# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers # When using multiple languages, the first language server that supports a given file will be used for that file. # The first language is the default language and the respective language server will be used as a fallback. # Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored. languages: - typescript - bash + +# the encoding used by text files in the project +# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings encoding: 'utf-8' -# whether to use the project's gitignore file to ignore files -# Added on 2025-04-07 +# whether to use project's .gitignore files to ignore files ignore_all_files_in_gitignore: true -# list of additional paths to ignore -# same syntax as gitignore, so you can use * and ** -# Was previously called `ignored_dirs`, please update your config if you are using that. -# Added (renamed) on 2025-04-07 +# list of additional paths to ignore in this project. +# Same syntax as gitignore, so you can use * and **. +# Note: global ignored_paths from serena_config.yml are also applied additively. ignored_paths: [] # whether the project is in read-only mode @@ -33,7 +42,9 @@ ignored_paths: [] # Added on 2025-04-18 read_only: false -# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details. +# list of tool names to exclude. +# This extends the existing exclusions (e.g. from the global configuration) +# # Below is the complete list of tools for convenience. # To make sure you have the latest list of tools, and to view their descriptions, # execute `uv run scripts/print_tool_overview.py`. @@ -77,6 +88,64 @@ excluded_tools: [] # initial prompt for the project. It will always be given to the LLM upon activating the project # (contrary to the memories, which are loaded on demand). initial_prompt: '' - +# the name by which the project can be referenced within Serena project_name: 'gitray' + +# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default). +# This extends the existing inclusions (e.g. from the global configuration). included_optional_tools: [] + +# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools. +# This cannot be combined with non-empty excluded_tools or included_optional_tools. +fixed_tools: [] + +# list of mode names to that are always to be included in the set of active modes +# The full set of modes to be activated is base_modes + default_modes. +# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply. +# Otherwise, this setting overrides the global configuration. +# Set this to [] to disable base modes for this project. +# Set this to a list of mode names to always include the respective modes for this project. +base_modes: + +# list of mode names that are to be activated by default. +# The full set of modes to be activated is base_modes + default_modes. +# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply. +# Otherwise, this overrides the setting from the global configuration (serena_config.yml). +# This setting can, in turn, be overridden by CLI parameters (--mode). +default_modes: + +# time budget (seconds) per tool call for the retrieval of additional symbol information +# such as docstrings or parameter information. +# This overrides the corresponding setting in the global configuration; see the documentation there. +# If null or missing, use the setting from the global configuration. +symbol_info_budget: + +# The language backend to use for this project. +# If not set, the global setting from serena_config.yml is used. +# Valid values: LSP, JetBrains +# Note: the backend is fixed at startup. If a project with a different backend +# is activated post-init, an error will be returned. +language_backend: + +# line ending convention to use when writing source files. +# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default) +# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings. +line_ending: + +# list of regex patterns which, when matched, mark a memory entry as read‑only. +# Extends the list from the global configuration, merging the two lists. +read_only_memory_patterns: [] + +# list of regex patterns for memories to completely ignore. +# Matching memories will not appear in list_memories or activate_project output +# and cannot be accessed via read_memory or write_memory. +# To access ignored memory files, use the read_file tool on the raw file path. +# Extends the list from the global configuration, merging the two lists. +# Example: ["_archive/.*", "_episodes/.*"] +ignored_memory_patterns: [] + +# advanced configuration option allowing to configure language server-specific options. +# Maps the language key to the options. +# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available. +# No documentation on options means no options are available. +ls_specific_settings: {} diff --git a/apps/frontend/__tests__/components/ActivityChart.test.tsx b/apps/frontend/__tests__/components/ActivityChart.test.tsx new file mode 100644 index 00000000..1c0c915b --- /dev/null +++ b/apps/frontend/__tests__/components/ActivityChart.test.tsx @@ -0,0 +1,147 @@ +import { render, screen } from '@testing-library/react'; +import { describe, test, expect, vi } from 'vitest'; +import { + ActivityChart, + ActivityChartTooltip, +} from '../../src/components/ActivityChart'; +import { Commit } from '@gitray/shared-types'; + +describe('ActivityChart Component', () => { + test('should render with no commits', () => { + // Arrange + const commits: Commit[] = []; + + // Act + render(); + + // Assert + expect( + document.querySelector('.recharts-responsive-container') + ).toBeInTheDocument(); + }); + + test('should render with commits in the last 30 days', () => { + // Arrange + const today = new Date(); + const yesterday = new Date(today); + yesterday.setDate(today.getDate() - 1); + + const commits: Commit[] = [ + { + sha: 'abc123', + message: 'feat: add feature', + authorName: 'Jonas', + authorEmail: 'jonas@example.com', + date: yesterday.toISOString(), + }, + { + sha: 'def456', + message: 'fix: fix bug', + authorName: 'Jonas', + authorEmail: 'jonas@example.com', + date: today.toISOString(), + }, + ]; + + // Act + render(); + + // Assert + expect( + document.querySelector('.recharts-responsive-container') + ).toBeInTheDocument(); + }); + + test('should bucket commits using local date, not UTC date', () => { + // Arrange — commits spread across multiple local days + // Dates are constructed via the Date constructor (local time), ensuring + // getFullYear/getMonth/getDate are used for bucketing, not toISOString (UTC). + const day1 = new Date(2026, 2, 10, 23, 45, 0); // Mar 10 23:45 local — would be Mar 11 UTC in UTC+1+ + const day2 = new Date(2026, 2, 11, 0, 15, 0); // Mar 11 00:15 local + const fixedNow = new Date(2026, 3, 7, 12, 0, 0); // Apr 7 — "today" for the 30-day window + + const RealDate = globalThis.Date; + const dateSpy = vi + .spyOn(globalThis, 'Date') + .mockImplementation((...args: unknown[]) => { + if (args.length === 0) return fixedNow; + // @ts-expect-error forward args to real Date constructor + return new RealDate(...args); + }); + + const commits: Commit[] = [ + { + sha: 'a1', + message: 'feat: a', + authorName: 'Jonas', + authorEmail: 'j@example.com', + date: day1.toISOString(), + }, + { + sha: 'a2', + message: 'feat: b', + authorName: 'Jonas', + authorEmail: 'j@example.com', + date: day2.toISOString(), + }, + ]; + + // Act + render(); + + // Assert — chart renders without error; bucketing logic ran without throwing + expect( + document.querySelector('.recharts-responsive-container') + ).toBeInTheDocument(); + + dateSpy.mockRestore(); + }); + + test('should ignore commits older than 30 days', () => { + // Arrange + const thirtyFiveDaysAgo = new Date(); + thirtyFiveDaysAgo.setDate(thirtyFiveDaysAgo.getDate() - 35); + + const commits: Commit[] = [ + { + sha: 'old123', + message: 'old commit', + authorName: 'Jonas', + authorEmail: 'jonas@example.com', + date: thirtyFiveDaysAgo.toISOString(), + }, + ]; + + // Act + render(); + + // Assert + expect( + document.querySelector('.recharts-responsive-container') + ).toBeInTheDocument(); + }); + + test('ActivityChartTooltip should render date and commit count when active', () => { + // Arrange + const payload = [{ value: 3, payload: { date: 'Apr 7' } }] as Parameters< + typeof ActivityChartTooltip + >[0]['payload']; + + // Act + render(); + + // Assert + expect(screen.getByText('Apr 7')).toBeInTheDocument(); + expect(screen.getByText('3 commits')).toBeInTheDocument(); + }); + + test('ActivityChartTooltip should render nothing when inactive', () => { + // Arrange / Act + const { container } = render( + + ); + + // Assert + expect(container.firstChild).toBeNull(); + }); +}); diff --git a/apps/frontend/__tests__/components/CodeChurnChart.test.tsx b/apps/frontend/__tests__/components/CodeChurnChart.test.tsx new file mode 100644 index 00000000..2b25cc03 --- /dev/null +++ b/apps/frontend/__tests__/components/CodeChurnChart.test.tsx @@ -0,0 +1,57 @@ +import { render, screen } from '@testing-library/react'; +import { describe, test, expect } from 'vitest'; +import { CodeChurnChart } from '../../src/components/CodeChurnChart'; +import type { CodeChurnAnalysis } from '@gitray/shared-types'; + +const mockChurnData: CodeChurnAnalysis = { + files: [ + { path: 'src/services/api.ts', changes: 45, risk: 'high' }, + { path: 'src/components/Dashboard.tsx', changes: 22, risk: 'medium' }, + { path: 'src/utils/dateUtils.ts', changes: 8, risk: 'low' }, + ], + metadata: { + totalFiles: 10, + totalChanges: 75, + highRiskCount: 1, + mediumRiskCount: 2, + lowRiskCount: 7, + riskThresholds: { high: 30, medium: 15, low: 14 }, + dateRange: { from: '2025-01-01', to: '2026-01-01' }, + analyzedAt: '2026-01-01T00:00:00Z', + }, +}; + +describe('CodeChurnChart Component', () => { + test('should render "No churn data available" when churnData is undefined', () => { + // Arrange & Act + render(); + + // Assert + expect(screen.getByText('No churn data available')).toBeInTheDocument(); + }); + + test('should render risk stats from churnData metadata', () => { + // Arrange & Act + render(); + + // Assert — stat cards display counts from metadata + expect(screen.getByText('High Risk Files')).toBeInTheDocument(); + expect(screen.getByText('Medium Risk Files')).toBeInTheDocument(); + expect(screen.getByText('Total Analyzed')).toBeInTheDocument(); + // highRiskCount=1, mediumRiskCount=2, totalFiles=10 + expect(screen.getByText('1')).toBeInTheDocument(); + expect(screen.getByText('2')).toBeInTheDocument(); + expect(screen.getByText('10')).toBeInTheDocument(); + }); + + test('should render top file in the Bug Hotspot Analysis section', () => { + // Arrange & Act + render(); + + // Assert — the top file (api.ts extracted from path) appears in hotspot list + // The filename is truncated from the path + expect(screen.getByText('Bug Hotspot Analysis')).toBeInTheDocument(); + // api.ts is 6 chars, under 25 char limit, so renders as-is + expect(screen.getAllByText('api.ts').length).toBeGreaterThan(0); + }); +}); diff --git a/apps/frontend/__tests__/components/FileDistributionChart.test.tsx b/apps/frontend/__tests__/components/FileDistributionChart.test.tsx new file mode 100644 index 00000000..6e0cb330 --- /dev/null +++ b/apps/frontend/__tests__/components/FileDistributionChart.test.tsx @@ -0,0 +1,55 @@ +import { render, screen } from '@testing-library/react'; +import { describe, test, expect } from 'vitest'; +import { FileDistributionChart } from '../../src/components/FileDistributionChart'; +import type { FileTypeDistribution } from '@gitray/shared-types'; + +const mockFileDistribution: FileTypeDistribution = { + extensions: { + '.ts': { count: 50, percentage: 60, size: 500000, averageSize: 10000 }, + '.tsx': { count: 20, percentage: 25, size: 200000, averageSize: 10000 }, + '.css': { count: 10, percentage: 12, size: 50000, averageSize: 5000 }, + '.md': { count: 2, percentage: 2, size: 5000, averageSize: 2500 }, + }, + categories: { + code: { count: 70, percentage: 85, size: 700000, averageSize: 10000 }, + documentation: { count: 2, percentage: 2, size: 5000, averageSize: 2500 }, + configuration: { + count: 10, + percentage: 12, + size: 50000, + averageSize: 5000, + }, + assets: { count: 0, percentage: 0, size: 0, averageSize: 0 }, + other: { count: 0, percentage: 0, size: 0, averageSize: 0 }, + }, + directories: [], + metadata: { + totalFiles: 82, + totalSize: 755000, + analyzedAt: '2026-01-01T00:00:00Z', + repositorySize: 'medium', + }, +}; + +describe('FileDistributionChart Component', () => { + test('should render "No file data available" when fileDistribution is undefined', () => { + // Arrange & Act + render(); + + // Assert + expect(screen.getByText('No file data available')).toBeInTheDocument(); + expect( + screen.getByText('File distribution could not be loaded') + ).toBeInTheDocument(); + }); + + test('should render the recharts container when fileDistribution data is provided', () => { + // Arrange & Act + render(); + + // Assert — Recharts renders its container div + expect( + document.querySelector('.recharts-responsive-container') + ).toBeInTheDocument(); + }); +}); diff --git a/apps/frontend/__tests__/components/Footer.test.tsx b/apps/frontend/__tests__/components/Footer.test.tsx new file mode 100644 index 00000000..ef013257 --- /dev/null +++ b/apps/frontend/__tests__/components/Footer.test.tsx @@ -0,0 +1,40 @@ +import { render, screen } from '@testing-library/react'; +import { describe, test, expect } from 'vitest'; +import { Footer } from '../../src/components/Footer'; + +describe('Footer Component', () => { + test('should render the GitHub repository link', () => { + // Arrange & Act + render(