mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 08:03:12 +00:00
Merge mr-p1-registry: register creationRetiredNames and type bridge operation names
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
@@ -23,6 +23,7 @@ const CREATE_LIMITS = {
|
||||
|
||||
export const MOBILE_WEB_PRODUCTION_WORKSPACE_CREATION_GRANTS = [
|
||||
{ capability: 'workspace', operation: 'creationRepositories', limits: READ_LIMITS },
|
||||
{ capability: 'workspace', operation: 'creationRetiredNames', limits: READ_LIMITS },
|
||||
{ capability: 'workspace', operation: 'creationSettings', limits: READ_LIMITS },
|
||||
{ capability: 'workspace', operation: 'creationTrustedHooks', limits: READ_LIMITS },
|
||||
{ capability: 'workspace', operation: 'creationGitLabAvailability', limits: READ_LIMITS },
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import { MOBILE_WORKTREE_CREATE_IDEMPOTENCY_CAPABILITY } from '../tasks/worktree-create-capability'
|
||||
import { webHostWorkspaceCreationOperations } from '../worktree/web-host-workspace-creation-operations'
|
||||
import { createMobileWebBridgeRoundtripFixture } from './mobile-web-bridge-roundtrip-fixture'
|
||||
import { MOBILE_WEB_PRODUCTION_WORKSPACE_CREATION_GRANTS } from './mobile-web-production-workspace-creation-grants'
|
||||
|
||||
@@ -90,4 +91,45 @@ describe('mobile web workspace creation round trip', () => {
|
||||
expect(sendRequest).not.toHaveBeenCalledWith('settings.get')
|
||||
expect(JSON.stringify(shellMessages)).not.toMatch(/repo-secret|worktree-secret|ssh-private-id/)
|
||||
})
|
||||
|
||||
it('reaches the retired-name adapter through the hosted creation operations', async () => {
|
||||
const sendRequest = vi.fn(async (method: string) => {
|
||||
if (method === 'repo.list') {
|
||||
return {
|
||||
ok: true,
|
||||
result: {
|
||||
repos: [{ id: '/host/repo-secret', displayName: 'Orca', path: '/Users/private/orca' }]
|
||||
}
|
||||
}
|
||||
}
|
||||
if (method === 'worktree.listRetiredNames') {
|
||||
return {
|
||||
ok: true,
|
||||
result: {
|
||||
retiredNamesByRepo: { '/host/repo-secret': ['nautilus'] },
|
||||
retiredNameTiersByRepo: { '/host/repo-secret': 2 }
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(`Unexpected method ${method}`)
|
||||
})
|
||||
let requestIndex = 0
|
||||
const { client: pageClient, shellMessages } = createMobileWebBridgeRoundtripFixture({
|
||||
grants: [...MOBILE_WEB_PRODUCTION_WORKSPACE_CREATION_GRANTS],
|
||||
rpcClient: { sendRequest } as unknown as RpcClient,
|
||||
createRequestId: () => String.fromCharCode(65 + requestIndex++).repeat(22)
|
||||
})
|
||||
const operations = webHostWorkspaceCreationOperations(pageClient)
|
||||
|
||||
const [repository] = await operations.listRepositories()
|
||||
|
||||
await expect(operations.readRetiredWorktreeNames(repository!.id)).resolves.toEqual({
|
||||
exhaustedTiers: 2,
|
||||
names: ['nautilus']
|
||||
})
|
||||
expect(sendRequest).toHaveBeenLastCalledWith('worktree.listRetiredNames', {
|
||||
repo: 'id:/host/repo-secret'
|
||||
})
|
||||
expect(JSON.stringify(shellMessages)).not.toContain('repo-secret')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { z } from 'zod'
|
||||
import {
|
||||
MOBILE_WEB_BRIDGE_MAX_PENDING_REQUESTS,
|
||||
type MobileWebBridgeCapability,
|
||||
type MobileWebBridgeOperationName,
|
||||
type MobileWebBridgePageMessage,
|
||||
type MobileWebBridgeShellMessage
|
||||
} from '../../shared/mobile-web/bridge-contract'
|
||||
@@ -23,7 +24,7 @@ export class MobileWebOneShotRequestClient {
|
||||
private readonly options: {
|
||||
getGrant: (
|
||||
capability: MobileWebBridgeCapability,
|
||||
operation: string
|
||||
operation: MobileWebBridgeOperationName
|
||||
) => OperationGrant | undefined
|
||||
postMessage: (message: MobileWebBridgePageMessage) => boolean
|
||||
envelope: () => Pick<MobileWebBridgePageMessage, 'version' | 'shellSessionId' | 'buildId'>
|
||||
@@ -33,9 +34,9 @@ export class MobileWebOneShotRequestClient {
|
||||
}
|
||||
) {}
|
||||
|
||||
request<TPayload, TResult>(
|
||||
capability: MobileWebBridgeCapability,
|
||||
operation: string,
|
||||
request<TCapability extends MobileWebBridgeCapability, TPayload, TResult>(
|
||||
capability: TCapability,
|
||||
operation: MobileWebBridgeOperationName<TCapability>,
|
||||
payload: TPayload,
|
||||
payloadSchema: z.ZodType<TPayload>,
|
||||
resultSchema: z.ZodType<TResult>,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ZodType } from 'zod'
|
||||
import type { MobileWebBridgeOperationName } from '../../shared/mobile-web/bridge-operation-registry'
|
||||
import {
|
||||
MobileWebTaskItemChecksPayloadSchema,
|
||||
MobileWebTaskItemChecksResultSchema,
|
||||
@@ -148,7 +149,11 @@ export class MobileWebTaskItemRequestClient extends MobileWebTaskProjectRequestC
|
||||
)
|
||||
}
|
||||
|
||||
private mutateReview(operation: string, payload: unknown, schema: ZodType) {
|
||||
private mutateReview(
|
||||
operation: MobileWebBridgeOperationName<'task'>,
|
||||
payload: unknown,
|
||||
schema: ZodType
|
||||
) {
|
||||
return this.requests.request(
|
||||
'task',
|
||||
operation,
|
||||
@@ -158,7 +163,11 @@ export class MobileWebTaskItemRequestClient extends MobileWebTaskProjectRequestC
|
||||
)
|
||||
}
|
||||
|
||||
private mutateFile(operation: string, payload: unknown, schema: ZodType) {
|
||||
private mutateFile(
|
||||
operation: MobileWebBridgeOperationName<'task'>,
|
||||
payload: unknown,
|
||||
schema: ZodType
|
||||
) {
|
||||
return this.requests.request(
|
||||
'task',
|
||||
operation,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { MobileWebBridgeOperationName } from '../../shared/mobile-web/bridge-operation-registry'
|
||||
import {
|
||||
MobileWebTaskLinearCommentPayloadSchema,
|
||||
MobileWebTaskLinearCommentResultSchema,
|
||||
@@ -122,7 +123,11 @@ export class MobileWebTaskLinearRequestClient extends MobileWebTaskItemRequestCl
|
||||
)
|
||||
}
|
||||
|
||||
private mutateLinear(operation: string, payload: unknown, schema: ZodType) {
|
||||
private mutateLinear(
|
||||
operation: MobileWebBridgeOperationName<'task'>,
|
||||
payload: unknown,
|
||||
schema: ZodType
|
||||
) {
|
||||
return mutate(schema, this.requests, operation, payload)
|
||||
}
|
||||
}
|
||||
@@ -130,7 +135,7 @@ export class MobileWebTaskLinearRequestClient extends MobileWebTaskItemRequestCl
|
||||
function mutate(
|
||||
schema: ZodType,
|
||||
requests: MobileWebOneShotRequestClient,
|
||||
operation: string,
|
||||
operation: MobileWebBridgeOperationName<'task'>,
|
||||
payload: unknown
|
||||
) {
|
||||
return requests.request(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { MobileWebBridgeOperationName } from '../../shared/mobile-web/bridge-operation-registry'
|
||||
import {
|
||||
MobileWebTaskProjectAssignableUsersPayloadSchema,
|
||||
MobileWebTaskProjectAssignableUsersResultSchema,
|
||||
@@ -266,7 +267,11 @@ export class MobileWebTaskProjectRequestClient {
|
||||
return this.mutate('mergeProjectPullRequest', payload, MobileWebTaskProjectMergePayloadSchema)
|
||||
}
|
||||
|
||||
private mutate(operation: string, payload: unknown, payloadSchema: ZodType) {
|
||||
private mutate(
|
||||
operation: MobileWebBridgeOperationName<'task'>,
|
||||
payload: unknown,
|
||||
payloadSchema: ZodType
|
||||
) {
|
||||
return this.requests.request(
|
||||
'task',
|
||||
operation,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { MobileWebBridgeOperationName } from '../../shared/mobile-web/bridge-operation-registry'
|
||||
import {
|
||||
MobileWebCreationAgentDetectionPayloadSchema,
|
||||
MobileWebCreationAgentDetectionResultSchema,
|
||||
@@ -171,7 +172,7 @@ export class MobileWebWorkspaceCreationRequestClient {
|
||||
)
|
||||
}
|
||||
|
||||
private availability(operation: string): Promise<boolean> {
|
||||
private availability(operation: MobileWebBridgeOperationName<'workspace'>): Promise<boolean> {
|
||||
return this.emptyRequest<{ available: boolean }>(
|
||||
operation,
|
||||
MobileWebCreationAvailabilityPayloadSchema,
|
||||
@@ -180,7 +181,7 @@ export class MobileWebWorkspaceCreationRequestClient {
|
||||
}
|
||||
|
||||
private repoRequest<TResult>(
|
||||
operation: string,
|
||||
operation: MobileWebBridgeOperationName<'workspace'>,
|
||||
payload: MobileWebCreationRepoPayload,
|
||||
resultSchema: Parameters<MobileWebOneShotRequestClient['request']>[4]
|
||||
): Promise<TResult> {
|
||||
@@ -194,7 +195,7 @@ export class MobileWebWorkspaceCreationRequestClient {
|
||||
}
|
||||
|
||||
private emptyRequest<TResult>(
|
||||
operation: string,
|
||||
operation: MobileWebBridgeOperationName<'workspace'>,
|
||||
payloadSchema: Parameters<MobileWebOneShotRequestClient['request']>[3],
|
||||
resultSchema: Parameters<MobileWebOneShotRequestClient['request']>[4]
|
||||
): Promise<TResult> {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { z } from 'zod'
|
||||
import type { MobileWebBridgeOperationName } from '../../shared/mobile-web/bridge-operation-registry'
|
||||
import {
|
||||
MobileWebCreationBranchSearchResultSchema,
|
||||
MobileWebCreationGitHubLookupPayloadSchema,
|
||||
@@ -155,7 +156,7 @@ export class MobileWebWorkspaceCreationSourceRequestClient {
|
||||
}
|
||||
|
||||
private request<TPayload, TResult>(
|
||||
operation: string,
|
||||
operation: MobileWebBridgeOperationName<'workspace'>,
|
||||
payload: TPayload,
|
||||
payloadSchema: z.ZodType<TPayload>,
|
||||
resultSchema: z.ZodType<TResult>
|
||||
|
||||
@@ -23,7 +23,10 @@ export {
|
||||
isMobileWebBridgeOperation,
|
||||
MOBILE_WEB_BRIDGE_OPERATIONS
|
||||
} from './bridge-operation-registry'
|
||||
export type { MobileWebBridgeCapability } from './bridge-operation-registry'
|
||||
export type {
|
||||
MobileWebBridgeCapability,
|
||||
MobileWebBridgeOperationName
|
||||
} from './bridge-operation-registry'
|
||||
export {
|
||||
MOBILE_WEB_BRIDGE_ENVELOPE_RESERVE_BYTES,
|
||||
MOBILE_WEB_BRIDGE_MAX_GRANTS,
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { readFileSync, readdirSync } from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { MOBILE_WEB_BRIDGE_OPERATIONS } from './bridge-operation-registry'
|
||||
|
||||
const REPO_ROOT = resolve(__dirname, '..', '..', '..')
|
||||
const PAGE_DIR = join(REPO_ROOT, 'src', 'mobile-web', 'src')
|
||||
const SHELL_DIR = join(REPO_ROOT, 'mobile', 'src', 'mobile-web')
|
||||
|
||||
const registered = new Set(
|
||||
Object.entries(MOBILE_WEB_BRIDGE_OPERATIONS).flatMap(([capability, operations]) =>
|
||||
operations.map((operation) => `${capability}.${operation}`)
|
||||
)
|
||||
)
|
||||
|
||||
function sources(dir: string, keep: (name: string) => boolean): { name: string; text: string }[] {
|
||||
return readdirSync(dir)
|
||||
.filter((name) => name.endsWith('.ts') && !name.includes('.test.') && keep(name))
|
||||
.map((name) => ({ name, text: readFileSync(join(dir, name), 'utf8') }))
|
||||
}
|
||||
|
||||
describe('mobile web bridge operation registry census', () => {
|
||||
it('registers every capability and operation pair a page request client names', () => {
|
||||
const files = sources(PAGE_DIR, () => true)
|
||||
const named = new Set<string>()
|
||||
for (const { text } of files) {
|
||||
for (const match of text.matchAll(/\.request\(\s*'([A-Za-z]+)',\s*'([A-Za-z]+)'/g)) {
|
||||
named.add(`${match[1]}.${match[2]}`)
|
||||
}
|
||||
}
|
||||
|
||||
expect(files.length).toBeGreaterThanOrEqual(40)
|
||||
expect(named.size).toBeGreaterThanOrEqual(150)
|
||||
expect([...named].filter((pair) => !registered.has(pair))).toEqual([])
|
||||
})
|
||||
|
||||
// A bare `string` here is how `workspace.creationRetiredNames` shipped unregistered: the helper
|
||||
// erased the operation name before the compiler could check it against the registry.
|
||||
it('types every page request client operation parameter against the registry', () => {
|
||||
const files = sources(PAGE_DIR, (name) => name.endsWith('-request-client.ts'))
|
||||
const untyped = files
|
||||
.filter(({ text }) => /\boperation\??:\s*string\b/.test(text))
|
||||
.map(({ name }) => name)
|
||||
|
||||
expect(files.map(({ name }) => name)).toContain('mobile-web-one-shot-request-client.ts')
|
||||
expect(files.length).toBeGreaterThanOrEqual(20)
|
||||
expect(untyped).toEqual([])
|
||||
})
|
||||
|
||||
it('names every registered operation in a shell module outside the grant tables', () => {
|
||||
const files = sources(SHELL_DIR, (name) => !name.startsWith('mobile-web-production-'))
|
||||
const named = new Set<string>()
|
||||
for (const { text } of files) {
|
||||
for (const match of text.matchAll(/'([A-Za-z][A-Za-z0-9]*)'/g)) {
|
||||
named.add(match[1]!)
|
||||
}
|
||||
}
|
||||
const undispatched = [...registered].filter(
|
||||
(pair) => !named.has(pair.slice(pair.indexOf('.') + 1))
|
||||
)
|
||||
|
||||
expect(files.length).toBeGreaterThanOrEqual(100)
|
||||
expect(undispatched).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -9,6 +9,7 @@ export const MOBILE_WEB_BRIDGE_OPERATIONS = {
|
||||
'update',
|
||||
'remove',
|
||||
'creationRepositories',
|
||||
'creationRetiredNames',
|
||||
'creationSettings',
|
||||
'creationTrustedHooks',
|
||||
'creationGitLabAvailability',
|
||||
@@ -252,6 +253,12 @@ export const MobileWebBridgeCapabilitySchema = z.enum([
|
||||
|
||||
export type MobileWebBridgeCapability = z.infer<typeof MobileWebBridgeCapabilitySchema>
|
||||
|
||||
/** Operation names a page may request for one capability. Keeps request clients from naming an
|
||||
* operation the shell never granted. */
|
||||
export type MobileWebBridgeOperationName<
|
||||
TCapability extends MobileWebBridgeCapability = MobileWebBridgeCapability
|
||||
> = (typeof MOBILE_WEB_BRIDGE_OPERATIONS)[TCapability][number]
|
||||
|
||||
export function isMobileWebBridgeOperation(
|
||||
capability: MobileWebBridgeCapability,
|
||||
operation: string
|
||||
|
||||
Reference in New Issue
Block a user