Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions harness-tests/core/conversation-http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,38 @@ it('rejects ambiguous workspace selectors without accessing storage', async () =
expect(mocks.resolveIdentity).not.toHaveBeenCalled()
})

it.each(['signed-out', 'locked'] as const)(
'rejects %s sends before looking up the conversation object',
async (access) => {
mocks.currentUser.mockResolvedValue(
access === 'signed-out' ? null : { userId: 'user', capabilities: [] },
)
const req = new Request(request().url, {
method: 'POST',
headers: { Origin: 'https://tanstack.com' },
body: JSON.stringify({ text: 'hello', messageId: 'message' }),
})
expect((await handleConversationSend(req, 'c', 'send')).status).toBe(
access === 'signed-out' ? 401 : 403,
)
expect(mocks.resolveIdentity).not.toHaveBeenCalled()
expect(mocks.getByName).not.toHaveBeenCalled()
expect(mocks.begin).not.toHaveBeenCalled()
},
)
it('rejects sends whose ownership check fails before accessing the object', async () => {
mocks.resolveIdentity.mockRejectedValue(new Error('authorization failed'))
const req = new Request(request().url, {
method: 'POST',
headers: { Origin: 'https://tanstack.com' },
body: JSON.stringify({ text: 'hello', messageId: 'message' }),
})
await expect(handleConversationSend(req, 'c', 'send')).rejects.toThrow(
'authorization failed',
)
expect(mocks.getByName).not.toHaveBeenCalled()
expect(mocks.begin).not.toHaveBeenCalled()
})
it('denies cross-origin submission before looking up the account', async () => {
const req = new Request(request().url, {
method: 'POST',
Expand Down Expand Up @@ -201,6 +233,7 @@ it('uses server identity and context rather than request-supplied values', async
}),
})
expect((await handleConversationSend(req, 'c', 'send')).status).toBe(200)
expect(mocks.bind).not.toHaveBeenCalled()
expect(mocks.begin.mock.calls[0][0]).toMatchObject({
userId: 'user',
fixture: false,
Expand Down
20 changes: 19 additions & 1 deletion harness-tests/core/conversation-location.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { describe, expect, it } from 'vitest'
import { conversationLocation } from '../../src/chat/core/conversation-destination'
import {
conversationLocation,
selectedConversationLocation,
} from '../../src/chat/core/conversation-destination'
import { fileViewPath } from '../../src/chat/core/files'
import {
signInDestination,
Expand Down Expand Up @@ -66,3 +69,18 @@ describe('canonical conversation links', () => {
expect(signInDestination('https://evil.example/chat/c/room')).toBe('/')
})
})

it('selects another conversation without carrying the previous message path', () => {
const result = selectedConversationLocation(
{ workspaceId: 'one', botId: 'other', conversationId: 'other-room' },
validateWorkspaceSearch({
message: 'previous-message',
panel: 'usage',
details: true,
}),
)
expect(result.to).toBe('/chat/c/$conversationId')
expect(result.params).toEqual({ conversationId: 'other-room' })
expect(result.search).not.toHaveProperty('message')
expect(result.search.panel).toBe('usage')
})
165 changes: 165 additions & 0 deletions harness-tests/core/mcp-connections-preparation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { beforeEach, expect, it, vi } from 'vitest'
import { defaultPolicy } from '../../src/chat/core/types'

const mocks = vi.hoisted(() => ({
policy: vi.fn(),
credentials: vi.fn(),
accounts: vi.fn(),
plugins: vi.fn(),
runtimeAccounts: vi.fn(),
kody: vi.fn(),
}))
vi.mock('../../src/chat/workspace-policy.server', () => ({
readWorkspacePolicy: mocks.policy,
}))
vi.mock('../../src/chat/server/credentials', () => ({
readCredentials: mocks.credentials,
}))
vi.mock('../../src/chat/server/mcp-accounts', () => ({
McpAccounts: class {
configuredServers = mocks.accounts
},
}))
vi.mock('../../src/chat/server/plugin-connections', () => ({
pluginMcpConnections: mocks.plugins,
}))
vi.mock('../../src/chat/server/mcp-account-runtime', () => ({
runtimeMcpAccounts: mocks.runtimeAccounts,
}))
vi.mock('../../src/chat/server/kody', () => ({ kodyConnection: mocks.kody }))
import { connectedMcpServers } from '../../src/chat/server/mcp-connections'

const env = {
ENCRYPTION_KEY: 'synthetic-fixture',
KODY_ORIGIN: 'https://example.test',
}
const scope = { workspaceId: 'fixture' }
beforeEach(() => {
vi.resetAllMocks()
mocks.policy.mockResolvedValue(defaultPolicy)
mocks.credentials.mockResolvedValue(null)
mocks.accounts.mockResolvedValue([])
mocks.plugins.mockResolvedValue([])
mocks.runtimeAccounts.mockResolvedValue([])
})

it('lists authorized accounts while the independent credential read is pending', async () => {
let release = () => {}
const gate = new Promise<null>((resolve) => {
release = () => resolve(null)
})
mocks.credentials.mockReturnValue(gate)
const result = connectedMcpServers(
env,
'fixture-user',
defaultPolicy,
undefined,
scope,
)
try {
await vi.waitFor(() => expect(mocks.accounts).toHaveBeenCalledOnce())
release()
await expect(result).resolves.toEqual([])
} finally {
release()
await result
}
})

it('does not read credentials or accounts when workspace authorization fails', async () => {
mocks.policy.mockRejectedValue(new Error('Synthetic revoked membership'))
await expect(
connectedMcpServers(env, 'fixture-user', defaultPolicy, undefined, scope),
).resolves.toEqual([])
expect(mocks.credentials).not.toHaveBeenCalled()
expect(mocks.accounts).not.toHaveBeenCalled()
})

it('does not read Kody credentials when that capability is disabled', async () => {
await expect(
connectedMcpServers(
env,
'fixture-user',
{ ...defaultPolicy, allowKody: false },
undefined,
scope,
),
).resolves.toEqual([])
expect(mocks.credentials).not.toHaveBeenCalled()
expect(mocks.accounts).toHaveBeenCalledOnce()
})

it('settles the account read before reporting a credential failure', async () => {
let release = () => {}
const gate = new Promise<[]>((resolve) => {
release = () => resolve([])
})
mocks.accounts.mockReturnValue(gate)
mocks.credentials.mockRejectedValue(new Error('Synthetic credential failure'))
let settled = false
const result = connectedMcpServers(
env,
'fixture-user',
defaultPolicy,
undefined,
scope,
)
const observed = result.then(
() => {
settled = true
},
() => {
settled = true
},
)
try {
await vi.waitFor(() => expect(mocks.accounts).toHaveBeenCalledOnce())
expect(settled).toBe(false)
release()
await expect(result).rejects.toThrow('Synthetic credential failure')
} finally {
release()
await observed
}
})

it('resolves a selected Kody connection with fresh runtime authorization', async () => {
mocks.credentials.mockResolvedValue({
kody: { access_token: 'metadata-token' },
})
mocks.kody.mockResolvedValue({
id: 'kody',
label: 'Kody',
url: 'https://example.test/mcp',
accessToken: 'fresh-runtime-token',
})
const result = await connectedMcpServers(
env,
'fixture-user',
defaultPolicy,
'kody',
scope,
)
expect(result).toHaveLength(1)
expect(result[0].accessToken).toBe('fresh-runtime-token')
expect(mocks.kody).toHaveBeenCalledWith(env, 'fixture-user')
expect(mocks.accounts).not.toHaveBeenCalled()
})

it('does not read unrelated Kody credentials when resolving a selected MCP account', async () => {
await expect(
connectedMcpServers(
env,
'fixture-user',
defaultPolicy,
'mcp:fixture-account',
scope,
),
).resolves.toEqual([])
expect(mocks.credentials).not.toHaveBeenCalled()
expect(mocks.runtimeAccounts).toHaveBeenCalledWith(
env,
{ workspaceId: 'fixture', userId: 'fixture-user' },
{ id: 'fixture-account' },
)
})
27 changes: 27 additions & 0 deletions harness-tests/core/run-models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,33 @@ beforeEach(() => {
})

describe('run model configuration', () => {
it('reuses credentials already read for this run without a second database query', async () => {
configured('openai', 'gpt-5-mini', { apiKey: 'fresh-server-key' })
const result = await resolveRunModel(env, scope, state.credentials)
expect(result.connection.apiKey).toBe('fresh-server-key')
expect(result.selection.provider).toBe('openai')
expect(state.read).not.toHaveBeenCalled()
})

it('treats an already-read missing credential row as included without rereading', async () => {
configured('openai', 'gpt-5-mini')
const result = await resolveRunModel(env, scope, null)
expect(result.selection.provider).toBe('included')
expect(state.read).not.toHaveBeenCalled()
})

it('keeps fixture isolation when a caller supplies personal credentials', async () => {
configured('openai', 'gpt-5-mini')
const result = await resolveRunModel(
env,
{ ...scope, fixture: true },
state.credentials,
)
expect(result.selection.provider).toBe('included')
expect(result.connection.apiKey).toBeUndefined()
expect(state.read).not.toHaveBeenCalled()
})

it('uses the configured included model and preserves its existing default behavior', async () => {
const result = await resolveRunModel(env, scope)
expect(result.selection).toEqual(select('included', env.INCLUDED_MODEL))
Expand Down
Loading
Loading