diff --git a/src/main/gitlab/gl-utils.test.ts b/src/main/gitlab/gl-utils.test.ts index 8697567f442..d48c28d2540 100644 --- a/src/main/gitlab/gl-utils.test.ts +++ b/src/main/gitlab/gl-utils.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable max-lines -- Why: GitLab remote parsing coverage needs many URL/host fixtures against the same mocked git/glab helpers. */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const { gitExecFileAsyncMock, glabExecFileAsyncMock, sshExecMock } = vi.hoisted(() => ({ @@ -22,96 +21,12 @@ import { getGlabKnownHosts, getProjectRef, getProjectRefForRemote, - parseGitLabProjectRef, parseGlabApiResponse, parseGlabAuthStatusHosts, resolveIssueSource } from './gl-utils' import { registerSshGitProvider, unregisterSshGitProvider } from '../providers/ssh-git-dispatch' -describe('gitlab project ref parsing', () => { - it('parses HTTPS and SSH GitLab.com remotes', () => { - expect(parseGitLabProjectRef('https://gitlab.com/acme/widgets.git')).toEqual({ - host: 'gitlab.com', - path: 'acme/widgets' - }) - expect(parseGitLabProjectRef('git@gitlab.com:stablyai/orca.git')).toEqual({ - host: 'gitlab.com', - path: 'stablyai/orca' - }) - }) - - it('preserves nested group paths', () => { - expect(parseGitLabProjectRef('git@gitlab.com:group/subgroup/project.git')).toEqual({ - host: 'gitlab.com', - path: 'group/subgroup/project' - }) - expect(parseGitLabProjectRef('https://gitlab.com/g1/g2/g3/proj.git')).toEqual({ - host: 'gitlab.com', - path: 'g1/g2/g3/proj' - }) - }) - - it('returns null for non-GitLab hosts when host not in knownHosts', () => { - expect(parseGitLabProjectRef('git@github.com:stablyai/orca.git')).toBeNull() - expect(parseGitLabProjectRef('git@example.com:foo/bar.git')).toBeNull() - }) - - it('matches self-hosted hosts when included in knownHosts', () => { - expect( - parseGitLabProjectRef('git@gitlab.example.com:team/api.git', [ - 'gitlab.com', - 'gitlab.example.com' - ]) - ).toEqual({ host: 'gitlab.example.com', path: 'team/api' }) - }) - - it('parses GitLab remotes with non-standard ports without treating the port as a path segment', () => { - expect( - parseGitLabProjectRef('ssh://git@gitlab.example.com:2222/team/api.git', [ - 'gitlab.com', - 'gitlab.example.com' - ]) - ).toEqual({ host: 'gitlab.example.com', path: 'team/api' }) - expect( - parseGitLabProjectRef('https://gitlab.example.com:8443/team/api.git', [ - 'gitlab.com', - 'gitlab.example.com' - ]) - ).toEqual({ host: 'gitlab.example.com', path: 'team/api' }) - }) - - it('rejects single-segment paths (host root or user-only)', () => { - expect(parseGitLabProjectRef('git@gitlab.com:foo.git')).toBeNull() - expect(parseGitLabProjectRef('https://gitlab.com/foo.git')).toBeNull() - }) - - it('handles missing .git suffix', () => { - expect(parseGitLabProjectRef('https://gitlab.com/acme/widgets')).toEqual({ - host: 'gitlab.com', - path: 'acme/widgets' - }) - }) - - it('strips trailing slashes after .git suffixes', () => { - expect(parseGitLabProjectRef('https://gitlab.com/acme/widgets.git/')).toEqual({ - host: 'gitlab.com', - path: 'acme/widgets' - }) - expect(parseGitLabProjectRef('ssh://git@gitlab.com/acme/widgets.git/')).toEqual({ - host: 'gitlab.com', - path: 'acme/widgets' - }) - }) - - it('preserves git protocol remote support', () => { - expect(parseGitLabProjectRef('git://gitlab.com/acme/widgets.git')).toEqual({ - host: 'gitlab.com', - path: 'acme/widgets' - }) - }) -}) - describe('gitlab project ref resolution', () => { beforeEach(() => { gitExecFileAsyncMock.mockReset() diff --git a/src/main/gitlab/project-ref-parser.test.ts b/src/main/gitlab/project-ref-parser.test.ts new file mode 100644 index 00000000000..53adc0c5aa5 --- /dev/null +++ b/src/main/gitlab/project-ref-parser.test.ts @@ -0,0 +1,99 @@ +import { describe, expect, it } from 'vitest' +import { parseGitLabProjectRef, parseRemoteProjectRefCandidate } from './project-ref-parser' + +describe('gitlab project ref parsing', () => { + it('parses HTTPS and SSH GitLab.com remotes', () => { + expect(parseGitLabProjectRef('https://gitlab.com/acme/widgets.git')).toEqual({ + host: 'gitlab.com', + path: 'acme/widgets' + }) + expect(parseGitLabProjectRef('git@gitlab.com:stablyai/orca.git')).toEqual({ + host: 'gitlab.com', + path: 'stablyai/orca' + }) + }) + + it('preserves nested group paths', () => { + expect(parseGitLabProjectRef('git@gitlab.com:group/subgroup/project.git')).toEqual({ + host: 'gitlab.com', + path: 'group/subgroup/project' + }) + expect(parseGitLabProjectRef('https://gitlab.com/g1/g2/g3/proj.git')).toEqual({ + host: 'gitlab.com', + path: 'g1/g2/g3/proj' + }) + }) + + it('returns null for non-GitLab hosts when host not in knownHosts', () => { + expect(parseGitLabProjectRef('git@github.com:stablyai/orca.git')).toBeNull() + expect(parseGitLabProjectRef('git@example.com:foo/bar.git')).toBeNull() + }) + + it('matches self-hosted hosts when included in knownHosts', () => { + expect( + parseGitLabProjectRef('git@gitlab.example.com:team/api.git', [ + 'gitlab.com', + 'gitlab.example.com' + ]) + ).toEqual({ host: 'gitlab.example.com', path: 'team/api' }) + }) + + it('parses GitLab remotes with non-standard ports without treating the port as a path segment', () => { + expect( + parseGitLabProjectRef('ssh://git@gitlab.example.com:2222/team/api.git', [ + 'gitlab.com', + 'gitlab.example.com' + ]) + ).toEqual({ host: 'gitlab.example.com', path: 'team/api' }) + expect( + parseGitLabProjectRef('https://gitlab.example.com:8443/team/api.git', [ + 'gitlab.com', + 'gitlab.example.com' + ]) + ).toEqual({ host: 'gitlab.example.com', path: 'team/api' }) + }) + + it('rejects single-segment paths (host root or user-only)', () => { + expect(parseGitLabProjectRef('git@gitlab.com:foo.git')).toBeNull() + expect(parseGitLabProjectRef('https://gitlab.com/foo.git')).toBeNull() + }) + + it('handles missing .git suffix', () => { + expect(parseGitLabProjectRef('https://gitlab.com/acme/widgets')).toEqual({ + host: 'gitlab.com', + path: 'acme/widgets' + }) + }) + + it('strips trailing slashes after .git suffixes', () => { + expect(parseGitLabProjectRef('https://gitlab.com/acme/widgets.git/')).toEqual({ + host: 'gitlab.com', + path: 'acme/widgets' + }) + expect(parseGitLabProjectRef('ssh://git@gitlab.com/acme/widgets.git/')).toEqual({ + host: 'gitlab.com', + path: 'acme/widgets' + }) + }) + + it('preserves git protocol remote support', () => { + expect(parseGitLabProjectRef('git://gitlab.com/acme/widgets.git')).toEqual({ + host: 'gitlab.com', + path: 'acme/widgets' + }) + }) +}) + +describe('gitlab remote project ref candidates', () => { + it('extracts self-hosted candidates before the host is trusted', () => { + expect(parseRemoteProjectRefCandidate('git@gitlab.internal:team/orca.git')).toEqual({ + host: 'gitlab.internal', + path: 'team/orca' + }) + }) + + it('rejects non-git URLs and single-segment project paths', () => { + expect(parseRemoteProjectRefCandidate('file:///tmp/repo')).toBeNull() + expect(parseRemoteProjectRefCandidate('git@gitlab.internal:team.git')).toBeNull() + }) +}) diff --git a/src/renderer/src/components/stats/UsageSessionsTable.tsx b/src/renderer/src/components/stats/UsageSessionsTable.tsx index f84e49f46dc..242350c67b9 100644 --- a/src/renderer/src/components/stats/UsageSessionsTable.tsx +++ b/src/renderer/src/components/stats/UsageSessionsTable.tsx @@ -69,7 +69,7 @@ export function UsageSessionsTable({ {translate('auto.components.stats.UsageSessionsTable.c17bed0416', 'Project')} - {translate('auto.components.stats.UsageSessionsTable.1afc25eb06', 'Model')} + {translate('auto.components.stats.UsageSessionsTable.f6a2c8d019', 'Model')} {eventsLabel} diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 5df73366196..090fd8b3712 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -2991,6 +2991,22 @@ "d9a4b3e2f1c5": "events" } } + }, + "UsageBreakdownSection": { + "7765a4c3e1": "n/a", + "247c93ca92": "• inferred pricing" + }, + "UsageSessionsTable": { + "1afc25eb06": "Turns", + "0f03975d59": "Events", + "21ea00bfa8": "Cache", + "e0b988599d": "Total", + "01476891c7": "Last active", + "c17bed0416": "Project", + "f6a2c8d019": "Model", + "faf3444859": "Input", + "a8b7487ff7": "Output", + "cfe2282ffa": "Unknown" } }, "sparse": { diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 123efec8a56..85181b824ac 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -2991,6 +2991,22 @@ "d9a4b3e2f1c5": "eventos" } } + }, + "UsageBreakdownSection": { + "7765a4c3e1": "n / A", + "247c93ca92": "• precios inferidos" + }, + "UsageSessionsTable": { + "1afc25eb06": "vueltas", + "0f03975d59": "Eventos", + "21ea00bfa8": "Cache", + "e0b988599d": "Total", + "01476891c7": "Último activo", + "c17bed0416": "Proyecto", + "f6a2c8d019": "Modelo", + "faf3444859": "Aporte", + "a8b7487ff7": "Producción", + "cfe2282ffa": "Desconocido" } }, "sparse": { diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 61b7102f7b7..0eda87f35b2 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -2991,6 +2991,22 @@ "d9a4b3e2f1c5": "イベント" } } + }, + "UsageBreakdownSection": { + "7765a4c3e1": "該当なし", + "247c93ca92": "• 推定価格" + }, + "UsageSessionsTable": { + "1afc25eb06": "ターン", + "0f03975d59": "イベント", + "21ea00bfa8": "キャッシュ", + "e0b988599d": "合計", + "01476891c7": "最後にアクティブだった", + "c17bed0416": "プロジェクト", + "f6a2c8d019": "モデル", + "faf3444859": "入力", + "a8b7487ff7": "出力", + "cfe2282ffa": "未知" } }, "sparse": { diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 89f50e34bf1..44e4a0a2f77 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -2991,6 +2991,22 @@ "d9a4b3e2f1c5": "이벤트" } } + }, + "UsageBreakdownSection": { + "7765a4c3e1": "해당 없음", + "247c93ca92": "• 추론된 가격" + }, + "UsageSessionsTable": { + "1afc25eb06": "턴", + "0f03975d59": "이벤트", + "21ea00bfa8": "캐시", + "e0b988599d": "총", + "01476891c7": "마지막 활동", + "c17bed0416": "프로젝트", + "f6a2c8d019": "모델", + "faf3444859": "입력", + "a8b7487ff7": "출력", + "cfe2282ffa": "알려지지 않은" } }, "sparse": { diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 4d3b299d62e..fe4b81186e2 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -2991,6 +2991,22 @@ "d9a4b3e2f1c5": "事件" } } + }, + "UsageBreakdownSection": { + "7765a4c3e1": "不适用", + "247c93ca92": "• 推断定价" + }, + "UsageSessionsTable": { + "1afc25eb06": "轮次", + "0f03975d59": "事件", + "21ea00bfa8": "缓存", + "e0b988599d": "总计", + "01476891c7": "最后活跃", + "c17bed0416": "项目", + "f6a2c8d019": "模型", + "faf3444859": "输入", + "a8b7487ff7": "输出", + "cfe2282ffa": "未知" } }, "sparse": {