Skip to content

Commit e6ee11b

Browse files
committed
fix: tests, lint, workflow model name logic
1 parent 779ad18 commit e6ee11b

12 files changed

+124
-27
lines changed

extensions/cli/src/args.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { vi } from "vitest";
2+
23
import { processRule as processPromptOrRule } from "./hubLoader.js";
34
describe("processPromptOrRule (loadRuleFromHub integration)", () => {
45
// Mock fetch for hub tests

extensions/cli/src/configEnhancer.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ vi.mock("./hubLoader.js", () => ({
1313
// Return as-is for direct content
1414
return Promise.resolve(rule);
1515
}),
16+
loadPackageFromHub: vi.fn(() =>
17+
Promise.resolve({ name: "test", provider: "test" }),
18+
),
1619
loadPackagesFromHub: vi.fn(() => Promise.resolve([])),
1720
mcpProcessor: {},
1821
modelProcessor: {},
@@ -23,6 +26,8 @@ vi.mock("./services/ServiceContainer.js", () => ({
2326
serviceContainer: {
2427
get: vi.fn(() =>
2528
Promise.resolve({
29+
workflowService: null,
30+
workflowModelName: null,
2631
workflowFile: null,
2732
workflow: null,
2833
}),

extensions/cli/src/configEnhancer.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66

77
import { BaseCommandOptions } from "./commands/BaseCommandOptions.js";
88
import {
9+
loadPackageFromHub,
910
loadPackagesFromHub,
1011
mcpProcessor,
1112
modelProcessor,
@@ -27,12 +28,13 @@ export class ConfigEnhancer {
2728
_options: BaseCommandOptions,
2829
): Promise<AssistantUnrolled> {
2930
let enhancedConfig = { ...config };
31+
const options = { ..._options };
3032

3133
// Add workflow rules/mcp servers if present
32-
const options = { ..._options };
33-
const { workflowFile } = await serviceContainer.get<WorkflowServiceState>(
34+
const workflowState = await serviceContainer.get<WorkflowServiceState>(
3435
SERVICE_NAMES.WORKFLOW,
3536
);
37+
const { workflowFile, workflowService } = workflowState;
3638

3739
if (workflowFile) {
3840
const { rules, model, tools, prompt } = workflowFile;
@@ -50,12 +52,17 @@ export class ConfigEnhancer {
5052
}
5153
}
5254

53-
// Note that --model takes precedence over workflow model
55+
// --model takes precedence over workflow model
5456
if (model) {
55-
options.model = [...(options.model ?? []), model];
57+
const workflowModel = await loadPackageFromHub(model, modelProcessor);
58+
enhancedConfig.models = [
59+
workflowModel,
60+
...(enhancedConfig.models ?? []),
61+
];
62+
workflowService?.setWorkflowModelName(workflowModel.name);
5663
}
5764

58-
// Add workflow prompt as prefix
65+
// Add workflow prompt as prefix (see processAndCombinePrompts)
5966
if (prompt) {
6067
options.prompt = [prompt, ...(options.prompt || [])];
6168
}
@@ -141,11 +148,8 @@ export class ConfigEnhancer {
141148
const modifiedConfig = { ...config };
142149

143150
// Prepend processed MCPs to existing mcpServers array for consistency
144-
const existingMcpServers = (modifiedConfig as any).mcpServers || [];
145-
(modifiedConfig as any).mcpServers = [
146-
...processedMcps,
147-
...existingMcpServers,
148-
];
151+
const existingMcpServers = modifiedConfig.mcpServers || [];
152+
modifiedConfig.mcpServers = [...processedMcps, ...existingMcpServers];
149153

150154
return modifiedConfig;
151155
}
@@ -159,12 +163,11 @@ export class ConfigEnhancer {
159163
): Promise<AssistantUnrolled> {
160164
const processedModels = await loadPackagesFromHub(models, modelProcessor);
161165

162-
// Clone the config to avoid mutating the original
163166
const modifiedConfig = { ...config };
164167

165168
// Prepend processed models to existing models array so they become the default
166-
const existingModels = (modifiedConfig as any).models || [];
167-
(modifiedConfig as any).models = [...processedModels, ...existingModels];
169+
const existingModels = modifiedConfig.models || [];
170+
modifiedConfig.models = [...processedModels, ...existingModels];
168171

169172
return modifiedConfig;
170173
}

extensions/cli/src/hubLoader.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import { parseWorkflowFile, WorkflowFile } from "@continuedev/config-yaml";
12
import JSZip from "jszip";
23

3-
import { parseWorkflowFile, WorkflowFile } from "@continuedev/config-yaml";
44
import { env } from "./env.js";
55
import { logger } from "./util/logger.js";
66

extensions/cli/src/services/ModelService.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,14 @@ export class ModelService
5656
model && (model.roles?.includes("chat") || model.roles === undefined),
5757
) || []) as ModelConfig[];
5858

59-
// Check if workflow has a model specified (highest priority)
60-
let preferredModelName: string | null = null;
59+
let preferredModelName: string | null | undefined = null;
6160
let modelSource = "default";
6261

63-
if (workflowServiceState?.workflowFile?.model) {
64-
preferredModelName = workflowServiceState.workflowFile.model;
62+
// Priority = workflow -> last selected model
63+
if (workflowServiceState?.workflowModelName) {
64+
preferredModelName = workflowServiceState.workflowModelName;
6565
modelSource = "workflow";
6666
} else {
67-
// Fall back to persisted model name if no workflow model
6867
preferredModelName = getModelName(authConfig);
6968
if (preferredModelName) {
7069
modelSource = "persisted";

extensions/cli/src/services/ModelService.workflow-priority.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ describe("ModelService workflow model prioritization", () => {
6060
prompt: "Test workflow",
6161
},
6262
slug: "test/workflow",
63+
workflowModelName: "gpt-4",
64+
workflowService: null,
6365
};
6466

6567
// Mock createLlmApi to return different models based on the selected model
@@ -100,6 +102,8 @@ describe("ModelService workflow model prioritization", () => {
100102
// No model specified
101103
},
102104
slug: "test/workflow",
105+
workflowModelName: null,
106+
workflowService: null,
103107
};
104108

105109
// Mock getModelName to return a persisted model
@@ -142,6 +146,8 @@ describe("ModelService workflow model prioritization", () => {
142146
prompt: "Test workflow",
143147
},
144148
slug: "test/workflow",
149+
workflowModelName: "non-existent-model",
150+
workflowService: null,
145151
};
146152

147153
const getLlmApiMock = vi.mocked(config.getLlmApi);
@@ -171,6 +177,8 @@ describe("ModelService workflow model prioritization", () => {
171177
const workflowServiceState: WorkflowServiceState = {
172178
workflowFile: null,
173179
slug: null,
180+
workflowModelName: null,
181+
workflowService: null,
174182
};
175183

176184
// Make sure getModelName returns null (no persisted model)
@@ -207,6 +215,8 @@ describe("ModelService workflow model prioritization", () => {
207215
prompt: "Test workflow",
208216
},
209217
slug: "test/workflow",
218+
workflowModelName: "gpt-4",
219+
workflowService: null,
210220
};
211221

212222
// Mock getModelName to return a different persisted model

extensions/cli/src/services/WorkflowService.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Mock, vi } from "vitest";
22

33
import { workflowProcessor } from "../hubLoader.js";
4+
45
import { WorkflowService } from "./WorkflowService.js";
56

67
// Mock the hubLoader module
@@ -44,6 +45,8 @@ describe("WorkflowService", () => {
4445
expect(state).toEqual({
4546
workflowFile: null,
4647
slug: null,
48+
workflowModelName: null,
49+
workflowService: service,
4750
});
4851
});
4952

@@ -53,6 +56,8 @@ describe("WorkflowService", () => {
5356
expect(state).toEqual({
5457
workflowFile: null,
5558
slug: null,
59+
workflowModelName: null,
60+
workflowService: service,
5661
});
5762
});
5863

@@ -62,6 +67,8 @@ describe("WorkflowService", () => {
6267
expect(state).toEqual({
6368
workflowFile: null,
6469
slug: null,
70+
workflowModelName: null,
71+
workflowService: service,
6572
});
6673

6774
// Should not call loadPackageFromHub with invalid slug
@@ -74,6 +81,8 @@ describe("WorkflowService", () => {
7481
expect(state).toEqual({
7582
workflowFile: null,
7683
slug: null,
84+
workflowModelName: null,
85+
workflowService: service,
7786
});
7887

7988
expect(mockLoadPackageFromHub).not.toHaveBeenCalled();
@@ -96,6 +105,8 @@ describe("WorkflowService", () => {
96105
expect(state).toEqual({
97106
workflowFile: mockWorkflowFile,
98107
slug: "owner/package",
108+
workflowModelName: null,
109+
workflowService: service,
99110
});
100111

101112
expect(mockLoadPackageFromHub).toHaveBeenCalledWith(
@@ -112,6 +123,8 @@ describe("WorkflowService", () => {
112123
expect(state).toEqual({
113124
workflowFile: null,
114125
slug: null,
126+
workflowModelName: null,
127+
workflowService: service,
115128
});
116129

117130
expect(mockLoadPackageFromHub).toHaveBeenCalledWith(
@@ -133,6 +146,8 @@ describe("WorkflowService", () => {
133146
expect(state).toEqual({
134147
workflowFile: mockWorkflowFile,
135148
slug: "owner/minimal",
149+
workflowModelName: null,
150+
workflowService: service,
136151
});
137152
});
138153
});

extensions/cli/src/services/WorkflowService.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,21 @@ import { WorkflowServiceState } from "./types.js";
99
* Loads workflows from the hub and extracts model, tools, and prompt information
1010
*/
1111
export class WorkflowService extends BaseService<WorkflowServiceState> {
12+
/**
13+
* Set the resolved workflow model name after it's been processed
14+
* Called by ConfigEnhancer after resolving the model slug
15+
*/
16+
setWorkflowModelName(modelName: string): void {
17+
this.setState({
18+
workflowModelName: modelName,
19+
});
20+
}
1221
constructor() {
1322
super("WorkflowService", {
23+
workflowService: null,
1424
workflowFile: null,
1525
slug: null,
26+
workflowModelName: null,
1627
});
1728
}
1829

@@ -22,8 +33,10 @@ export class WorkflowService extends BaseService<WorkflowServiceState> {
2233
async doInitialize(workflowSlug?: string): Promise<WorkflowServiceState> {
2334
if (!workflowSlug) {
2435
return {
36+
workflowService: this,
2537
workflowFile: null,
2638
slug: null,
39+
workflowModelName: null,
2740
};
2841
}
2942

@@ -41,14 +54,18 @@ export class WorkflowService extends BaseService<WorkflowServiceState> {
4154
);
4255

4356
return {
57+
workflowService: this,
4458
workflowFile,
4559
slug: workflowSlug,
60+
workflowModelName: null, // Will be set by ConfigEnhancer after model resolution
4661
};
4762
} catch (error: any) {
4863
logger.error("Failed to initialize WorkflowService:", error);
4964
return {
65+
workflowService: this,
5066
workflowFile: null,
5167
slug: null,
68+
workflowModelName: null,
5269
};
5370
}
5471
}

extensions/cli/src/services/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { BaseCommandOptions } from "../commands/BaseCommandOptions.js";
1313
import { PermissionMode, ToolPermissions } from "../permissions/types.js";
1414

1515
import { MCPService } from "./MCPService.js";
16+
import { WorkflowService } from "./WorkflowService.js";
1617

1718
/**
1819
* Service lifecycle states
@@ -125,6 +126,8 @@ export interface StorageSyncServiceState {
125126
export interface WorkflowServiceState {
126127
workflowFile: WorkflowFile | null;
127128
slug: string | null;
129+
workflowModelName: string | null;
130+
workflowService: WorkflowService | null;
128131
}
129132

130133
export type { ChatHistoryState } from "./ChatHistoryService.js";

extensions/cli/src/services/workflow-integration.test.ts

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,12 @@ describe("Workflow Integration Tests", () => {
136136
const workflowState = workflowService.getState();
137137
expect(workflowState.workflowFile?.model).toBe("gpt-4-workflow");
138138

139+
// Mock loadPackageFromHub to return a model for the workflow model
140+
mockLoadPackageFromHub.mockResolvedValueOnce({
141+
name: "gpt-4-workflow",
142+
provider: "openai",
143+
});
144+
139145
// Test that ConfigEnhancer adds the workflow model to options
140146
const baseConfig = { models: [] };
141147
const baseOptions = {}; // No --model flag
@@ -145,11 +151,18 @@ describe("Workflow Integration Tests", () => {
145151
baseOptions,
146152
);
147153

148-
// Should have loaded the workflow model via injectModels
149-
expect(mockLoadPackagesFromHub).toHaveBeenCalledWith(
150-
["gpt-4-workflow"],
151-
expect.anything(),
154+
// Should have loaded the workflow model directly via loadPackageFromHub
155+
expect(mockLoadPackageFromHub).toHaveBeenCalledWith(
156+
"gpt-4-workflow",
157+
mockModelProcessor,
152158
);
159+
160+
// The workflow model should be prepended to the models array
161+
expect(enhancedConfig.models).toHaveLength(1);
162+
expect(enhancedConfig.models?.[0]).toEqual({
163+
name: "gpt-4-workflow",
164+
provider: "openai",
165+
});
153166
});
154167

155168
it("should not add workflow model when no workflow is active", async () => {
@@ -177,6 +190,18 @@ describe("Workflow Integration Tests", () => {
177190
mockLoadPackageFromHub.mockResolvedValue(mockWorkflowFile);
178191
await workflowService.initialize("owner/workflow");
179192

193+
// Mock loadPackageFromHub for workflow model and loadPackagesFromHub for user models
194+
mockLoadPackageFromHub.mockResolvedValueOnce({
195+
name: "gpt-4-workflow",
196+
provider: "openai",
197+
});
198+
mockLoadPackagesFromHub.mockResolvedValueOnce([
199+
{
200+
name: "user-specified-model",
201+
provider: "anthropic",
202+
},
203+
]);
204+
180205
// Test that --model flag takes precedence
181206
const baseConfig = { models: [] };
182207
const baseOptions = { model: ["user-specified-model"] }; // User specified --model
@@ -186,12 +211,28 @@ describe("Workflow Integration Tests", () => {
186211
baseOptions,
187212
);
188213

189-
// Should process both the user model and workflow model
190-
// The ConfigEnhancer adds workflow model to the existing model options
214+
// Should process the user model via loadPackagesFromHub
191215
expect(mockLoadPackagesFromHub).toHaveBeenCalledWith(
192-
["user-specified-model", "gpt-4-workflow"],
193-
expect.anything(),
216+
["user-specified-model"],
217+
mockModelProcessor,
218+
);
219+
220+
// Should also load the workflow model
221+
expect(mockLoadPackageFromHub).toHaveBeenCalledWith(
222+
"gpt-4-workflow",
223+
mockModelProcessor,
194224
);
225+
226+
// Both models should be in the config, with user model first (takes precedence)
227+
expect(enhancedConfig.models).toHaveLength(2);
228+
expect(enhancedConfig.models?.[0]).toEqual({
229+
name: "user-specified-model",
230+
provider: "anthropic",
231+
});
232+
expect(enhancedConfig.models?.[1]).toEqual({
233+
name: "gpt-4-workflow",
234+
provider: "openai",
235+
});
195236
});
196237
});
197238

0 commit comments

Comments
 (0)