mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
619 lines
21 KiB
TypeScript
619 lines
21 KiB
TypeScript
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
|
import { join } from 'node:path'
|
|
import { tmpdir } from 'node:os'
|
|
import { createElement } from 'react'
|
|
import { renderToStaticMarkup } from 'react-dom/server'
|
|
import { create } from 'zustand'
|
|
import type { AppState } from '../../src/renderer/src/store/types'
|
|
import type * as GhUtils from '../../src/main/github/gh-utils'
|
|
import type * as IssueMetadata from '../../src/renderer/src/hooks/useIssueMetadata'
|
|
import type { GitHubWorkItem } from '../../src/shared/github/work-item-types'
|
|
import type { GitHubOwnerRepo } from '../../src/shared/github/pull-request-types'
|
|
import type { TaskSourceContext } from '../../src/shared/task-source-context'
|
|
import type { Repo } from '../../src/shared/repo-types'
|
|
|
|
const fixture = vi.hoisted(() => {
|
|
const state: {
|
|
loads: { key: string | null; load: () => Promise<unknown[]> }[]
|
|
requests: { args: string[]; host?: string }[]
|
|
gh: ReturnType<typeof vi.fn>
|
|
apiUpdate: ReturnType<typeof vi.fn>
|
|
preference: 'origin' | 'upstream'
|
|
store: ReturnType<typeof createTestStore> | null
|
|
} = {
|
|
loads: [],
|
|
requests: [],
|
|
gh: vi.fn(),
|
|
apiUpdate: vi.fn(),
|
|
preference: 'upstream',
|
|
store: null
|
|
}
|
|
return state
|
|
})
|
|
|
|
vi.mock('@/store', () => ({
|
|
useAppStore: Object.assign(
|
|
(selector: (state: ReturnType<typeof metadataStore.getState>) => unknown) =>
|
|
metadataStore(selector),
|
|
{ getState: () => fixture.store?.getState() ?? metadataStore.getState() }
|
|
)
|
|
}))
|
|
vi.mock('@/i18n/i18n', () => ({ translate: (_key: string, fallback: string) => fallback }))
|
|
vi.mock('@/components/github/github-duplicate-issue-candidates', () => ({
|
|
useGitHubDuplicateIssueCandidates: () => []
|
|
}))
|
|
vi.mock('@/components/github/github-work-item-comment-mutations', () => ({
|
|
notifyWorkItemDetailsMutation: vi.fn()
|
|
}))
|
|
vi.mock('@/hooks/useIssueMetadata', async (original) => ({
|
|
...(await original<typeof IssueMetadata>()),
|
|
useImmediateMutation: () => ({ isPending: () => false, run: vi.fn() })
|
|
}))
|
|
vi.mock('@/hooks/useMetadataListRequest', () => ({
|
|
useMetadataListRequest: <T>(args: { cacheKey: string | null; load: () => Promise<T[]> }) => {
|
|
fixture.loads.push({ key: args.cacheKey, load: args.load })
|
|
return { data: [], loading: false, error: null }
|
|
}
|
|
}))
|
|
vi.mock('../../src/main/github/gh-utils', async (original) => ({
|
|
...(await original<typeof GhUtils>()),
|
|
ghExecFileAsync: fixture.gh,
|
|
acquire: vi.fn(),
|
|
release: vi.fn(),
|
|
getOwnerRepoForRemote: async (_path: string, remote: string) => ({
|
|
owner: remote === 'upstream' ? 'upstream-owner' : 'fork-owner',
|
|
repo: 'widgets'
|
|
})
|
|
}))
|
|
vi.mock('../../src/main/git/remotes/remote-name-listing', () => ({
|
|
shouldProbeGitRemote: async () => true
|
|
}))
|
|
|
|
import { GHEditSection } from '../../src/renderer/src/components/github-item-dialog/edit-item-fields/gh-edit-section'
|
|
import {
|
|
runGHEditLabelToggle,
|
|
runGHEditStateChange
|
|
} from '../../src/renderer/src/components/github-item-dialog/edit-item-fields/gh-edit-section-mutations'
|
|
import { findTaskPageDialogWorkItem } from '../../src/renderer/src/components/task-page/task-page-dialog-work-item'
|
|
import { getTaskPageRepoSourceContext } from '../../src/renderer/src/lib/task-repo-source-context'
|
|
import { workItemsCacheKey } from '../../src/renderer/src/store/github/cache-identity'
|
|
import { createTestStore } from '../../src/renderer/src/store/slices/github-slice-test-harness'
|
|
import {
|
|
materializeTaskPageItemList,
|
|
reapplyPendingTaskPageGitHubMutationsToCache
|
|
} from '../../src/renderer/src/components/task-page/github/task-page-github-work-item-mutation-pages'
|
|
import { setRuntimeEnvironmentConnectionGenerationForTests } from '../../src/renderer/src/store/slices/runtime-status-connection-generation'
|
|
import { getLastConfirmedClientValue } from '../../src/renderer/src/components/task-page/github/task-page-github-confirmed-client-values'
|
|
import { RuntimeRpcCallError } from '../../src/renderer/src/runtime/runtime-rpc-result'
|
|
import { getTaskSourceCacheScope } from '../../src/shared/task-source-context'
|
|
import { listLabels, listAssignableUsers } from '../../src/main/github/issue-field-options'
|
|
import { useRepoLabels, useRepoAssignees } from '../../src/renderer/src/hooks/useGitHubRepoMetadata'
|
|
import { updateIssue } from '../../src/main/github/issue-update'
|
|
import {
|
|
resetTaskPageGitHubMutationRegistryForTests,
|
|
setTaskPageGitHubMutationQueryKey
|
|
} from '../../src/renderer/src/components/task-page/github/task-page-github-work-item-mutation-registry'
|
|
|
|
const registeredRepo: Repo = {
|
|
id: 'repo-1',
|
|
path: join(tmpdir(), 'orca-opened-issue-repository-fixture'),
|
|
displayName: 'widgets',
|
|
badgeColor: 'primary',
|
|
addedAt: 1,
|
|
upstream: { owner: 'upstream-owner', repo: 'widgets', host: 'github.com' }
|
|
}
|
|
|
|
const metadataStore = create<
|
|
Pick<AppState, 'repos' | 'patchWorkItem' | 'patchProjectRowContent' | 'recordFeatureInteraction'>
|
|
>()(() => ({
|
|
repos: [registeredRepo],
|
|
patchWorkItem: vi.fn(),
|
|
patchProjectRowContent: vi.fn(),
|
|
recordFeatureInteraction: vi.fn()
|
|
}))
|
|
|
|
function renderEditSection(props: Parameters<typeof GHEditSection>[0]): void {
|
|
renderToStaticMarkup(createElement(GHEditSection, props))
|
|
}
|
|
|
|
function sourceFor(preference: 'origin' | 'upstream'): TaskSourceContext {
|
|
const source = getTaskPageRepoSourceContext(
|
|
{ ...registeredRepo, issueSourcePreference: preference },
|
|
'github'
|
|
)
|
|
if (!source) {
|
|
throw new Error('Registered fixture must produce a source context')
|
|
}
|
|
return source
|
|
}
|
|
|
|
const sourceContext = sourceFor('origin')
|
|
const fork: GitHubWorkItem = {
|
|
id: 'issue:5',
|
|
type: 'issue',
|
|
number: 5,
|
|
title: 'FORK title',
|
|
state: 'open',
|
|
url: 'https://github.com/fork-owner/widgets/issues/5',
|
|
labels: [],
|
|
updatedAt: '',
|
|
author: null,
|
|
repoId: 'repo-1'
|
|
}
|
|
const upstream: GitHubWorkItem = {
|
|
...fork,
|
|
title: 'UPSTREAM title',
|
|
url: 'https://github.com/upstream-owner/widgets/issues/5'
|
|
}
|
|
const issueRepo = { owner: 'fork-owner', repo: 'widgets', host: 'github.com' }
|
|
const unsupported = () =>
|
|
new RuntimeRpcCallError({
|
|
id: 'old-server',
|
|
ok: false,
|
|
error: { code: 'method_not_found', message: 'unsupported' }
|
|
})
|
|
|
|
type MetadataArgs = {
|
|
repoPath: string
|
|
ownerRepo?: GitHubOwnerRepo
|
|
expectedGhAccount?: Parameters<typeof listLabels>[5]
|
|
}
|
|
|
|
beforeEach(() => {
|
|
fixture.store = null
|
|
fixture.preference = 'upstream'
|
|
metadataStore.setState({ repos: [registeredRepo] })
|
|
fixture.loads = []
|
|
fixture.requests = []
|
|
resetTaskPageGitHubMutationRegistryForTests()
|
|
setTaskPageGitHubMutationQueryKey('current-upstream-list')
|
|
fixture.gh.mockReset()
|
|
fixture.gh.mockImplementation(async (args: string[], options: { host?: string }) => {
|
|
fixture.requests.push({ args, host: options.host })
|
|
return { stdout: '', stderr: '' }
|
|
})
|
|
fixture.apiUpdate = vi.fn((args: Parameters<typeof window.api.gh.updateIssue>[0]) =>
|
|
updateIssue(
|
|
args.repoPath,
|
|
args.number,
|
|
args.updates,
|
|
null,
|
|
{},
|
|
fixture.preference,
|
|
args.ownerRepo
|
|
)
|
|
)
|
|
vi.stubGlobal('window', {
|
|
api: {
|
|
gh: {
|
|
updateIssue: fixture.apiUpdate,
|
|
listLabels: (args: MetadataArgs) =>
|
|
listLabels(
|
|
args.repoPath,
|
|
fixture.preference,
|
|
null,
|
|
{},
|
|
args.ownerRepo,
|
|
args.expectedGhAccount
|
|
),
|
|
listAssignableUsers: (args: MetadataArgs) =>
|
|
listAssignableUsers(
|
|
args.repoPath,
|
|
fixture.preference,
|
|
null,
|
|
{},
|
|
args.ownerRepo,
|
|
args.expectedGhAccount
|
|
)
|
|
}
|
|
}
|
|
})
|
|
})
|
|
|
|
afterEach(() => {
|
|
resetTaskPageGitHubMutationRegistryForTests()
|
|
vi.unstubAllGlobals()
|
|
})
|
|
|
|
it.each([
|
|
{ openedItem: fork, listItem: upstream, preference: 'upstream' },
|
|
{ openedItem: upstream, listItem: fork, preference: 'origin' },
|
|
{ openedItem: fork, listItem: fork, preference: 'origin' }
|
|
] as const)(
|
|
'scopes $openedItem.title labels under $preference to its canonical list row',
|
|
async ({ openedItem, listItem, preference }) => {
|
|
fixture.preference = preference
|
|
expect(sourceFor('upstream')).toEqual(sourceContext)
|
|
const store = createTestStore()
|
|
store.setState({ repos: [], recordFeatureInteraction: vi.fn() })
|
|
fixture.store = store
|
|
const key = workItemsCacheKey(
|
|
registeredRepo.id,
|
|
36,
|
|
'',
|
|
getTaskSourceCacheScope(sourceFor('upstream'))
|
|
)
|
|
store.setState({
|
|
workItemsCache: { [key]: { data: [listItem], fetchedAt: Date.now() } }
|
|
})
|
|
const opened =
|
|
findTaskPageDialogWorkItem(store.getState().workItemsCache, {
|
|
id: openedItem.id,
|
|
repoId: openedItem.repoId,
|
|
url: openedItem.url
|
|
}) ?? openedItem
|
|
expect(opened.url).toBe(openedItem.url)
|
|
const target = { ...issueRepo, owner: openedItem === fork ? 'fork-owner' : 'upstream-owner' }
|
|
let mutation: Promise<unknown> = Promise.resolve()
|
|
runGHEditLabelToggle({
|
|
item: opened,
|
|
itemNumber: opened.number,
|
|
itemRepoId: opened.repoId,
|
|
repoPath: registeredRepo.path,
|
|
sourceContext,
|
|
projectOrigin: undefined,
|
|
issueRepo: target,
|
|
label: 'fork-only-label',
|
|
localLabels: [],
|
|
run: async (_key, options) => {
|
|
options.onOptimistic?.()
|
|
mutation = options.mutate()
|
|
await mutation
|
|
options.onSuccess?.()
|
|
return true
|
|
},
|
|
onLabelsChange: vi.fn(),
|
|
patchProjectRowIfNeeded: vi.fn(),
|
|
onMutated: vi.fn()
|
|
})
|
|
await mutation
|
|
expect(fixture.requests[0].args).toContain(`${target.owner}/widgets`)
|
|
expect(fixture.apiUpdate).toHaveBeenCalledWith(expect.objectContaining({ ownerRepo: target }))
|
|
expect(store.getState().workItemsCache[key]?.data?.[0].labels).toEqual(
|
|
listItem.url === openedItem.url ? ['fork-only-label'] : []
|
|
)
|
|
}
|
|
)
|
|
|
|
it.each([
|
|
{ openedItem: fork, owner: 'fork-owner', preference: 'origin' },
|
|
{ openedItem: fork, owner: 'fork-owner', preference: 'upstream' },
|
|
{ openedItem: upstream, owner: 'upstream-owner', preference: 'origin' },
|
|
{ openedItem: upstream, owner: 'upstream-owner', preference: 'upstream' }
|
|
] as const)(
|
|
'loads $owner picker candidates while preference=$preference',
|
|
async ({ preference, openedItem, owner }) => {
|
|
fixture.preference = preference
|
|
renderEditSection({
|
|
item: openedItem,
|
|
repoPath: registeredRepo.path,
|
|
repoId: fork.repoId,
|
|
sourceContext,
|
|
projectOrigin: undefined,
|
|
localState: 'open',
|
|
localLabels: [],
|
|
assignees: [],
|
|
onStateChange: vi.fn(),
|
|
onLabelsChange: vi.fn(),
|
|
onMutated: vi.fn(),
|
|
onUse: vi.fn()
|
|
})
|
|
for (const request of fixture.loads.filter((load) => load.key !== null)) {
|
|
await request.load()
|
|
}
|
|
expect(
|
|
fixture.requests.map((request) => request.args.find((arg) => arg.startsWith('repos/')))
|
|
).toEqual([`repos/${owner}/widgets/labels`, `repos/${owner}/widgets/assignees?per_page=100`])
|
|
}
|
|
)
|
|
|
|
it.each(
|
|
[true, false].flatMap((tracked) => [
|
|
{ openedItem: fork, listItem: fork, tracked },
|
|
{ openedItem: fork, listItem: upstream, tracked },
|
|
{ openedItem: upstream, listItem: fork, tracked },
|
|
{ openedItem: upstream, listItem: upstream, tracked }
|
|
])
|
|
)(
|
|
'a $openedItem.title close only controls its own row while search lags (list=$listItem.title, tracked=$tracked)',
|
|
async ({ openedItem, listItem, tracked }) => {
|
|
const target = { ...issueRepo, owner: openedItem === fork ? 'fork-owner' : 'upstream-owner' }
|
|
fixture.store = createTestStore()
|
|
fixture.store.setState({ repos: [registeredRepo], recordFeatureInteraction: vi.fn() })
|
|
const cache = fixture.store.getState().githubTaskCache
|
|
const binding = cache.capture(registeredRepo, sourceContext)!
|
|
if (!tracked) {
|
|
// An old server without the item route: no task record can own the edit, so main's hold applies.
|
|
await cache.readCanonical(openedItem, binding, () => Promise.reject(unsupported()))
|
|
expect(cache.canTrack(binding)).toBe(false)
|
|
}
|
|
let pending = Promise.resolve()
|
|
runGHEditStateChange({
|
|
newState: 'closed',
|
|
localState: 'open',
|
|
item: openedItem,
|
|
itemNumber: fork.number,
|
|
itemRepoId: fork.repoId,
|
|
repoPath: registeredRepo.path,
|
|
sourceContext,
|
|
projectOrigin: undefined,
|
|
issueRepo: target,
|
|
run: (_key, options) => {
|
|
pending = (async () => {
|
|
options.onOptimistic?.()
|
|
await options.mutate()
|
|
options.onSuccess?.()
|
|
})()
|
|
return pending
|
|
},
|
|
onStateChange: vi.fn(),
|
|
patchProjectRowIfNeeded: vi.fn(),
|
|
onMutated: vi.fn()
|
|
})
|
|
await pending
|
|
expect(fixture.apiUpdate).toHaveBeenCalledWith(expect.objectContaining({ ownerRepo: target }))
|
|
// A record owns a tracked close; only an untracked one keeps main's registry hold.
|
|
const hold = getLastConfirmedClientValue(
|
|
getTaskSourceCacheScope(sourceContext),
|
|
fork.repoId,
|
|
fork.id,
|
|
'state'
|
|
)
|
|
expect(hold).toBe(tracked ? undefined : 'closed')
|
|
// The lagging search row goes through the fetch, the Tasks page list and its projection.
|
|
const [shown] = materializeTaskPageItemList({
|
|
networkItems: [...cache.acceptSearch([listItem], binding)],
|
|
previousItems: [listItem],
|
|
queryKey: 'current-upstream-list'
|
|
})
|
|
expect(cache.projectItem(shown).state).toBe(listItem.url === openedItem.url ? 'closed' : 'open')
|
|
}
|
|
)
|
|
|
|
it.each(
|
|
[true, false].flatMap(
|
|
(tracked) =>
|
|
[
|
|
{ tracked, from: 'open', to: 'closed' },
|
|
{ tracked, from: 'closed', to: 'open' }
|
|
] as const
|
|
)
|
|
)(
|
|
'a failed $to edit leaves stored copies as they were through a refresh (tracked=$tracked)',
|
|
async ({ tracked, from, to }) => {
|
|
const store = createTestStore()
|
|
store.setState({ repos: [registeredRepo], recordFeatureInteraction: vi.fn() })
|
|
fixture.store = store
|
|
const cache = store.getState().githubTaskCache
|
|
const binding = cache.capture(registeredRepo, sourceContext)!
|
|
if (!tracked) {
|
|
await cache.readCanonical(fork, binding, () => Promise.reject(unsupported()))
|
|
}
|
|
const row = { ...fork, state: from }
|
|
const key = workItemsCacheKey(registeredRepo.id, 36, '', getTaskSourceCacheScope(sourceContext))
|
|
store.setState({ workItemsCache: { [key]: { data: [row], fetchedAt: 1 } } })
|
|
fixture.apiUpdate.mockRejectedValueOnce(new Error('denied'))
|
|
let pending = Promise.resolve()
|
|
let pages = [row]
|
|
runGHEditStateChange({
|
|
newState: to,
|
|
localState: from,
|
|
item: row,
|
|
itemNumber: fork.number,
|
|
itemRepoId: fork.repoId,
|
|
repoPath: registeredRepo.path,
|
|
sourceContext,
|
|
projectOrigin: undefined,
|
|
issueRepo,
|
|
run: (_key, options) => {
|
|
pending = (async () => {
|
|
options.onOptimistic?.()
|
|
// A Tasks refresh lands while the edit is in flight, with search still lagging.
|
|
reapplyPendingTaskPageGitHubMutationsToCache({
|
|
items: store.getState().workItemsCache[key]?.data ?? [],
|
|
patchWorkItem: store.getState().patchWorkItem,
|
|
sourceContextByRepoId: new Map([[registeredRepo.id, sourceContext]])
|
|
})
|
|
pages = materializeTaskPageItemList({
|
|
networkItems: [row],
|
|
previousItems: pages,
|
|
queryKey: 'current-upstream-list'
|
|
})
|
|
await options.mutate().catch(() => options.onRevert?.())
|
|
})()
|
|
return pending
|
|
},
|
|
onStateChange: vi.fn(),
|
|
patchProjectRowIfNeeded: vi.fn(),
|
|
onMutated: vi.fn()
|
|
})
|
|
await pending
|
|
const raw = store.getState().workItemsCache[key]!.data![0]
|
|
const shown = [raw, pages[0]].map((copy) => cache.projectItem(copy).state)
|
|
expect([raw.state, pages[0].state, ...shown]).toEqual([from, from, from, from])
|
|
expect(
|
|
getLastConfirmedClientValue(
|
|
getTaskSourceCacheScope(sourceContext),
|
|
fork.repoId,
|
|
fork.id,
|
|
'state'
|
|
)
|
|
).toBeUndefined()
|
|
}
|
|
)
|
|
|
|
it('lets a record own a close that began on an unsupported server and succeeded after a reconnect', async () => {
|
|
setRuntimeEnvironmentConnectionGenerationForTests('p34245-takeover', 1)
|
|
const remote = { ...registeredRepo, executionHostId: 'runtime:p34245-takeover' }
|
|
const remoteSource = getTaskPageRepoSourceContext(remote, 'github')!
|
|
const store = createTestStore()
|
|
store.setState({
|
|
repos: [remote],
|
|
runtimeStatusByEnvironmentId: new Map(),
|
|
recordFeatureInteraction: vi.fn()
|
|
})
|
|
fixture.store = store
|
|
const cache = store.getState().githubTaskCache
|
|
await cache.readCanonical(fork, cache.capture(remote, remoteSource)!, () =>
|
|
Promise.reject(unsupported())
|
|
)
|
|
expect(cache.canTrack(cache.capture(remote, remoteSource)!)).toBe(false)
|
|
runGHEditStateChange({
|
|
newState: 'closed',
|
|
localState: 'open',
|
|
item: fork,
|
|
itemNumber: fork.number,
|
|
itemRepoId: fork.repoId,
|
|
repoPath: remote.path,
|
|
sourceContext: remoteSource,
|
|
projectOrigin: undefined,
|
|
issueRepo,
|
|
// The write is in flight while the upgraded server reconnects, then it succeeds.
|
|
run: async (_key, options) => {
|
|
options.onOptimistic?.()
|
|
setRuntimeEnvironmentConnectionGenerationForTests('p34245-takeover', 2)
|
|
store.setState({ runtimeStatusByEnvironmentId: new Map() })
|
|
options.onSuccess?.()
|
|
},
|
|
onStateChange: vi.fn(),
|
|
patchProjectRowIfNeeded: vi.fn(),
|
|
onMutated: vi.fn()
|
|
})
|
|
const scope = getTaskSourceCacheScope(remoteSource)
|
|
expect(getLastConfirmedClientValue(scope, fork.repoId, fork.id, 'state')).toBeUndefined()
|
|
const binding = cache.capture(remote, remoteSource)!
|
|
expect(cache.isPending(fork, binding) || cache.readRevision(fork, binding) > 0).toBe(true)
|
|
expect(cache.projectItem(cache.acceptSearch([fork], binding)[0]).state).toBe('closed')
|
|
})
|
|
|
|
it('sends a duplicate close with its reason even when the issue already reads closed', async () => {
|
|
fixture.store = createTestStore()
|
|
fixture.store.setState({ repos: [], recordFeatureInteraction: vi.fn() })
|
|
let pending: Promise<void> = Promise.resolve()
|
|
runGHEditStateChange({
|
|
newState: 'closed',
|
|
closeAction: { stateReason: 'duplicate', duplicateOf: 42 },
|
|
localState: 'closed',
|
|
item: { ...fork, state: 'closed' },
|
|
itemNumber: fork.number,
|
|
itemRepoId: fork.repoId,
|
|
repoPath: registeredRepo.path,
|
|
sourceContext,
|
|
projectOrigin: undefined,
|
|
issueRepo,
|
|
run: (_key, options) => {
|
|
pending = options.mutate().then(() => options.onSuccess?.())
|
|
return pending
|
|
},
|
|
onStateChange: vi.fn(),
|
|
patchProjectRowIfNeeded: vi.fn(),
|
|
onMutated: vi.fn()
|
|
})
|
|
await pending
|
|
expect(fixture.apiUpdate).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
updates: expect.objectContaining({ stateReason: 'duplicate', duplicateOf: 42 })
|
|
})
|
|
)
|
|
// The hold covers only the confirmed state, never the duplicate metadata.
|
|
expect(
|
|
getLastConfirmedClientValue(
|
|
getTaskSourceCacheScope(sourceContext),
|
|
fork.repoId,
|
|
fork.id,
|
|
'state'
|
|
)
|
|
).toBe('closed')
|
|
})
|
|
|
|
it('keeps metadata requests without an explicit target compatible', async () => {
|
|
function Metadata() {
|
|
useRepoLabels(registeredRepo.path, registeredRepo.id)
|
|
useRepoAssignees(registeredRepo.path, registeredRepo.id)
|
|
return null
|
|
}
|
|
renderToStaticMarkup(createElement(Metadata))
|
|
for (const request of fixture.loads) {
|
|
await request.load()
|
|
}
|
|
const scope = JSON.stringify([
|
|
'local',
|
|
null,
|
|
registeredRepo.id,
|
|
registeredRepo.path,
|
|
registeredRepo.id,
|
|
registeredRepo.path,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null
|
|
])
|
|
expect(fixture.loads.map((load) => load.key)).toEqual([
|
|
`${registeredRepo.id}::${scope}`,
|
|
`${registeredRepo.id}::${scope}`
|
|
])
|
|
expect(
|
|
fixture.requests.map((request) => request.args.find((arg) => arg.startsWith('repos/')))
|
|
).toEqual([
|
|
'repos/upstream-owner/widgets/labels',
|
|
'repos/upstream-owner/widgets/assignees?per_page=100'
|
|
])
|
|
})
|
|
|
|
it('keeps Project row metadata on the existing slug route', async () => {
|
|
const labels = vi.fn().mockResolvedValue({ ok: true, labels: [] })
|
|
const users = vi.fn().mockResolvedValue({ ok: true, users: [] })
|
|
vi.stubGlobal('window', {
|
|
api: { gh: { listLabelsBySlug: labels, listAssignableUsersBySlug: users } }
|
|
})
|
|
renderEditSection({
|
|
item: fork,
|
|
repoPath: registeredRepo.path,
|
|
repoId: fork.repoId,
|
|
sourceContext,
|
|
projectOrigin: {
|
|
owner: 'project-owner',
|
|
repo: 'outside',
|
|
host: 'ghe.example',
|
|
number: fork.number,
|
|
type: 'issue',
|
|
projectId: 'project-1',
|
|
projectItemId: 'row-1',
|
|
cacheKey: 'project-key'
|
|
},
|
|
localState: 'open',
|
|
localLabels: [],
|
|
assignees: [],
|
|
onStateChange: vi.fn(),
|
|
onLabelsChange: vi.fn(),
|
|
onMutated: vi.fn(),
|
|
onUse: vi.fn()
|
|
})
|
|
for (const request of fixture.loads.filter((load) => load.key !== null)) {
|
|
await request.load()
|
|
}
|
|
expect(labels).toHaveBeenCalledWith({
|
|
owner: 'project-owner',
|
|
repo: 'outside',
|
|
host: 'ghe.example'
|
|
})
|
|
expect(users).toHaveBeenCalledWith({
|
|
owner: 'project-owner',
|
|
repo: 'outside',
|
|
host: 'ghe.example'
|
|
})
|
|
expect(fixture.requests).toEqual([])
|
|
})
|
|
|
|
it('does not fall back to upstream metadata for an invalid explicit repository', async () => {
|
|
const invalid = { owner: '../escape', repo: 'widgets', host: 'github.com' }
|
|
await expect(listLabels(registeredRepo.path, 'upstream', null, {}, invalid)).resolves.toEqual([])
|
|
await expect(
|
|
listAssignableUsers(registeredRepo.path, 'upstream', null, {}, invalid)
|
|
).resolves.toEqual([])
|
|
expect(fixture.requests).toEqual([])
|
|
})
|