@foundation0/git 1.0.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/{packages/git/README.md → README.md} +14 -5
  2. package/{packages/git/mcp → mcp}/README.md +3 -1
  3. package/{packages/git/mcp → mcp}/cli.mjs +0 -0
  4. package/package.json +43 -13
  5. package/{packages/git/src → src}/git-service-api.ts +3 -0
  6. package/{packages/git/src → src}/index.ts +8 -0
  7. package/src/label-management.ts +587 -0
  8. package/src/platform/config.ts +60 -0
  9. package/.codex.example/config.toml +0 -10
  10. package/.env.example +0 -10
  11. package/packages/fs/README.md +0 -47
  12. package/packages/fs/node_modules/.bin/f0-git-mcp +0 -21
  13. package/packages/fs/node_modules/.bin/f0-git-mcp-server +0 -21
  14. package/packages/fs/node_modules/.bin/f0-git-mcp-server.CMD +0 -12
  15. package/packages/fs/node_modules/.bin/f0-git-mcp-server.ps1 +0 -41
  16. package/packages/fs/node_modules/.bin/f0-git-mcp.CMD +0 -12
  17. package/packages/fs/node_modules/.bin/f0-git-mcp.ps1 +0 -41
  18. package/packages/fs/node_modules/.bin/tsc +0 -21
  19. package/packages/fs/node_modules/.bin/tsc.CMD +0 -12
  20. package/packages/fs/node_modules/.bin/tsc.ps1 +0 -41
  21. package/packages/fs/node_modules/.bin/tsserver +0 -21
  22. package/packages/fs/node_modules/.bin/tsserver.CMD +0 -12
  23. package/packages/fs/node_modules/.bin/tsserver.ps1 +0 -41
  24. package/packages/fs/node_modules/.bin/vite +0 -21
  25. package/packages/fs/node_modules/.bin/vite.CMD +0 -12
  26. package/packages/fs/node_modules/.bin/vite.ps1 +0 -41
  27. package/packages/fs/node_modules/.bin/vitest +0 -21
  28. package/packages/fs/node_modules/.bin/vitest.CMD +0 -12
  29. package/packages/fs/node_modules/.bin/vitest.ps1 +0 -41
  30. package/packages/fs/package.json +0 -28
  31. package/packages/fs/src/cli.ts +0 -74
  32. package/packages/fs/src/git-fs.ts +0 -705
  33. package/packages/fs/src/index.ts +0 -33
  34. package/packages/fs/src/mount.ts +0 -297
  35. package/packages/fs/tsconfig.json +0 -7
  36. package/packages/git/mcp/tests/e2e/git-mcp-e2e.spec.ts +0 -157
  37. package/packages/git/mcp/tests/e2e/server.fixture.ts +0 -109
  38. package/packages/git/node_modules/.bin/tsc +0 -21
  39. package/packages/git/node_modules/.bin/tsc.CMD +0 -12
  40. package/packages/git/node_modules/.bin/tsc.ps1 +0 -41
  41. package/packages/git/node_modules/.bin/tsserver +0 -21
  42. package/packages/git/node_modules/.bin/tsserver.CMD +0 -12
  43. package/packages/git/node_modules/.bin/tsserver.ps1 +0 -41
  44. package/packages/git/node_modules/.bin/vite +0 -21
  45. package/packages/git/node_modules/.bin/vite.CMD +0 -12
  46. package/packages/git/node_modules/.bin/vite.ps1 +0 -41
  47. package/packages/git/node_modules/.bin/vitest +0 -21
  48. package/packages/git/node_modules/.bin/vitest.CMD +0 -12
  49. package/packages/git/node_modules/.bin/vitest.ps1 +0 -41
  50. package/packages/git/node_modules/.vite/vitest/results.json +0 -1
  51. package/packages/git/package.json +0 -60
  52. package/packages/git/scripts/create-issue.mjs +0 -93
  53. package/packages/git/scripts/extract-git-spec.mjs +0 -234
  54. package/packages/git/scripts/fetch-gitea-swagger.mjs +0 -22
  55. package/packages/git/src/platform/config.ts +0 -140
  56. package/packages/git/tests/api.spec.ts +0 -55
  57. package/packages/git/tests/e2e/git-service-feature-e2e.spec.ts +0 -232
  58. package/packages/git/tests/git-service-api-object.spec.ts +0 -97
  59. package/packages/git/tests/git-service-feature-matrix.spec.ts +0 -182
  60. package/packages/git/tests/issue-dependencies.spec.ts +0 -81
  61. package/packages/git/tsconfig.json +0 -7
  62. package/packages/git/vitest.config.ts +0 -7
  63. package/packages/utils/package.json +0 -9
  64. package/packages/utils/src/awk.ts +0 -6
  65. package/packages/utils/src/cat.ts +0 -6
  66. package/packages/utils/src/cd.ts +0 -6
  67. package/packages/utils/src/chgrp.ts +0 -6
  68. package/packages/utils/src/chmod.ts +0 -6
  69. package/packages/utils/src/chown.ts +0 -6
  70. package/packages/utils/src/cp.ts +0 -6
  71. package/packages/utils/src/curl.ts +0 -6
  72. package/packages/utils/src/cut.ts +0 -6
  73. package/packages/utils/src/date.ts +0 -6
  74. package/packages/utils/src/echo.ts +0 -6
  75. package/packages/utils/src/find.ts +0 -6
  76. package/packages/utils/src/grep.ts +0 -6
  77. package/packages/utils/src/gunzip.ts +0 -6
  78. package/packages/utils/src/gzip.ts +0 -6
  79. package/packages/utils/src/head.ts +0 -6
  80. package/packages/utils/src/hostname.ts +0 -6
  81. package/packages/utils/src/index.ts +0 -37
  82. package/packages/utils/src/ls.ts +0 -6
  83. package/packages/utils/src/mkdir.ts +0 -6
  84. package/packages/utils/src/mv.ts +0 -6
  85. package/packages/utils/src/ping.ts +0 -6
  86. package/packages/utils/src/pwd.ts +0 -6
  87. package/packages/utils/src/rm.ts +0 -6
  88. package/packages/utils/src/rmdir.ts +0 -6
  89. package/packages/utils/src/sed.ts +0 -6
  90. package/packages/utils/src/sort.ts +0 -6
  91. package/packages/utils/src/tail.ts +0 -6
  92. package/packages/utils/src/tar.ts +0 -6
  93. package/packages/utils/src/touch.ts +0 -6
  94. package/packages/utils/src/tr.ts +0 -6
  95. package/packages/utils/src/uname.ts +0 -6
  96. package/packages/utils/src/uniq.ts +0 -6
  97. package/packages/utils/src/unzip.ts +0 -6
  98. package/packages/utils/src/util.ts +0 -4
  99. package/packages/utils/src/wc.ts +0 -6
  100. package/packages/utils/src/wget.ts +0 -6
  101. package/packages/utils/src/whoami.ts +0 -6
  102. package/packages/utils/src/zip.ts +0 -6
  103. package/pnpm-workspace.yaml +0 -2
  104. package/tsconfig.base.json +0 -12
  105. /package/{packages/git/gitea-swagger.json → gitea-swagger.json} +0 -0
  106. /package/{packages/git/mcp → mcp}/src/cli.ts +0 -0
  107. /package/{packages/git/mcp → mcp}/src/client.ts +0 -0
  108. /package/{packages/git/mcp → mcp}/src/index.ts +0 -0
  109. /package/{packages/git/mcp → mcp}/src/server.ts +0 -0
  110. /package/{packages/git/src → src}/api.ts +0 -0
  111. /package/{packages/git/src → src}/git-service-feature-spec.generated.ts +0 -0
  112. /package/{packages/git/src → src}/issue-dependencies.ts +0 -0
  113. /package/{packages/git/src → src}/platform/gitea-adapter.ts +0 -0
  114. /package/{packages/git/src → src}/platform/gitea-rules.ts +0 -0
  115. /package/{packages/git/src → src}/platform/index.ts +0 -0
  116. /package/{packages/git/src → src}/repository.ts +0 -0
  117. /package/{packages/git/src → src}/spec-mock.ts +0 -0
@@ -1,140 +0,0 @@
1
- import { existsSync, readFileSync } from 'node:fs'
2
- import { dirname, join } from 'node:path'
3
-
4
- export type PlatformName = 'GITEA' | 'GITHUB'
5
-
6
- export interface GitPlatformConfig {
7
- platform: PlatformName
8
- giteaHost: string
9
- giteaToken?: string
10
- giteaApiVersion?: string
11
- giteaSwaggerPath?: string
12
- }
13
-
14
- interface DotEnv {
15
- [key: string]: string
16
- }
17
-
18
- const DEFAULT_GITEA_HOST = 'https://gitea.example.com'
19
- const DEFAULT_PLATFORM: PlatformName = 'GITEA'
20
- const DEFAULT_GITEA_SWAGGER_PATH = '/swagger.v1.json'
21
-
22
- const parseLine = (line: string): [string, string] | null => {
23
- const trimmed = line.trim()
24
- if (!trimmed || trimmed.startsWith('#')) {
25
- return null
26
- }
27
-
28
- const index = trimmed.indexOf('=')
29
- if (index < 0) {
30
- return null
31
- }
32
-
33
- const key = trimmed.slice(0, index).trim()
34
- const rawValue = trimmed.slice(index + 1).trim()
35
- if (!key) {
36
- return null
37
- }
38
-
39
- if (rawValue.length >= 2 && rawValue.startsWith('"') && rawValue.endsWith('"')) {
40
- return [key, rawValue.slice(1, -1)]
41
- }
42
-
43
- if (rawValue.length >= 2 && rawValue.startsWith("'") && rawValue.endsWith("'")) {
44
- return [key, rawValue.slice(1, -1)]
45
- }
46
-
47
- return [key, rawValue]
48
- }
49
-
50
- const readDotEnv = (filePath: string): DotEnv => {
51
- if (!existsSync(filePath)) {
52
- return {}
53
- }
54
-
55
- const env: DotEnv = {}
56
-
57
- const contents = readFileSync(filePath, 'utf8')
58
- for (const line of contents.split(/\r?\n/)) {
59
- const parsed = parseLine(line)
60
- if (!parsed) {
61
- continue
62
- }
63
-
64
- const [key, value] = parsed
65
- env[key] = value
66
- }
67
-
68
- return env
69
- }
70
-
71
- const searchDotEnv = (): DotEnv => {
72
- const candidates = [
73
- join(process.cwd(), '.env'),
74
- join(dirname(process.cwd()), '.env'),
75
- ]
76
-
77
- for (const candidate of candidates) {
78
- const values = readDotEnv(candidate)
79
- if (Object.keys(values).length > 0) {
80
- return values
81
- }
82
- }
83
-
84
- return {}
85
- }
86
-
87
- const normalizePlatform = (value?: string): PlatformName => {
88
- const platform = (value ?? '').toUpperCase()
89
- if (platform === 'GITEA') {
90
- return 'GITEA'
91
- }
92
-
93
- if (platform === 'GITHUB') {
94
- return 'GITHUB'
95
- }
96
-
97
- return DEFAULT_PLATFORM
98
- }
99
-
100
- export const getGitPlatformConfig = (overrides: Partial<GitPlatformConfig> = {}): GitPlatformConfig => {
101
- const env = searchDotEnv()
102
-
103
- const platform = normalizePlatform(
104
- overrides.platform ??
105
- process.env.PLATFORM ??
106
- process.env.GIT_PLATFORM ??
107
- env.PLATFORM ??
108
- env.GIT_PLATFORM,
109
- )
110
-
111
- const giteaHost =
112
- overrides.giteaHost ??
113
- process.env.GITEA_HOST ??
114
- env.GITEA_HOST ??
115
- DEFAULT_GITEA_HOST
116
-
117
- const giteaToken =
118
- overrides.giteaToken ??
119
- process.env.GITEA_TOKEN ??
120
- env.GITEA_TOKEN
121
-
122
- const giteaApiVersion =
123
- overrides.giteaApiVersion ??
124
- process.env.GITEA_API_VERSION ??
125
- env.GITEA_API_VERSION
126
-
127
- const giteaSwaggerPath =
128
- overrides.giteaSwaggerPath ??
129
- process.env.GITEA_SWAGGER_PATH ??
130
- env.GITEA_SWAGGER_PATH ??
131
- DEFAULT_GITEA_SWAGGER_PATH
132
-
133
- return {
134
- platform,
135
- giteaHost,
136
- giteaToken,
137
- giteaApiVersion,
138
- giteaSwaggerPath,
139
- }
140
- }
@@ -1,55 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
-
3
- import { createRemoteGitApiClient, GIT_API_VERSION } from '../src'
4
-
5
- describe('remote Git API client', () => {
6
- it('builds default response that matches the mock contract', () => {
7
- const client = createRemoteGitApiClient()
8
- const response = client.request({
9
- path: ['repos', 'owner', 'repo', 'issues', '123'],
10
- query: ['state=open'],
11
- headers: ['Accept: application/vnd.github+json'],
12
- })
13
-
14
- expect(response).toEqual({
15
- endpoint: 'https://api.github.com/repos/owner/repo/issues/123',
16
- method: 'GET',
17
- path: ['repos', 'owner', 'repo', 'issues', '123'],
18
- query: ['state=open'],
19
- headers: ['Accept: application/vnd.github+json'],
20
- requestParts: [
21
- 'GET',
22
- 'repos',
23
- 'owner',
24
- 'repo',
25
- 'issues',
26
- '123',
27
- 'state=open',
28
- 'Accept: application/vnd.github+json',
29
- ],
30
- apiVersion: GIT_API_VERSION,
31
- })
32
- })
33
-
34
- it('supports API base, version, and async request custom defaults', async () => {
35
- const client = createRemoteGitApiClient({
36
- apiBase: 'https://internal.example.com',
37
- apiVersion: '2022-11-28-preview',
38
- defaultMethod: 'POST',
39
- })
40
- const response = await client.requestAsync({
41
- method: 'PATCH',
42
- path: ['repos', 'owner', 'repo', 'issues', '999'],
43
- })
44
-
45
- expect(response).toEqual({
46
- endpoint: 'https://internal.example.com/repos/owner/repo/issues/999',
47
- method: 'PATCH',
48
- path: ['repos', 'owner', 'repo', 'issues', '999'],
49
- query: [],
50
- headers: [],
51
- requestParts: ['PATCH', 'repos', 'owner', 'repo', 'issues', '999'],
52
- apiVersion: '2022-11-28-preview',
53
- })
54
- })
55
- })
@@ -1,232 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
-
3
- import swaggerSpec from '../../gitea-swagger.json'
4
- import { getGitPlatformConfig } from '../../src/platform/config'
5
-
6
- interface SwaggerParameter {
7
- name?: string
8
- in?: string
9
- required?: boolean
10
- schema?: {
11
- type?: string
12
- enum?: unknown[]
13
- format?: string
14
- }
15
- }
16
-
17
- interface SwaggerOperation {
18
- parameters?: SwaggerParameter[]
19
- }
20
-
21
- interface SwaggerPathEntry {
22
- [method: string]: SwaggerOperation | undefined
23
- parameters?: SwaggerParameter[]
24
- }
25
-
26
- interface SwaggerOperationCase {
27
- pathTemplate: string
28
- method: string
29
- resolvedPath: string
30
- }
31
-
32
- const normalizePlatformHost = (host: string): string => {
33
- const trimmed = host.replace(/\/$/, '')
34
- if (trimmed.endsWith('/api/v1')) {
35
- return trimmed
36
- }
37
-
38
- return `${trimmed}/api/v1`
39
- }
40
-
41
- const platformConfig = getGitPlatformConfig({ platform: 'GITEA' })
42
- const host = normalizePlatformHost(platformConfig.giteaHost)
43
- const apiToken = platformConfig.giteaToken
44
- const testOwner = process.env.GITEA_TEST_OWNER ?? 'example-org'
45
- const testRepo = process.env.GITEA_TEST_REPO ?? 'example-repo'
46
-
47
- const defaultValueFor = (name: string, schema?: SwaggerParameter['schema']): string => {
48
- const normalized = name.toLowerCase()
49
-
50
- if (schema?.type === 'integer' || schema?.type === 'number') {
51
- return '1'
52
- }
53
-
54
- if (schema?.type === 'boolean') {
55
- return 'true'
56
- }
57
-
58
- if (schema?.enum && schema.enum.length > 0) {
59
- return String(schema.enum[0])
60
- }
61
-
62
- if (normalized === 'owner' || normalized === 'org' || normalized === 'organization' || normalized === 'user' || normalized === 'username') {
63
- return testOwner
64
- }
65
-
66
- if (normalized === 'repo' || normalized === 'repository') {
67
- return testRepo
68
- }
69
-
70
- if (normalized === 'archive' || normalized.includes('archive')) {
71
- return 'zip'
72
- }
73
-
74
- if (normalized === 'difftype' || normalized.includes('diff') || normalized.includes('diff-type')) {
75
- return 'patch'
76
- }
77
-
78
- if (normalized.includes('filepath') || normalized.includes('file-path') || normalized.includes('path') || normalized.includes('filename')) {
79
- return 'README.md'
80
- }
81
-
82
- if (normalized.includes('pagename') || normalized.includes('page-name')) {
83
- return 'readme'
84
- }
85
-
86
- if (normalized.includes('sha') || normalized.includes('branch') || normalized.includes('ref')) {
87
- return 'main'
88
- }
89
-
90
- if (normalized.includes('number') || /[id]/.test(normalized) || normalized.includes('id') || normalized.includes('index') || normalized.includes('tag_id')) {
91
- return '1'
92
- }
93
-
94
- if (normalized.includes('tag') || normalized.includes('release')) {
95
- return 'v1'
96
- }
97
-
98
- if (normalized.includes('name') || normalized.includes('title') || normalized.includes('secretname') || normalized.includes('username')) {
99
- return 'sample'
100
- }
101
-
102
- return '1'
103
- }
104
-
105
- const getPathDefaults = (pathTemplate: string, pathItem: SwaggerPathEntry, operation: SwaggerOperation): Record<string, string> => {
106
- const parameterDefs = new Map<string, SwaggerParameter>()
107
-
108
- for (const parameter of [...(pathItem.parameters ?? []), ...(operation.parameters ?? [])]) {
109
- if (!parameter?.name) {
110
- continue
111
- }
112
-
113
- if (parameter.in === 'path') {
114
- parameterDefs.set(parameter.name, parameter)
115
- }
116
- }
117
-
118
- const placeholders = new Set<string>()
119
- const pattern = /\{([^}]+)\}/g
120
- let match: RegExpExecArray | null = null
121
-
122
- while ((match = pattern.exec(pathTemplate)) !== null) {
123
- placeholders.add(match[1])
124
- }
125
-
126
- const defaults: Record<string, string> = {}
127
- for (const placeholder of placeholders) {
128
- const definition = parameterDefs.get(placeholder)
129
- defaults[placeholder] = defaultValueFor(placeholder, definition?.schema)
130
- }
131
-
132
- return defaults
133
- }
134
-
135
- const resolvePath = (pathTemplate: string, defaults: Record<string, string>): string =>
136
- pathTemplate.replace(/\{([^}]+)\}/g, (_match, key: string) => {
137
- const value = defaults[key] ?? '1'
138
- return encodeURIComponent(value)
139
- })
140
-
141
- const buildSwaggerCases = (): SwaggerOperationCase[] => {
142
- const cases: SwaggerOperationCase[] = []
143
-
144
- const paths = swaggerSpec.paths as Record<string, SwaggerPathEntry> | undefined
145
- if (!paths) {
146
- return cases
147
- }
148
-
149
- const sortedPaths = Object.keys(paths).sort()
150
- for (const pathTemplate of sortedPaths) {
151
- const pathItem = paths[pathTemplate]
152
- if (!pathItem || typeof pathItem !== 'object') {
153
- continue
154
- }
155
-
156
- const methods = Object.keys(pathItem).filter(
157
- (method) => method !== 'parameters' && typeof (pathItem as Record<string, unknown>)[method] === 'object',
158
- )
159
-
160
- for (const method of methods.sort()) {
161
- const operation = pathItem[method]
162
- if (!operation || typeof operation !== 'object') {
163
- continue
164
- }
165
-
166
- const defaults = getPathDefaults(pathTemplate, pathItem, operation)
167
- const resolvedPath = resolvePath(pathTemplate, defaults)
168
-
169
- cases.push({
170
- pathTemplate,
171
- method: method.toUpperCase(),
172
- resolvedPath,
173
- })
174
- }
175
- }
176
-
177
- return cases
178
- }
179
-
180
- const headers: Record<string, string> = {
181
- Accept: 'application/json',
182
- }
183
-
184
- if (apiToken) {
185
- headers.Authorization = `token ${apiToken}`
186
- }
187
-
188
- const swaggerCases = buildSwaggerCases()
189
-
190
- const getAllowedStatuses = (method: string): number[] => {
191
- const normalizedMethod = method.toUpperCase()
192
- if (normalizedMethod === 'GET') {
193
- return [200, 201, 204, 301, 302, 304, 400, 401, 403, 404, 405, 409, 422, 429]
194
- }
195
-
196
- if (normalizedMethod === 'POST') {
197
- return [200, 201, 202, 204, 400, 401, 403, 404, 405, 409, 422, 429]
198
- }
199
-
200
- if (normalizedMethod === 'PATCH' || normalizedMethod === 'PUT') {
201
- return [200, 201, 202, 204, 400, 401, 403, 404, 405, 409, 422, 429]
202
- }
203
-
204
- if (normalizedMethod === 'DELETE') {
205
- return [200, 202, 204, 400, 401, 403, 404, 405, 409, 422, 429]
206
- }
207
-
208
- if (normalizedMethod === 'HEAD') {
209
- return [200, 204, 301, 302, 304, 401, 403, 404]
210
- }
211
-
212
- return [200, 201, 204, 400, 401, 403, 404, 405, 422, 429]
213
- }
214
-
215
- describe('e2e git service API coverage from swagger', () => {
216
- it('reads host configuration', () => {
217
- expect(host).toMatch(/^https?:\/\/.+/)
218
- })
219
-
220
- for (const caseSpec of swaggerCases) {
221
- const label = `${caseSpec.method} ${caseSpec.pathTemplate}`
222
- it(`hits ${label}`, async () => {
223
- const response = await fetch(`${host}${caseSpec.resolvedPath}`, {
224
- method: caseSpec.method,
225
- headers,
226
- })
227
-
228
- expect(response.status).toBeLessThan(500)
229
- expect(getAllowedStatuses(caseSpec.method)).toContain(response.status)
230
- })
231
- }
232
- })
@@ -1,97 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest'
2
-
3
- import { createGitServiceApi } from '../src'
4
-
5
- describe('gitServiceApi object', () => {
6
- it('exposes repo.issue.create and posts issue payload', async () => {
7
- const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
8
- new Response(
9
- JSON.stringify({
10
- id: 1,
11
- number: 5,
12
- title: 'Hello',
13
- html_url: 'http://gitea.example.com/example-org/example-repo/issues/5',
14
- }),
15
- { status: 201, headers: { 'content-type': 'application/json' } },
16
- ),
17
- )
18
-
19
- const api = createGitServiceApi({
20
- config: {
21
- platform: 'GITEA',
22
- giteaHost: 'https://gitea.example.com',
23
- giteaToken: 'token',
24
- },
25
- defaultOwner: 'example-org',
26
- defaultRepo: 'example-repo',
27
- })
28
-
29
- expect(typeof api.repo).toBe('object')
30
- expect(typeof (api.repo as Record<string, unknown>).issue).toBe('object')
31
- expect(typeof (api.repo as Record<string, unknown>).issue.create).toBe('function')
32
-
33
- const result = await (api.repo as Record<string, any>).issue.create({
34
- data: {
35
- title: 'Hello',
36
- body: 'world',
37
- },
38
- })
39
-
40
- expect(fetchSpy).toHaveBeenCalledTimes(1)
41
-
42
- const [requestUrl, requestInit] = fetchSpy.mock.calls[0]
43
- expect(requestUrl).toBe('https://gitea.example.com/api/v1/repos/example-org/example-repo/issues')
44
- expect(requestInit?.method).toBe('POST')
45
-
46
- expect(JSON.parse(requestInit?.body as string)).toEqual({
47
- title: 'Hello',
48
- body: 'world',
49
- })
50
-
51
- expect(result.status).toBe(201)
52
- expect(result.body).toEqual({
53
- id: 1,
54
- number: 5,
55
- title: 'Hello',
56
- html_url: 'http://gitea.example.com/example-org/example-repo/issues/5',
57
- })
58
-
59
- fetchSpy.mockRestore()
60
- })
61
-
62
- it('aborts requests when requestTimeoutMs elapses', async () => {
63
- const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(
64
- (_url: any, init?: any) =>
65
- new Promise((_resolve, reject) => {
66
- const signal = init?.signal as AbortSignal | undefined
67
- if (signal?.aborted) {
68
- reject(new DOMException('Aborted', 'AbortError'))
69
- return
70
- }
71
- signal?.addEventListener('abort', () => reject(new DOMException('Aborted', 'AbortError')))
72
- }) as any,
73
- )
74
-
75
- const api = createGitServiceApi({
76
- config: {
77
- platform: 'GITEA',
78
- giteaHost: 'https://gitea.example.com',
79
- giteaToken: 'token',
80
- },
81
- defaultOwner: 'example-org',
82
- defaultRepo: 'example-repo',
83
- requestTimeoutMs: 5,
84
- })
85
-
86
- await expect(
87
- (api.repo as Record<string, any>).issue.create({
88
- data: {
89
- title: 'Hello',
90
- body: 'world',
91
- },
92
- }),
93
- ).rejects.toThrow(/Request timed out after 5ms:/)
94
-
95
- fetchSpy.mockRestore()
96
- })
97
- })
@@ -1,182 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
-
3
- import swaggerSpec from '../gitea-swagger.json'
4
- import { GIT_API_VERSION, buildGitApiMockResponse, createGitPlatformAdapter, type GitApiFeatureContext } from '../src'
5
- import { gitServiceFeatureSpec } from '../src/git-service-feature-spec.generated'
6
- import { getGitPlatformConfig } from '../src/platform/config'
7
-
8
- interface FeatureMatrixCase {
9
- path: string[]
10
- args: string[]
11
- flagValues: Record<string, string | boolean | undefined>
12
- }
13
-
14
- const parseArgumentValue = (argument: string): string | null => {
15
- const trimmed = argument.trim()
16
- if (!trimmed) {
17
- return null
18
- }
19
-
20
- if (trimmed.startsWith('-') || trimmed === '[flags]') {
21
- return null
22
- }
23
-
24
- const firstAlternative = trimmed.split('|')[0].trim()
25
- if (!firstAlternative) {
26
- return null
27
- }
28
-
29
- const value = firstAlternative
30
- .replace(/^\[/, '')
31
- .replace(/\]$/, '')
32
- .replace(/^\{/, '')
33
- .replace(/\}$/, '')
34
- .replace(/^</, '')
35
- .replace(/>$/, '')
36
- .replace(/\.\.\.$/, '')
37
- .trim()
38
-
39
- if (!value || value.startsWith('-')) {
40
- return null
41
- }
42
-
43
- return value
44
- }
45
-
46
- const buildArgCases = (path: string[], args: string[]): FeatureMatrixCase[] => {
47
- const values = args
48
- .map((arg) => parseArgumentValue(arg))
49
- .filter((value): value is string => value !== null)
50
-
51
- if (values.length === 0) {
52
- return [{ path, args: [], flagValues: {} }]
53
- }
54
-
55
- const cases: FeatureMatrixCase[] = []
56
- for (let index = 0; index < values.length; index += 1) {
57
- cases.push({ path, args: values.slice(0, index + 1), flagValues: {} })
58
- }
59
-
60
- return cases
61
- }
62
-
63
- const buildFlagCase = (path: string[], args: string[], flag: { name: string; takesValue: boolean }): FeatureMatrixCase => ({
64
- path,
65
- args: args.map((arg) => parseArgumentValue(arg)).filter((value): value is string => value !== null),
66
- flagValues: {
67
- [flag.name]: flag.takesValue ? 'value' : true,
68
- },
69
- })
70
-
71
- const platformConfig = getGitPlatformConfig({
72
- platform: 'GITEA',
73
- })
74
- const testRepoOwner = process.env.GITEA_TEST_OWNER ?? 'example-org'
75
- const testRepoName = process.env.GITEA_TEST_REPO ?? 'example-repo'
76
-
77
- const adapter = createGitPlatformAdapter({
78
- config: platformConfig,
79
- swaggerSpec,
80
- })
81
-
82
- const prependTestRepoArgs = (args: string[]) => [testRepoOwner, testRepoName, ...args]
83
-
84
- const expectedResponse = (
85
- mappingPath: string[],
86
- mappingMethod: string,
87
- query: string[],
88
- headers: string[],
89
- apiBase: string,
90
- apiVersion?: string,
91
- ) =>
92
- buildGitApiMockResponse({
93
- path: mappingPath,
94
- method: mappingMethod,
95
- query,
96
- headers,
97
- apiBase,
98
- apiVersion,
99
- })
100
-
101
- describe('git service feature matrix', () => {
102
- it('is populated from the generated feature spec', () => {
103
- expect(gitServiceFeatureSpec.features.length).toBeGreaterThan(0)
104
- })
105
-
106
- for (const feature of gitServiceFeatureSpec.features) {
107
- const label = feature.path.join(' ')
108
- const featureContext = (args: string[], flagValues: FeatureMatrixCase['flagValues']): GitApiFeatureContext => ({
109
- feature,
110
- args,
111
- flagValues,
112
- })
113
-
114
- const argumentCases = buildArgCases(feature.path, feature.args)
115
-
116
- for (const [index, currentCase] of argumentCases.entries()) {
117
- const title =
118
- currentCase.args.length === 0
119
- ? `maps ${label} with no arguments`
120
- : `maps ${label} with first ${index + 1} argument(s)`
121
-
122
- it(title, async () => {
123
- const testArgs = prependTestRepoArgs(currentCase.args)
124
- const mapped = await adapter.mapFeature(featureContext(testArgs, currentCase.flagValues))
125
- const response = await adapter.mapFeatureResponse(featureContext(testArgs, currentCase.flagValues))
126
-
127
- expect(response).toEqual(
128
- expectedResponse(
129
- mapped.mappedPath,
130
- mapped.method,
131
- mapped.query,
132
- mapped.headers,
133
- mapped.apiBase,
134
- mapped.apiVersion ?? GIT_API_VERSION,
135
- ),
136
- )
137
- })
138
- }
139
-
140
- for (const flag of feature.flags) {
141
- const currentCase = buildFlagCase(feature.path, feature.args, flag)
142
- const title = `maps ${label} with flag ${flag.name}${flag.takesValue ? '=value' : ''}`
143
-
144
- it(title, async () => {
145
- const testArgs = prependTestRepoArgs(currentCase.args)
146
- const mapped = await adapter.mapFeature(featureContext(testArgs, currentCase.flagValues))
147
- const response = await adapter.mapFeatureResponse(featureContext(testArgs, currentCase.flagValues))
148
-
149
- expect(response).toEqual(
150
- expectedResponse(
151
- mapped.mappedPath,
152
- mapped.method,
153
- mapped.query,
154
- mapped.headers,
155
- mapped.apiBase,
156
- mapped.apiVersion ?? GIT_API_VERSION,
157
- ),
158
- )
159
- })
160
- }
161
-
162
- it(`${label} validates against local swagger spec`, async () => {
163
- const args = feature.args
164
- .map((arg) => parseArgumentValue(arg))
165
- .filter((value): value is string => value !== null)
166
- .slice(0, 2)
167
- const testArgs = prependTestRepoArgs(args)
168
-
169
- const validation = await adapter.validateFeature(featureContext(testArgs, {}))
170
-
171
- const response = await adapter.mapFeatureResponse(featureContext(testArgs, {}))
172
-
173
- expect(response.endpoint).toBe(`${validation.apiBase}/${validation.mappedPath.join('/')}`)
174
- expect(response.path).toEqual(validation.mappedPath)
175
- if (validation.mapped) {
176
- expect(validation.reason).toBeUndefined()
177
- } else {
178
- expect(validation.reason).toBeTruthy()
179
- }
180
- })
181
- }
182
- })