-
-
Notifications
You must be signed in to change notification settings - Fork 59
Testing #129
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Testing #129
Changes from all commits
22191e3
dde090e
90df3ec
4e4fbbc
cd5fe25
8506ae7
5fe91eb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,165 @@ | ||
| import { describe, it, expect } from 'vitest' | ||
| import { buildAnalyticalModel } from './analytics' | ||
|
|
||
| function makeRepo(name, overrides = {}) { | ||
| return { | ||
| name, | ||
| pushed_at: new Date().toISOString(), | ||
| open_issues_count: 0, | ||
| stargazers_count: 0, | ||
| forks_count: 0, | ||
| ...overrides, | ||
| } | ||
| } | ||
|
|
||
| describe('buildAnalyticalModel', () => { | ||
| it('populates totalRepos from totalReposPerOrg, including repos with no contributor data', () => { | ||
| const orgs = [{ login: 'org-a' }] | ||
| const repoA = makeRepo('repo-a') | ||
| const repoB = makeRepo('repo-b') // exists only in the "total" fetch, not scoped | ||
|
|
||
| const reposPerOrg = { 'org-a': [repoA] } // scoped (e.g. top-10 without PAT) | ||
| const totalReposPerOrg = { 'org-a': [repoA, repoB] } // full fetch | ||
| const contribsPerRepo = { | ||
| 'org-a/repo-a': [{ login: 'alice', contributions: 10 }], | ||
| // no entry for repo-b | ||
| } | ||
|
|
||
| const result = buildAnalyticalModel(orgs, reposPerOrg, contribsPerRepo, totalReposPerOrg) | ||
|
|
||
| expect(result.totalRepos).toHaveLength(2) | ||
| const repoBResult = result.totalRepos.find(r => r.name === 'repo-b') | ||
| expect(repoBResult.contributors).toEqual([]) | ||
| expect(repoBResult.busFactor).toEqual({ factor: 0, risk: 'unknown' }) | ||
| expect(repoBResult.orgLogin).toBe('org-a') | ||
| }) | ||
|
|
||
| it('populates allRepos only from the scoped reposPerOrg, not from totalReposPerOrg', () => { | ||
| const orgs = [{ login: 'org-a' }] | ||
| const repoA = makeRepo('repo-a') | ||
| const repoB = makeRepo('repo-b') | ||
|
|
||
| const reposPerOrg = { 'org-a': [repoA] } | ||
| const totalReposPerOrg = { 'org-a': [repoA, repoB] } | ||
| const contribsPerRepo = {} | ||
|
|
||
| const result = buildAnalyticalModel(orgs, reposPerOrg, contribsPerRepo, totalReposPerOrg) | ||
|
|
||
| expect(result.allRepos).toHaveLength(1) | ||
| expect(result.allRepos[0].name).toBe('repo-a') | ||
| }) | ||
|
|
||
| it('does NOT include a contributor whose only appearance is in a totalRepos-only repo', () => { | ||
| // repo-b has contributor data in contribsPerRepo, but repo-b is only in | ||
| // totalReposPerOrg, not reposPerOrg. The contributor map is built solely | ||
| // from the reposPerOrg loop, so this contributor must not appear. | ||
| const orgs = [{ login: 'org-a' }] | ||
| const repoA = makeRepo('repo-a') | ||
| const repoB = makeRepo('repo-b') | ||
|
|
||
| const reposPerOrg = { 'org-a': [repoA] } | ||
| const totalReposPerOrg = { 'org-a': [repoA, repoB] } | ||
| const contribsPerRepo = { | ||
| 'org-a/repo-b': [{ login: 'ghost-contributor', avatar_url: '', contributions: 5 }], | ||
| } | ||
|
|
||
| const result = buildAnalyticalModel(orgs, reposPerOrg, contribsPerRepo, totalReposPerOrg) | ||
|
|
||
| expect(result.contributors.find(c => c.login === 'ghost-contributor')).toBeUndefined() | ||
| }) | ||
|
|
||
| it('deduplicates a contributor appearing across multiple scoped repos, summing contributions', () => { | ||
| const orgs = [{ login: 'org-a' }] | ||
| const repoA = makeRepo('repo-a') | ||
| const repoB = makeRepo('repo-b') | ||
|
|
||
| const reposPerOrg = { 'org-a': [repoA, repoB] } | ||
| const totalReposPerOrg = { 'org-a': [repoA, repoB] } | ||
| const contribsPerRepo = { | ||
| 'org-a/repo-a': [{ login: 'alice', avatar_url: '', contributions: 10 }], | ||
| 'org-a/repo-b': [{ login: 'alice', avatar_url: '', contributions: 15 }], | ||
| } | ||
|
|
||
| const result = buildAnalyticalModel(orgs, reposPerOrg, contribsPerRepo, totalReposPerOrg) | ||
|
|
||
| expect(result.contributors).toHaveLength(1) | ||
| expect(result.contributors[0].totalContribs).toBe(25) | ||
| expect(result.contributors[0].repos).toHaveLength(2) | ||
| }) | ||
|
|
||
| it('marks a contributor as isConnector once they appear in 3+ scoped repos', () => { | ||
| const orgs = [{ login: 'org-a' }] | ||
| const repos = [makeRepo('r1'), makeRepo('r2'), makeRepo('r3')] | ||
|
|
||
| const reposPerOrg = { 'org-a': repos } | ||
| const totalReposPerOrg = { 'org-a': repos } | ||
| const contribsPerRepo = Object.fromEntries( | ||
| repos.map(r => [`org-a/${r.name}`, [{ login: 'carol', avatar_url: '', contributions: 1 }]]) | ||
| ) | ||
|
|
||
| const result = buildAnalyticalModel(orgs, reposPerOrg, contribsPerRepo, totalReposPerOrg) | ||
|
|
||
| expect(result.contributors[0].isConnector).toBe(true) | ||
| }) | ||
|
|
||
| it('does not mark a contributor as isConnector with fewer than 3 scoped repos', () => { | ||
| const orgs = [{ login: 'org-a' }] | ||
| const repos = [makeRepo('r1'), makeRepo('r2')] | ||
|
|
||
| const reposPerOrg = { 'org-a': repos } | ||
| const totalReposPerOrg = { 'org-a': repos } | ||
| const contribsPerRepo = Object.fromEntries( | ||
| repos.map(r => [`org-a/${r.name}`, [{ login: 'dave', avatar_url: '', contributions: 1 }]]) | ||
| ) | ||
|
|
||
| const result = buildAnalyticalModel(orgs, reposPerOrg, contribsPerRepo, totalReposPerOrg) | ||
|
|
||
| expect(result.contributors[0].isConnector).toBe(false) | ||
| }) | ||
|
|
||
| it('marks a contributor as isCrossOrg when they appear in 2+ orgs', () => { | ||
| const orgs = [{ login: 'org-a' }, { login: 'org-b' }] | ||
| const repoA = makeRepo('repo-a') | ||
| const repoB = makeRepo('repo-b') | ||
|
|
||
| const reposPerOrg = { 'org-a': [repoA], 'org-b': [repoB] } | ||
| const totalReposPerOrg = { 'org-a': [repoA], 'org-b': [repoB] } | ||
| const contribsPerRepo = { | ||
| 'org-a/repo-a': [{ login: 'erin', avatar_url: '', contributions: 5 }], | ||
| 'org-b/repo-b': [{ login: 'erin', avatar_url: '', contributions: 5 }], | ||
| } | ||
|
|
||
| const result = buildAnalyticalModel(orgs, reposPerOrg, contribsPerRepo, totalReposPerOrg) | ||
|
|
||
| expect(result.contributors[0].isCrossOrg).toBe(true) | ||
| expect(result.contributors[0].orgs.sort()).toEqual(['org-a', 'org-b']) | ||
| }) | ||
|
|
||
| it('sorts contributors by totalContribs descending', () => { | ||
| const orgs = [{ login: 'org-a' }] | ||
| const repoA = makeRepo('repo-a') | ||
|
|
||
| const reposPerOrg = { 'org-a': [repoA] } | ||
| const totalReposPerOrg = { 'org-a': [repoA] } | ||
| const contribsPerRepo = { | ||
| 'org-a/repo-a': [ | ||
| { login: 'low', avatar_url: '', contributions: 5 }, | ||
| { login: 'high', avatar_url: '', contributions: 50 }, | ||
| { login: 'mid', avatar_url: '', contributions: 20 }, | ||
| ], | ||
| } | ||
|
|
||
| const result = buildAnalyticalModel(orgs, reposPerOrg, contribsPerRepo, totalReposPerOrg) | ||
|
|
||
| expect(result.contributors.map(c => c.login)).toEqual(['high', 'mid', 'low']) | ||
| }) | ||
|
|
||
| it('handles an org with zero repos without throwing', () => { | ||
| const orgs = [{ login: 'empty-org' }] | ||
| const result = buildAnalyticalModel(orgs, { 'empty-org': [] }, {}, { 'empty-org': [] }) | ||
|
|
||
| expect(result.allRepos).toEqual([]) | ||
| expect(result.totalRepos).toEqual([]) | ||
| expect(result.contributors).toEqual([]) | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,120 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { describe, it, expect } from 'vitest' | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { buildTimeSeries } from './analytics' | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| describe('buildTimeSeries', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| it('buckets a plain issue by its created_at month', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const issues = [{ created_at: '2026-03-15T00:00:00Z' }] | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = buildTimeSeries(issues, 'monthly') | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(result).toHaveLength(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(result[0].date).toBe('2026-03') | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(result[0].issues_created).toBe(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(result[0].prs_created).toBe(0) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| it('counts a pull_request item as a PR, not an issue', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const issues = [{ created_at: '2026-03-15T00:00:00Z', pull_request: {} }] | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = buildTimeSeries(issues, 'monthly') | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(result[0].prs_created).toBe(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(result[0].issues_created).toBe(0) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| it('buckets closed_at into the correct month, separate from created_at', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const issues = [{ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| created_at: '2026-01-01T00:00:00Z', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| closed_at: '2026-02-01T00:00:00Z', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }] | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = buildTimeSeries(issues, 'monthly') | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const jan = result.find(r => r.date === '2026-01') | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const feb = result.find(r => r.date === '2026-02') | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(jan.issues_created).toBe(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(jan.issues_closed).toBe(0) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(feb.issues_closed).toBe(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(feb.issues_created).toBe(0) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| it('buckets a merged PR into prs_merged using pull_request.merged_at, not closed_at', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const issues = [{ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| created_at: '2026-01-01T00:00:00Z', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| closed_at: '2026-01-05T00:00:00Z', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| pull_request: { merged_at: '2026-01-05T00:00:00Z' }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }] | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = buildTimeSeries(issues, 'monthly') | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const jan = result.find(r => r.date === '2026-01') | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(jan.prs_created).toBe(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(jan.prs_closed).toBe(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(jan.prs_merged).toBe(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| it('does not count prs_merged for a closed-but-not-merged PR', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const issues = [{ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| created_at: '2026-01-01T00:00:00Z', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| closed_at: '2026-01-05T00:00:00Z', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| pull_request: { merged_at: null }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }] | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = buildTimeSeries(issues, 'monthly') | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const jan = result.find(r => r.date === '2026-01') | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(jan.prs_closed).toBe(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(jan.prs_merged).toBe(0) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| it('produces one bucket per week when granularity is weekly, distinct from monthly', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const issues = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { created_at: '2026-01-01T00:00:00Z' }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { created_at: '2026-01-08T00:00:00Z' }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const weekly = buildTimeSeries(issues, 'weekly') | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Two different weeks -> two buckets, and week-format labels | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(weekly.length).toBeGreaterThanOrEqual(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| weekly.forEach(bucket => expect(bucket.date).toMatch(/^\d{4}-W\d{2}$/)) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+71
to
+82
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win Weak assertion doesn't match the test's stated intent. The comment says two distinct weeks should produce two buckets, but the assertion only checks ✅ Suggested fix const weekly = buildTimeSeries(issues, 'weekly')
- // Two different weeks -> two buckets, and week-format labels
- expect(weekly.length).toBeGreaterThanOrEqual(1)
+ // Two different weeks -> two buckets, and week-format labels
+ expect(weekly).toHaveLength(2)
weekly.forEach(bucket => expect(bucket.date).toMatch(/^\d{4}-W\d{2}$/))As per path instructions, test files should avoid tautological assertions and have descriptions that clarify the test's purpose — here the assertion should actually validate what the description promises. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Path instructions |
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| it('sorts buckets chronologically', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const issues = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { created_at: '2026-03-01T00:00:00Z' }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { created_at: '2026-01-01T00:00:00Z' }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { created_at: '2026-02-01T00:00:00Z' }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = buildTimeSeries(issues, 'monthly') | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(result.map(r => r.date)).toEqual(['2026-01', '2026-02', '2026-03']) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| it('returns at most the last 12 buckets when more months are present', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const issues = Array.from({ length: 15 }, (_, i) => ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| created_at: `2025-${String((i % 12) + 1).padStart(2, '0')}-01T00:00:00Z`, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| })) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // Construct 15 distinct months across two years to exceed the 12-bucket cap | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const spreadIssues = Array.from({ length: 15 }, (_, i) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const year = 2025 + Math.floor(i / 12) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const month = (i % 12) + 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return { created_at: `${year}-${String(month).padStart(2, '0')}-01T00:00:00Z` } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = buildTimeSeries(spreadIssues, 'monthly') | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(result.length).toBeLessThanOrEqual(12) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| it('returns an empty array when given no issues', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(buildTimeSeries([], 'monthly')).toEqual([]) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| it('ignores an item with no created_at and no closed_at', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = buildTimeSeries([{ created_at: null }], 'monthly') | ||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(result).toEqual([]) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
Does vitest 4.1.10 require vite 6 or higher as a peer dependency?💡 Result:
No, Vitest 4.1.10 does not require Vite 6 (or higher) specifically as a peer dependency. Instead, Vite is listed as a standard dependency in the Vitest package, meaning it is required as a core component for Vitest to function [1][2][3]. While Vitest 4.0 and later versions require Vite >= 6.0.0 to operate [4][5], this requirement is enforced through the package's dependencies rather than as a peer dependency [2][3]. Some internal sub-packages of Vitest (such as
@vitest/mocker) do utilize peer dependencies to manage their relationship with Vite [6][7], but the main vitest package itself maintains Vite as a direct dependency [1]. In summary, you must have a compatible version of Vite (Vite 6 or higher for Vitest 4.x) installed in your project for Vitest to work, but it is not technically a peer dependency of the main vitest package [1][2][4].Citations:
🏁 Script executed:
Repository: AOSSIE-Org/OrgExplorer
Length of output: 245
🏁 Script executed:
Repository: AOSSIE-Org/OrgExplorer
Length of output: 3531
Align Vite with Vitest’s supported range.
vitest@4.1.10declares Vite^6.0.0 || ^7.0.0 || ^8.0.0, sovite@^5.4.6leaves the test suite with incompatible Vite versions / unresolved Vitest runtime dependencies.🔧 Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents