From dc7b49b48d15ff3b94ebb8180fe5023a4787ca04 Mon Sep 17 00:00:00 2001 From: VascoSch92 Date: Tue, 26 May 2026 14:38:06 +0200 Subject: [PATCH 1/2] test(profiles): add integration coverage for ProfilesClient and switchLlm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the integration tests tracked in #149 (follow-up to #148, which shipped mocked unit tests only): - Profiles CRUD round-trip (save → get → list → activate → rename → delete) via manager.profiles against a live agent-server. - A switchLlm happy-path assertion next to the existing switchProfile 404 check. Both live in the deterministic suite so they run in CI's deterministic integration job without an LLM key. --- .../deterministic-api.integration.test.ts | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/__tests__/integration/deterministic-api.integration.test.ts b/src/__tests__/integration/deterministic-api.integration.test.ts index c15a5cf..bec0a26 100644 --- a/src/__tests__/integration/deterministic-api.integration.test.ts +++ b/src/__tests__/integration/deterministic-api.integration.test.ts @@ -4,6 +4,7 @@ import { deleteWorkspaceFile, readWorkspaceFile, sleep, + uniqueDirName, uniqueFileName, workspaceFileExists, } from './test-utils'; @@ -132,6 +133,12 @@ describe('Deterministic API Integration Tests', () => { conversation.switchProfile('__profile_that_should_not_exist__') ).rejects.toBeInstanceOf(HttpError); + // switchLlm swaps the LLM object directly (no profile on disk), so it + // resolves where the missing-profile switchProfile above rejects. + await expect( + conversation.switchLlm({ model: 'dummy/model', api_key: 'dummy-key' }) + ).resolves.toBeUndefined(); + const acpConversation = await manager.acp.createConversation( { kind: 'Agent', @@ -215,4 +222,53 @@ describe('Deterministic API Integration Tests', () => { }, config.testTimeout ); + + it( + 'round-trips a profile through save, get, list, activate, rename, and delete', + async () => { + const profileName = uniqueDirName('it-profile'); + const renamedProfile = `${profileName}-renamed`; + const llm = { model: 'dummy/model', api_key: 'dummy-key' }; + + try { + const saved = await manager.profiles.saveProfile(profileName, { llm }); + expect(saved.name).toBe(profileName); + + const detail = await manager.profiles.getProfile(profileName); + expect(detail.name).toBe(profileName); + // The default (no X-Expose-Secrets) response nulls api_key and reports + // the presence of the saved key via api_key_set instead. + expect(detail.config.api_key).toBeNull(); + expect(detail.api_key_set).toBe(true); + + const list = await manager.profiles.listProfiles(); + expect(list.profiles.some((profile) => profile.name === profileName)).toBe(true); + + const activated = await manager.profiles.activateProfile(profileName); + expect(activated.name).toBe(profileName); + expect(activated.llm_applied).toBe(true); + + const afterActivate = await manager.profiles.listProfiles(); + expect(afterActivate.active_profile).toBe(profileName); + + const renamed = await manager.profiles.renameProfile(profileName, renamedProfile); + expect(renamed.name).toBe(renamedProfile); + + const renamedDetail = await manager.profiles.getProfile(renamedProfile); + expect(renamedDetail.name).toBe(renamedProfile); + // The original name stops resolving once the profile is renamed. + await expect(manager.profiles.getProfile(profileName)).rejects.toBeInstanceOf(HttpError); + + const deleted = await manager.profiles.deleteProfile(renamedProfile); + expect(deleted.name).toBe(renamedProfile); + + const finalList = await manager.profiles.listProfiles(); + expect(finalList.profiles.some((profile) => profile.name === renamedProfile)).toBe(false); + } finally { + await manager.profiles.deleteProfile(profileName).catch(() => undefined); + await manager.profiles.deleteProfile(renamedProfile).catch(() => undefined); + } + }, + config.testTimeout + ); }); From 8d055e8df983c78db605c857202328bffa8d7d03 Mon Sep 17 00:00:00 2001 From: VascoSch92 Date: Tue, 26 May 2026 18:17:07 +0200 Subject: [PATCH 2/2] test(profiles): verify switchLlm swap takes effect and tighten CRUD assertions - switchLlm: switch to a fresh usage_id and read the model back off the agent (state.getAgent), proving the swap actually happened. Reusing the agent's existing usage_id is a silent registry no-op, so the prior resolves-only check verified nothing. - profiles CRUD: assert the saved LLM config (model) round-trips on get and survives the (atomic) rename, and that active_profile follows the active profile to its new name on rename. --- .../deterministic-api.integration.test.ts | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/__tests__/integration/deterministic-api.integration.test.ts b/src/__tests__/integration/deterministic-api.integration.test.ts index bec0a26..f531b4f 100644 --- a/src/__tests__/integration/deterministic-api.integration.test.ts +++ b/src/__tests__/integration/deterministic-api.integration.test.ts @@ -133,11 +133,17 @@ describe('Deterministic API Integration Tests', () => { conversation.switchProfile('__profile_that_should_not_exist__') ).rejects.toBeInstanceOf(HttpError); - // switchLlm swaps the LLM object directly (no profile on disk), so it - // resolves where the missing-profile switchProfile above rejects. - await expect( - conversation.switchLlm({ model: 'dummy/model', api_key: 'dummy-key' }) - ).resolves.toBeUndefined(); + // switchLlm swaps the LLM in place, keyed by usage_id (first-write-wins + // in the registry). Reusing the agent's existing usage_id would be a + // silent no-op, so use a fresh one and read the model back off the + // agent to prove the swap actually took effect. + await conversation.switchLlm({ + model: 'dummy/switched-model', + api_key: 'dummy-key', + usage_id: 'switched', + }); + const switchedAgent = await conversation.state.getAgent(); + expect(switchedAgent.llm.model).toBe('dummy/switched-model'); const acpConversation = await manager.acp.createConversation( { @@ -236,8 +242,10 @@ describe('Deterministic API Integration Tests', () => { const detail = await manager.profiles.getProfile(profileName); expect(detail.name).toBe(profileName); - // The default (no X-Expose-Secrets) response nulls api_key and reports - // the presence of the saved key via api_key_set instead. + // The saved LLM config round-trips... + expect(detail.config.model).toBe('dummy/model'); + // ...but the default (no X-Expose-Secrets) response nulls api_key and + // reports the presence of the saved key via api_key_set instead. expect(detail.config.api_key).toBeNull(); expect(detail.api_key_set).toBe(true); @@ -256,6 +264,12 @@ describe('Deterministic API Integration Tests', () => { const renamedDetail = await manager.profiles.getProfile(renamedProfile); expect(renamedDetail.name).toBe(renamedProfile); + // The config survives the (atomic) rename... + expect(renamedDetail.config.model).toBe('dummy/model'); + // ...and because the renamed profile was the active one, the + // active_profile pointer follows it to the new name. + const afterRename = await manager.profiles.listProfiles(); + expect(afterRename.active_profile).toBe(renamedProfile); // The original name stops resolving once the profile is renamed. await expect(manager.profiles.getProfile(profileName)).rejects.toBeInstanceOf(HttpError);