- Store runtime selection as { provider?, model? } layers and resolve them per
field (conversation -> project -> global); drop the legacy auto provider.
- Migrate stored selections (database v49) and clear deleted profile ids.
- Re-resolve selections in Main per request and at queue dispatch.
- Add ProjectRuntimeSelector and a two-column RuntimeModelPicker; manage
link opens model connections; restore per-runtime default model tabs.
- Stop duplicating trusted instructions and history across OpenCode,
Continue and DeepSeek Harness runtimes.
- Document the layered design, global provider removal plan and planned
purpose-specific models.
125 lines
3.4 KiB
TypeScript
125 lines
3.4 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
REMOTE_PROJECT_SAVE_LIMITS,
|
|
remoteProjectCreateDraftSchema,
|
|
remoteProjectSaveProgressSchema,
|
|
remoteProjectSaveRequestSchema,
|
|
remoteProjectUpdateDraftSchema
|
|
} from './remote-project-candidate-contracts'
|
|
|
|
const hostId = '00000000-0000-4000-8000-000000000010'
|
|
const projectId = '00000000-0000-4000-8000-000000000020'
|
|
const profileId = '00000000-0000-4000-8000-000000000040'
|
|
const draft = {
|
|
name: '远程项目',
|
|
description: 'Awaited save',
|
|
defaultWorkMode: 'ask' as const,
|
|
runtimeSelection: {
|
|
provider: 'opencode' as const,
|
|
model: { kind: 'profile' as const, profileId }
|
|
},
|
|
hostId,
|
|
remoteRootPath: '/srv/projects/goodbuddy'
|
|
}
|
|
|
|
describe('remote project save contracts', () => {
|
|
it('accepts closed create and update save requests', () => {
|
|
expect(
|
|
remoteProjectSaveRequestSchema.parse({
|
|
intent: 'create',
|
|
draft
|
|
})
|
|
).toEqual({ intent: 'create', draft })
|
|
expect(
|
|
remoteProjectSaveRequestSchema.parse({
|
|
intent: 'update',
|
|
draft: { projectId, ...draft }
|
|
})
|
|
).toEqual({ intent: 'update', draft: { projectId, ...draft } })
|
|
expect(() =>
|
|
remoteProjectCreateDraftSchema.parse({
|
|
...draft,
|
|
candidateId: projectId
|
|
})
|
|
).toThrow()
|
|
expect(() =>
|
|
remoteProjectUpdateDraftSchema.parse({
|
|
projectId,
|
|
...draft,
|
|
commitToken: 'token'
|
|
})
|
|
).toThrow()
|
|
})
|
|
|
|
it('accepts Execute without consent or trust fields', () => {
|
|
expect(
|
|
remoteProjectCreateDraftSchema.parse({
|
|
...draft,
|
|
defaultWorkMode: 'execute'
|
|
}).defaultWorkMode
|
|
).toBe('execute')
|
|
expect(() =>
|
|
remoteProjectCreateDraftSchema.parse({
|
|
...draft,
|
|
trust: {},
|
|
defaultWorkMode: 'execute'
|
|
})
|
|
).toThrow()
|
|
})
|
|
|
|
it('lets the Runtime follow global settings and bounds the absolute remote root', () => {
|
|
const withoutRuntime: Partial<typeof draft> = { ...draft }
|
|
delete withoutRuntime.runtimeSelection
|
|
expect(remoteProjectCreateDraftSchema.parse(withoutRuntime).runtimeSelection).toBeUndefined()
|
|
expect(() =>
|
|
remoteProjectCreateDraftSchema.parse({ ...draft, runtimeSelection: { provider: 'auto-ish' } })
|
|
).toThrow()
|
|
expect(() =>
|
|
remoteProjectCreateDraftSchema.parse({
|
|
...draft,
|
|
remoteRootPath: 'srv/project'
|
|
})
|
|
).toThrow()
|
|
expect(() =>
|
|
remoteProjectCreateDraftSchema.parse({
|
|
...draft,
|
|
remoteRootPath: '/srv/\nprivate'
|
|
})
|
|
).toThrow(/control characters/iu)
|
|
for (const remoteRootPath of [
|
|
'/srv//project',
|
|
'/srv/./project',
|
|
'/srv/../project'
|
|
]) {
|
|
expect(() =>
|
|
remoteProjectCreateDraftSchema.parse({
|
|
...draft,
|
|
remoteRootPath
|
|
})
|
|
).toThrow(/empty, dot, or parent segments/iu)
|
|
}
|
|
expect(() =>
|
|
remoteProjectCreateDraftSchema.parse({
|
|
...draft,
|
|
remoteRootPath: `/${'界'.repeat(
|
|
Math.floor(
|
|
REMOTE_PROJECT_SAVE_LIMITS.maximumAbsolutePathBytes / 3
|
|
) + 1
|
|
)}`
|
|
})
|
|
).toThrow(/4096 UTF-8 bytes/iu)
|
|
})
|
|
|
|
it('exposes only an ephemeral progress phase', () => {
|
|
expect(remoteProjectSaveProgressSchema.parse({ phase: 'agent' })).toEqual({
|
|
phase: 'agent'
|
|
})
|
|
expect(() =>
|
|
remoteProjectSaveProgressSchema.parse({
|
|
phase: 'agent',
|
|
candidateId: projectId
|
|
})
|
|
).toThrow()
|
|
})
|
|
})
|