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
44 changes: 34 additions & 10 deletions server/aws-lsp-codewhisperer/src/shared/amazonQServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
AMAZON_Q_SERVICE_SERVER_IAM_NAME,
AMAZON_Q_SERVICE_SERVER_TOKEN_NAME,
AmazonQServiceServerFactory,
AmazonQServiceServerIAM,
AmazonQServiceServerToken,
} from './amazonQServer'
import { BaseAmazonQServiceManager } from './amazonQServiceManager/BaseAmazonQServiceManager'

Expand Down Expand Up @@ -63,17 +65,39 @@ describe('AmazonQServiceServer', () => {
expect(result.serverInfo?.name).to.equal(TEST_SERVER_NAME)
})

it('gives the IAM and token servers distinct serverInfo names', () => {
// Regression guard. Runtimes such as agent-standalone register BOTH of these servers, and the
// runtime rejects initialize with `Duplicate servers defined` when two servers report the same
// name -- which fails the entire language server, not just the duplicate. A shared name here
// made every such runtime fall back to whatever server the client had bundled, visible only as
// a client-side warning, so Q kept working while silently running a different server.
const names = [AMAZON_Q_SERVICE_SERVER_IAM_NAME, AMAZON_Q_SERVICE_SERVER_TOKEN_NAME]

for (const name of names) {
expect(name).to.be.a('string').and.not.empty
it('gives the IAM and token servers distinct serverInfo names', async () => {
// Regression guard, asserted against the real exported servers rather than the constants, so
// it also catches the same name being passed to both factory calls.
//
// Runtimes such as agent-standalone register BOTH of these servers, and the runtime rejects
// initialize with `Duplicate servers defined` when two servers report the same name -- which
// fails the entire language server, not just the duplicate. A shared name here made every such
// runtime fall back to whatever server the client had bundled, visible only as a client-side
// warning, so Q kept working while silently running a different server.
const names: (string | undefined)[] = []

for (const qServer of [AmazonQServiceServerIAM, AmazonQServiceServerToken]) {
const serverFeatures = new TestFeatures()
try {
// The service managers refuse to initialize before the LSP connection has, so the
// client params have to be in place before the initializer runs.
serverFeatures.setClientParams({} as InitializeParams)
qServer(serverFeatures)

const initializer = serverFeatures.lsp.addInitializer.args[0]?.[0]
const result = (await initializer(
{} as InitializeParams,
{} as CancellationToken
)) as PartialInitializeResult

names.push(result.serverInfo?.name)
} finally {
serverFeatures.dispose()
TestAmazonQServiceManager.resetInstance()
}
}

expect(names).to.deep.equal([AMAZON_Q_SERVICE_SERVER_IAM_NAME, AMAZON_Q_SERVICE_SERVER_TOKEN_NAME])
expect(new Set(names).size, `server names must be unique: ${names.join(', ')}`).to.equal(names.length)
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,32 @@ describe('StreamingClientServiceToken', () => {
expect(streamingClientServiceDefault['shareCodeWhispererContentWithAWS']).to.be.undefined
})

describe('access-blocked observer', () => {
it('registers exactly one named middleware on the initialize step', () => {
// Guards the wiring rather than the callback: the harness stubs
// CodeWhispererStreaming.prototype.sendMessage, which bypasses the middleware stack, so a
// behavioural test here would pass without the middleware existing at all.
//
// The name matters. Without it a second registration stacks another observer instead of
// replacing the first, which would report the same block twice.
const registered = streamingClientService.client.middlewareStack
.identify()
.filter(entry => entry.includes('detectQDevPluginAccessBlocked'))

expect(registered).to.have.lengthOf(1)
expect(registered[0]).to.contain('initialize')
})

it('exposes a settable observer for the service manager to assign', () => {
// Assigned after construction, so the middleware has to read it at call time. If this
// became readonly or were dropped, chat-time blocks would go unobserved.
const observer = () => {}
streamingClientService.onAccessBlocked = observer

expect(streamingClientService.onAccessBlocked).to.equal(observer)
})
})

describe('generateAssistantResponse', () => {
const MOCKED_GENERATE_RESPONSE_REQUEST = {
conversationState: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ export class StreamingClientServiceToken extends StreamingClientServiceBase {
},
{
step: 'initialize',
// Named so a second registration replaces this one rather than stacking another
// observer, and so the middleware is identifiable in SDK stack introspection. Matches
// the token client's registration.
name: 'detectQDevPluginAccessBlocked',
}
)
}
Expand Down
Loading