fix: harden scoped tools and settings persistence

This commit is contained in:
lofyer
2026-08-13 14:56:53 +08:00
parent bf1ec5d2f1
commit aab961226f
62 changed files with 4343 additions and 1314 deletions
@@ -1386,7 +1386,7 @@ describe('AssistantDatabase', () => {
database.close()
})
it('rebinds persisted conversations whose model profile was removed', async () => {
it('repairs unattended channel selections without rebinding ordinary conversations', async () => {
const database = await createDatabase()
const removedProfileId =
'00000000-0000-4000-8000-000000000291'
@@ -1478,7 +1478,7 @@ describe('AssistantDatabase', () => {
},
continueModelSource: { kind: 'platform' }
})
).toBe(7)
).toBe(4)
expect(
database
.listConversations()
@@ -1486,9 +1486,9 @@ describe('AssistantDatabase', () => {
.sort((left, right) => left.title.localeCompare(right.title))
.map((conversation) => conversation.runtimeSelection)
).toEqual([
{ provider: 'model', profileId: defaultProfileId },
{ provider: 'opencode', profileId: runtimeProfileId },
{ provider: 'continue' },
{ provider: 'model', profileId: removedProfileId },
{ provider: 'opencode', profileId: removedProfileId },
{ provider: 'continue', profileId: removedProfileId },
{ provider: 'model', profileId: runtimeProfileId }
])
expect(database.getProject(channelProject.id).runtimeSelection).toEqual({
+3 -5
View File
@@ -42,7 +42,6 @@ import {
import {
agentRuntimeSelectionKey,
agentRuntimeSelectionSchema,
repairAgentRuntimeSelection,
repairChannelRuntimeSelection,
type AgentRuntimeSelection,
type RuntimeSelectionRepairSettings
@@ -1363,7 +1362,8 @@ export class AssistantDatabase {
.prepare(
`SELECT id, runtime_selection_json, channel
FROM conversations
WHERE runtime_selection_json IS NOT NULL`
WHERE runtime_selection_json IS NOT NULL
AND channel IS NOT NULL`
)
.all() as Array<{
id: string
@@ -1410,9 +1410,7 @@ export class AssistantDatabase {
if (!current) {
continue
}
const next = conversation.channel
? repairChannelRuntimeSelection(current, settings)
: repairAgentRuntimeSelection(current, settings)
const next = repairChannelRuntimeSelection(current, settings)
if (
agentRuntimeSelectionKey(next) ===
agentRuntimeSelectionKey(current)
@@ -80,6 +80,32 @@ describe('RemoteDelegationService', () => {
).toHaveLength(2)
})
it('shares one in-flight poll between concurrent callers', async () => {
let releaseTransport!: () => void
const transportReleased = new Promise<void>((resolve) => {
releaseTransport = resolve
})
const transport = vi.fn(async () => {
await transportReleased
return { status: 204, body: '' }
})
const service = new RemoteDelegationService({
endpoint: 'https://delegate.example',
token: 'test-token',
lookup: async () => [{ address: '1.1.1.1', family: 4 }],
transport,
onTask: vi.fn()
})
const first = service.pollOnce()
const second = service.pollOnce()
await vi.waitFor(() => expect(transport).toHaveBeenCalledOnce())
releaseTransport()
await Promise.all([first, second])
expect(transport).toHaveBeenCalledOnce()
})
it('drains a durable outbox before accepting another task', async () => {
const records = new Map<
string,
@@ -157,7 +183,7 @@ describe('RemoteDelegationService', () => {
const polling = service.pollOnce()
await vi.waitFor(() => expect(observedSignal).toBeDefined())
service.stop()
await service.stop()
await expect(polling).rejects.toBeDefined()
expect(observedSignal?.aborted).toBe(true)
@@ -157,7 +157,7 @@ export class RemoteDelegationService {
private readonly pendingResults = new Map<string, RemoteResult>()
private interval?: NodeJS.Timeout
private activeRequest?: AbortController
private polling = false
private activePoll?: Promise<void>
constructor(private readonly options: RemoteDelegationOptions) {
this.endpoint = normalizeEndpoint(options.endpoint)
@@ -179,19 +179,29 @@ export class RemoteDelegationService {
void this.pollOnce().catch(() => undefined)
}
stop(): void {
async stop(): Promise<void> {
if (this.interval) {
clearInterval(this.interval)
this.interval = undefined
}
this.activeRequest?.abort()
await this.activePoll?.catch(() => undefined)
}
async pollOnce(): Promise<void> {
if (this.polling) {
return
pollOnce(): Promise<void> {
if (this.activePoll) {
return this.activePoll
}
this.polling = true
const operation = this.performPoll()
this.activePoll = operation
return operation.finally(() => {
if (this.activePoll === operation) {
this.activePoll = undefined
}
})
}
private async performPoll(): Promise<void> {
const controller = new AbortController()
this.activeRequest = controller
try {
@@ -260,7 +270,6 @@ export class RemoteDelegationService {
if (this.activeRequest === controller) {
this.activeRequest = undefined
}
this.polling = false
}
}