@foundation0/git 1.0.0 → 1.2.0
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.
- package/{packages/git/mcp → mcp}/cli.mjs +0 -0
- package/package.json +43 -13
- package/.codex.example/config.toml +0 -10
- package/.env.example +0 -10
- package/packages/fs/README.md +0 -47
- package/packages/fs/node_modules/.bin/f0-git-mcp +0 -21
- package/packages/fs/node_modules/.bin/f0-git-mcp-server +0 -21
- package/packages/fs/node_modules/.bin/f0-git-mcp-server.CMD +0 -12
- package/packages/fs/node_modules/.bin/f0-git-mcp-server.ps1 +0 -41
- package/packages/fs/node_modules/.bin/f0-git-mcp.CMD +0 -12
- package/packages/fs/node_modules/.bin/f0-git-mcp.ps1 +0 -41
- package/packages/fs/node_modules/.bin/tsc +0 -21
- package/packages/fs/node_modules/.bin/tsc.CMD +0 -12
- package/packages/fs/node_modules/.bin/tsc.ps1 +0 -41
- package/packages/fs/node_modules/.bin/tsserver +0 -21
- package/packages/fs/node_modules/.bin/tsserver.CMD +0 -12
- package/packages/fs/node_modules/.bin/tsserver.ps1 +0 -41
- package/packages/fs/node_modules/.bin/vite +0 -21
- package/packages/fs/node_modules/.bin/vite.CMD +0 -12
- package/packages/fs/node_modules/.bin/vite.ps1 +0 -41
- package/packages/fs/node_modules/.bin/vitest +0 -21
- package/packages/fs/node_modules/.bin/vitest.CMD +0 -12
- package/packages/fs/node_modules/.bin/vitest.ps1 +0 -41
- package/packages/fs/package.json +0 -28
- package/packages/fs/src/cli.ts +0 -74
- package/packages/fs/src/git-fs.ts +0 -705
- package/packages/fs/src/index.ts +0 -33
- package/packages/fs/src/mount.ts +0 -297
- package/packages/fs/tsconfig.json +0 -7
- package/packages/git/mcp/tests/e2e/git-mcp-e2e.spec.ts +0 -157
- package/packages/git/mcp/tests/e2e/server.fixture.ts +0 -109
- package/packages/git/node_modules/.bin/tsc +0 -21
- package/packages/git/node_modules/.bin/tsc.CMD +0 -12
- package/packages/git/node_modules/.bin/tsc.ps1 +0 -41
- package/packages/git/node_modules/.bin/tsserver +0 -21
- package/packages/git/node_modules/.bin/tsserver.CMD +0 -12
- package/packages/git/node_modules/.bin/tsserver.ps1 +0 -41
- package/packages/git/node_modules/.bin/vite +0 -21
- package/packages/git/node_modules/.bin/vite.CMD +0 -12
- package/packages/git/node_modules/.bin/vite.ps1 +0 -41
- package/packages/git/node_modules/.bin/vitest +0 -21
- package/packages/git/node_modules/.bin/vitest.CMD +0 -12
- package/packages/git/node_modules/.bin/vitest.ps1 +0 -41
- package/packages/git/node_modules/.vite/vitest/results.json +0 -1
- package/packages/git/package.json +0 -60
- package/packages/git/scripts/create-issue.mjs +0 -93
- package/packages/git/scripts/extract-git-spec.mjs +0 -234
- package/packages/git/scripts/fetch-gitea-swagger.mjs +0 -22
- package/packages/git/tests/api.spec.ts +0 -55
- package/packages/git/tests/e2e/git-service-feature-e2e.spec.ts +0 -232
- package/packages/git/tests/git-service-api-object.spec.ts +0 -97
- package/packages/git/tests/git-service-feature-matrix.spec.ts +0 -182
- package/packages/git/tests/issue-dependencies.spec.ts +0 -81
- package/packages/git/tsconfig.json +0 -7
- package/packages/git/vitest.config.ts +0 -7
- package/packages/utils/package.json +0 -9
- package/packages/utils/src/awk.ts +0 -6
- package/packages/utils/src/cat.ts +0 -6
- package/packages/utils/src/cd.ts +0 -6
- package/packages/utils/src/chgrp.ts +0 -6
- package/packages/utils/src/chmod.ts +0 -6
- package/packages/utils/src/chown.ts +0 -6
- package/packages/utils/src/cp.ts +0 -6
- package/packages/utils/src/curl.ts +0 -6
- package/packages/utils/src/cut.ts +0 -6
- package/packages/utils/src/date.ts +0 -6
- package/packages/utils/src/echo.ts +0 -6
- package/packages/utils/src/find.ts +0 -6
- package/packages/utils/src/grep.ts +0 -6
- package/packages/utils/src/gunzip.ts +0 -6
- package/packages/utils/src/gzip.ts +0 -6
- package/packages/utils/src/head.ts +0 -6
- package/packages/utils/src/hostname.ts +0 -6
- package/packages/utils/src/index.ts +0 -37
- package/packages/utils/src/ls.ts +0 -6
- package/packages/utils/src/mkdir.ts +0 -6
- package/packages/utils/src/mv.ts +0 -6
- package/packages/utils/src/ping.ts +0 -6
- package/packages/utils/src/pwd.ts +0 -6
- package/packages/utils/src/rm.ts +0 -6
- package/packages/utils/src/rmdir.ts +0 -6
- package/packages/utils/src/sed.ts +0 -6
- package/packages/utils/src/sort.ts +0 -6
- package/packages/utils/src/tail.ts +0 -6
- package/packages/utils/src/tar.ts +0 -6
- package/packages/utils/src/touch.ts +0 -6
- package/packages/utils/src/tr.ts +0 -6
- package/packages/utils/src/uname.ts +0 -6
- package/packages/utils/src/uniq.ts +0 -6
- package/packages/utils/src/unzip.ts +0 -6
- package/packages/utils/src/util.ts +0 -4
- package/packages/utils/src/wc.ts +0 -6
- package/packages/utils/src/wget.ts +0 -6
- package/packages/utils/src/whoami.ts +0 -6
- package/packages/utils/src/zip.ts +0 -6
- package/pnpm-workspace.yaml +0 -2
- package/tsconfig.base.json +0 -12
- /package/{packages/git/README.md → README.md} +0 -0
- /package/{packages/git/gitea-swagger.json → gitea-swagger.json} +0 -0
- /package/{packages/git/mcp → mcp}/README.md +0 -0
- /package/{packages/git/mcp → mcp}/src/cli.ts +0 -0
- /package/{packages/git/mcp → mcp}/src/client.ts +0 -0
- /package/{packages/git/mcp → mcp}/src/index.ts +0 -0
- /package/{packages/git/mcp → mcp}/src/server.ts +0 -0
- /package/{packages/git/src → src}/api.ts +0 -0
- /package/{packages/git/src → src}/git-service-api.ts +0 -0
- /package/{packages/git/src → src}/git-service-feature-spec.generated.ts +0 -0
- /package/{packages/git/src → src}/index.ts +0 -0
- /package/{packages/git/src → src}/issue-dependencies.ts +0 -0
- /package/{packages/git/src → src}/platform/config.ts +0 -0
- /package/{packages/git/src → src}/platform/gitea-adapter.ts +0 -0
- /package/{packages/git/src → src}/platform/gitea-rules.ts +0 -0
- /package/{packages/git/src → src}/platform/index.ts +0 -0
- /package/{packages/git/src → src}/repository.ts +0 -0
- /package/{packages/git/src → src}/spec-mock.ts +0 -0
|
@@ -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
|
-
})
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import { extractDependencyIssueNumbers, syncIssueDependencies } from '../src/issue-dependencies'
|
|
4
|
-
|
|
5
|
-
describe('extractDependencyIssueNumbers', () => {
|
|
6
|
-
it('extracts issue numbers from direct array payload', () => {
|
|
7
|
-
const values = extractDependencyIssueNumbers([
|
|
8
|
-
{ index: 12, owner: 'example-org', repo: 'test' },
|
|
9
|
-
{ number: '34', owner: 'example-org', repo: 'test' },
|
|
10
|
-
])
|
|
11
|
-
|
|
12
|
-
expect(values.sort((a, b) => a - b)).toEqual([12, 34])
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
it('extracts issue numbers from wrapped objects', () => {
|
|
16
|
-
const values = extractDependencyIssueNumbers({
|
|
17
|
-
dependencies: [{ issue: { index: 5 } }, { issue: { number: 7 } }],
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
expect(values.sort((a, b) => a - b)).toEqual([5, 7])
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
it('extracts issue numbers from issue meta-like object', () => {
|
|
24
|
-
const values = extractDependencyIssueNumbers({ issue: { index: 8 }, number: 9 })
|
|
25
|
-
expect(values.sort((a, b) => a - b)).toEqual([8, 9])
|
|
26
|
-
})
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
describe('syncIssueDependencies', () => {
|
|
30
|
-
it('adds and removes dependencies and verifies final state', async () => {
|
|
31
|
-
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
|
|
32
|
-
new Response(JSON.stringify([{ number: 2 }]), {
|
|
33
|
-
status: 200,
|
|
34
|
-
headers: { 'content-type': 'application/json' },
|
|
35
|
-
}),
|
|
36
|
-
).mockResolvedValueOnce(
|
|
37
|
-
new Response(null, {
|
|
38
|
-
status: 204,
|
|
39
|
-
headers: { 'content-type': 'application/json' },
|
|
40
|
-
}),
|
|
41
|
-
).mockResolvedValueOnce(
|
|
42
|
-
new Response(JSON.stringify({ status: 'ok' }), {
|
|
43
|
-
status: 201,
|
|
44
|
-
headers: { 'content-type': 'application/json' },
|
|
45
|
-
}),
|
|
46
|
-
).mockResolvedValueOnce(
|
|
47
|
-
new Response(JSON.stringify([{ number: 1 }]), {
|
|
48
|
-
status: 200,
|
|
49
|
-
headers: { 'content-type': 'application/json' },
|
|
50
|
-
}),
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
const result = await syncIssueDependencies('example-org', 'example-repo', 42, [1], 'https://gitea.example.com', 'token', false)
|
|
54
|
-
|
|
55
|
-
expect(result).toEqual({
|
|
56
|
-
added: 1,
|
|
57
|
-
removed: 1,
|
|
58
|
-
unchanged: 0,
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
const calls = fetchSpy.mock.calls
|
|
62
|
-
expect(calls).toHaveLength(4)
|
|
63
|
-
const [url1, req1] = calls[0]
|
|
64
|
-
const [url2, req2] = calls[1]
|
|
65
|
-
const [url3, req3] = calls[2]
|
|
66
|
-
const [url4] = calls[3]
|
|
67
|
-
|
|
68
|
-
expect(url1).toBe('https://gitea.example.com/api/v1/repos/example-org/example-repo/issues/42/dependencies')
|
|
69
|
-
expect(req1?.method).toBe('GET')
|
|
70
|
-
|
|
71
|
-
expect(req2?.method).toBe('DELETE')
|
|
72
|
-
expect(JSON.parse(req2?.body as string)).toMatchObject({ index: 2, owner: 'example-org', repo: 'example-repo' })
|
|
73
|
-
|
|
74
|
-
expect(req3?.method).toBe('POST')
|
|
75
|
-
expect(JSON.parse(req3?.body as string)).toMatchObject({ index: 1, owner: 'example-org', repo: 'example-repo' })
|
|
76
|
-
|
|
77
|
-
expect(url4).toBe('https://gitea.example.com/api/v1/repos/example-org/example-repo/issues/42/dependencies')
|
|
78
|
-
|
|
79
|
-
fetchSpy.mockRestore()
|
|
80
|
-
})
|
|
81
|
-
})
|