mirror of
https://github.com/stablyai/orca.git
synced 2026-10-02 16:02:15 +00:00
fix(workspaces): honor legacy display-name provenance
This commit is contained in:
@@ -55,7 +55,7 @@ async function flush(): Promise<void> {
|
||||
// cutover). Records every call so tests can assert on the clientMutationId.
|
||||
function scriptedClient(
|
||||
outcomes: Array<
|
||||
| { id: string }
|
||||
| { id: string; displayName?: string }
|
||||
| { errorMessage: string }
|
||||
// takesMs models how long the ambiguity took to SURFACE — a clean close is
|
||||
// instant, a half-open socket waits out the liveness watchdog or the timeout.
|
||||
@@ -103,7 +103,12 @@ function scriptedClient(
|
||||
return {
|
||||
id: '1',
|
||||
ok: true,
|
||||
result: { worktree: { id: outcome.id } },
|
||||
result: {
|
||||
worktree: {
|
||||
id: outcome.id,
|
||||
...(outcome.displayName !== undefined ? { displayName: outcome.displayName } : {})
|
||||
}
|
||||
},
|
||||
_meta: { runtimeId: 'r' }
|
||||
}
|
||||
}
|
||||
@@ -227,6 +232,40 @@ describe('createWorktreeWithNameRetry', () => {
|
||||
expect(attempts[1]!.params.name).toBe('topic-2')
|
||||
})
|
||||
|
||||
it('uses the host-selected display name after a collision retry', async () => {
|
||||
const attempts: Attempt[] = []
|
||||
const client = scriptedClient(
|
||||
[{ errorMessage: 'already exists locally' }, { id: 'wt-host-name', displayName: 'topic-3' }],
|
||||
attempts
|
||||
)
|
||||
|
||||
await expect(
|
||||
createWorktreeWithNameRetry({
|
||||
client,
|
||||
baseName: 'topic',
|
||||
buildParams: (name) => ({ repo: 'id:r', name }),
|
||||
worktreeCreateIdempotency: false
|
||||
})
|
||||
).resolves.toEqual({ worktreeId: 'wt-host-name', name: 'topic-3' })
|
||||
})
|
||||
|
||||
it('falls back to the client candidate when an older host omits displayName', async () => {
|
||||
const attempts: Attempt[] = []
|
||||
const client = scriptedClient(
|
||||
[{ errorMessage: 'already exists locally' }, { id: 'wt-legacy' }],
|
||||
attempts
|
||||
)
|
||||
|
||||
await expect(
|
||||
createWorktreeWithNameRetry({
|
||||
client,
|
||||
baseName: 'topic',
|
||||
buildParams: (name) => ({ repo: 'id:r', name }),
|
||||
worktreeCreateIdempotency: false
|
||||
})
|
||||
).resolves.toEqual({ worktreeId: 'wt-legacy', name: 'topic-2' })
|
||||
})
|
||||
|
||||
it('advances generated retries without nesting suffixes', async () => {
|
||||
const attempts: Attempt[] = []
|
||||
const client = scriptedClient(
|
||||
|
||||
@@ -82,8 +82,17 @@ export async function createWorktreeWithNameRetry(
|
||||
: candidateParams
|
||||
const response = await sendWorktreeCreateResilient(client, params, worktreeCreateIdempotency)
|
||||
if (response.ok) {
|
||||
const result = (response as RpcSuccess).result as { worktree: { id: string } }
|
||||
return { worktreeId: result.worktree.id, name: candidateName }
|
||||
const result = (response as RpcSuccess).result as {
|
||||
worktree: { id: string; displayName?: string }
|
||||
}
|
||||
const authoritativeName = result.worktree.displayName
|
||||
return {
|
||||
worktreeId: result.worktree.id,
|
||||
name:
|
||||
typeof authoritativeName === 'string' && authoritativeName.trim()
|
||||
? authoritativeName
|
||||
: candidateName
|
||||
}
|
||||
}
|
||||
lastError = response.error.message
|
||||
if (!isRetryableWorktreeCreateConflict(lastError ?? '')) {
|
||||
|
||||
@@ -44,12 +44,17 @@ export function resolveWorktreeCreateDisplayNameRequest(
|
||||
input: string | undefined,
|
||||
kind: DisplayNameKind,
|
||||
fallbackName: string,
|
||||
cliCreated: boolean
|
||||
cliCreated: boolean,
|
||||
nameWasGenerated = false
|
||||
): { value: string | undefined; kind: DisplayNameKind } {
|
||||
// The CLI name is always an explicit command argument; its marker wins over a
|
||||
// missing or malformed kind so a future client cannot make it auto-managed.
|
||||
const effectiveKind = cliCreated ? 'user' : kind
|
||||
const effectiveInput = input ?? (cliCreated ? fallbackName : undefined)
|
||||
// Legacy clients omitted displayNameKind: an explicit displayName was the artifact-title
|
||||
// contract, while a name-only request was user-entered unless marked as generated.
|
||||
const effectiveKind = cliCreated
|
||||
? 'user'
|
||||
: (kind ?? (input !== undefined || nameWasGenerated ? 'generated' : 'user'))
|
||||
const effectiveInput = input ?? (effectiveKind === 'user' ? fallbackName : undefined)
|
||||
return {
|
||||
value: resolveWorktreeCreateDisplayName(effectiveInput, effectiveKind),
|
||||
kind: effectiveKind
|
||||
|
||||
@@ -145,6 +145,36 @@ describe('worktree create display-name provenance', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('recovers a legacy name-only user create without CLI provenance', () => {
|
||||
expect(resolveWorktreeCreateDisplayNameRequest(undefined, undefined, 'feature', false)).toEqual(
|
||||
{
|
||||
value: 'feature',
|
||||
kind: 'user'
|
||||
}
|
||||
)
|
||||
expect(
|
||||
resolveWorktreeCreateDisplayNameMeta('feature', 'feature', 'user', {
|
||||
requestedName: 'feature',
|
||||
sanitizedName: 'feature'
|
||||
})
|
||||
).toEqual({ displayName: 'feature', displayNameIsPinned: true })
|
||||
})
|
||||
|
||||
it('keeps a legacy generated name automatic when nameWasGenerated is set', () => {
|
||||
expect(
|
||||
resolveWorktreeCreateDisplayNameRequest(undefined, undefined, 'nautilus', false, true)
|
||||
).toEqual({
|
||||
value: undefined,
|
||||
kind: 'generated'
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps a legacy artifact display name generated when its kind is absent', () => {
|
||||
expect(
|
||||
resolveWorktreeCreateDisplayNameRequest('Issue title', undefined, 'feature', false)
|
||||
).toEqual({ value: 'Issue title', kind: 'generated' })
|
||||
})
|
||||
|
||||
it('treats a CLI name as intentional even if a caller supplies generated provenance', () => {
|
||||
expect(
|
||||
resolveWorktreeCreateDisplayNameRequest('Agent label', 'generated', 'feature', true)
|
||||
|
||||
@@ -1550,7 +1550,8 @@ export async function createRemoteWorktree(
|
||||
args.displayName,
|
||||
args.displayNameKind,
|
||||
args.name,
|
||||
args.cliProvenance?.kind === 'created-by-cli'
|
||||
args.cliProvenance?.kind === 'created-by-cli',
|
||||
args.nameWasGenerated === true
|
||||
)
|
||||
const requestedDisplayName = displayNameRequest.value
|
||||
|
||||
@@ -2030,7 +2031,8 @@ export async function createLocalWorktree(
|
||||
args.displayName,
|
||||
args.displayNameKind,
|
||||
args.name,
|
||||
args.cliProvenance?.kind === 'created-by-cli'
|
||||
args.cliProvenance?.kind === 'created-by-cli',
|
||||
args.nameWasGenerated === true
|
||||
)
|
||||
const requestedDisplayName = displayNameRequest.value
|
||||
// Why: explicit branches and non-username prefix modes never consume this; skipping the probe preserves the exact generated branch name.
|
||||
|
||||
@@ -333,6 +333,29 @@ describe('registerWorktreeHandlers', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('pins a legacy name-only user create when the branch matches', async () => {
|
||||
listWorktreesMock.mockResolvedValue([
|
||||
{
|
||||
path: '/workspace/feature',
|
||||
head: 'abc123',
|
||||
branch: 'feature',
|
||||
isBare: false,
|
||||
isMainWorktree: false
|
||||
}
|
||||
])
|
||||
store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta)
|
||||
|
||||
await handlers['worktrees:create'](null, {
|
||||
repoId: 'repo-1',
|
||||
name: 'feature'
|
||||
})
|
||||
|
||||
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
|
||||
'repo-1::/workspace/feature',
|
||||
expect.objectContaining({ displayName: 'feature', displayNameIsPinned: true })
|
||||
)
|
||||
})
|
||||
|
||||
it('persists linked issue and PR metadata during local create', async () => {
|
||||
listWorktreesMock.mockResolvedValue([
|
||||
{
|
||||
|
||||
@@ -19,7 +19,8 @@ export function createFolderWorkspace(
|
||||
args.displayName,
|
||||
args.displayNameKind,
|
||||
args.name,
|
||||
args.cliProvenance?.kind === 'created-by-cli'
|
||||
args.cliProvenance?.kind === 'created-by-cli',
|
||||
args.nameWasGenerated === true
|
||||
)
|
||||
const meta = store.setWorktreeMeta(worktreeId, {
|
||||
instanceId,
|
||||
|
||||
@@ -173,7 +173,8 @@ function buildProvisionedRootMeta(
|
||||
args.displayName,
|
||||
args.displayNameKind,
|
||||
args.name,
|
||||
false
|
||||
false,
|
||||
args.nameWasGenerated === true
|
||||
)
|
||||
const displayNameMeta = resolveWorktreeCreateDisplayNameMeta(
|
||||
displayNameRequest.value,
|
||||
|
||||
@@ -27054,7 +27054,8 @@ export class OrcaRuntimeService {
|
||||
args.displayName,
|
||||
args.displayNameKind,
|
||||
args.name,
|
||||
args.cliProvenance?.kind === 'created-by-cli'
|
||||
args.cliProvenance?.kind === 'created-by-cli',
|
||||
args.nameWasGenerated === true
|
||||
)
|
||||
const resolvedFolderDisplayName = displayNameRequest.value
|
||||
const meta = this.store.setWorktreeMeta(worktreeId, {
|
||||
@@ -27259,7 +27260,8 @@ export class OrcaRuntimeService {
|
||||
args.displayName,
|
||||
args.displayNameKind,
|
||||
args.name,
|
||||
args.cliProvenance?.kind === 'created-by-cli'
|
||||
args.cliProvenance?.kind === 'created-by-cli',
|
||||
args.nameWasGenerated === true
|
||||
)
|
||||
const requestedDisplayName = displayNameRequest.value
|
||||
const sanitizedName = sanitizeWorktreeName(args.name)
|
||||
|
||||
@@ -147,6 +147,33 @@ describe('worktree RPC methods', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps the legacy CLI name-only create shape explicit at the host boundary', async () => {
|
||||
const runtime = {
|
||||
getRuntimeId: () => 'test-runtime',
|
||||
dedupeWorktreeCreate: passthroughDedupe,
|
||||
showRepo: vi.fn().mockResolvedValue(repo),
|
||||
createManagedWorktree: vi.fn().mockResolvedValue({ worktree: { id: 'wt-cli' } })
|
||||
} as unknown as OrcaRuntimeService
|
||||
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
||||
|
||||
await dispatcher.dispatch(
|
||||
makeRequest('worktree.create', {
|
||||
repo: 'repo-1',
|
||||
name: 'feature',
|
||||
cliProvenanceRequest: {}
|
||||
})
|
||||
)
|
||||
|
||||
expect(runtime.createManagedWorktree).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'feature',
|
||||
displayName: undefined,
|
||||
displayNameKind: undefined,
|
||||
cliProvenance: expect.objectContaining({ kind: 'created-by-cli' })
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('mints automation provenance from a valid dispatch request on worktree creation', async () => {
|
||||
const dispatchToken = createAutomationDispatchToken('automation-1', 'run-1')
|
||||
const runtime = {
|
||||
|
||||
Reference in New Issue
Block a user