fix: harden scoped tools and settings persistence
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user