Skip to content

Commit 6019e7a

Browse files
committed
fix tests
1 parent 298dec1 commit 6019e7a

File tree

3 files changed

+188
-3
lines changed

3 files changed

+188
-3
lines changed

extensions/cli/src/onboarding.test.ts

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,169 @@
1+
import * as fs from "fs";
2+
import * as os from "os";
3+
import * as path from "path";
4+
15
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
26

37
import type { AuthConfig } from "./auth/workos.js";
8+
import { initializeWithOnboarding } from "./onboarding.js";
9+
10+
describe("onboarding config flag handling", () => {
11+
let tempDir: string;
12+
let mockAuthConfig: AuthConfig;
13+
14+
beforeEach(() => {
15+
// Create a temporary directory for test config files
16+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "continue-test-"));
17+
18+
// Create a minimal auth config for testing
19+
mockAuthConfig = {
20+
userId: "test-user",
21+
userEmail: "[email protected]",
22+
accessToken: "test-token",
23+
refreshToken: "test-refresh",
24+
expiresAt: Date.now() + 3600000,
25+
organizationId: "test-org",
26+
};
27+
});
28+
29+
afterEach(() => {
30+
// Clean up temporary directory
31+
if (fs.existsSync(tempDir)) {
32+
fs.rmSync(tempDir, { recursive: true, force: true });
33+
}
34+
});
35+
36+
test("should fail loudly when --config points to non-existent file", async () => {
37+
const configPath = path.join(tempDir, "non-existent.yaml");
38+
39+
// Verify the file doesn't exist
40+
expect(fs.existsSync(configPath)).toBe(false);
41+
42+
// Should throw an error that mentions both the path and the failure
43+
await expect(
44+
initializeWithOnboarding(mockAuthConfig, configPath),
45+
).rejects.toThrow(
46+
/Failed to load config from ".*non-existent\.yaml": .*ENOENT/,
47+
);
48+
});
49+
50+
test("should fail loudly when --config points to malformed YAML file", async () => {
51+
const configPath = path.join(tempDir, "malformed.yaml");
52+
53+
// Create a malformed YAML file
54+
fs.writeFileSync(
55+
configPath,
56+
`
57+
name: "Test Config"
58+
models:
59+
- name: "GPT-4"
60+
provider: "openai"
61+
invalid_yaml_syntax: [unclosed array
62+
`,
63+
);
64+
65+
// Verify the file exists
66+
expect(fs.existsSync(configPath)).toBe(true);
67+
68+
// Should throw an error mentioning the path and failure to load
69+
await expect(
70+
initializeWithOnboarding(mockAuthConfig, configPath),
71+
).rejects.toThrow(/Failed to load config from ".*malformed\.yaml": .+/);
72+
});
73+
74+
test("should fail loudly when --config points to file with missing required fields", async () => {
75+
const configPath = path.join(tempDir, "incomplete.yaml");
76+
77+
// Create a config file missing required fields
78+
fs.writeFileSync(
79+
configPath,
80+
`
81+
name: "Incomplete Config"
82+
# Missing models array and other required fields
83+
`,
84+
);
85+
86+
// Verify the file exists
87+
expect(fs.existsSync(configPath)).toBe(true);
88+
89+
// Should throw with our specific error format and include path
90+
await expect(
91+
initializeWithOnboarding(mockAuthConfig, configPath),
92+
).rejects.toThrow(/^Failed to load config from ".*": .+/);
93+
});
94+
95+
test("should handle different config path formats with proper error messages", async () => {
96+
const testPaths = [
97+
"./non-existent.yaml",
98+
"/absolute/path/config.yaml",
99+
"../relative/config.yaml",
100+
"simple-name.yaml",
101+
];
102+
103+
for (const configPath of testPaths) {
104+
await expect(
105+
initializeWithOnboarding(mockAuthConfig, configPath),
106+
).rejects.toThrow(/Failed to load config from ".*": .+/);
107+
}
108+
});
109+
110+
test("should handle empty string config path", async () => {
111+
// Empty string should be treated differently from undefined
112+
// Note: empty string triggers onboarding flow, but should still fail in our error format
113+
await expect(
114+
initializeWithOnboarding(mockAuthConfig, ""),
115+
).rejects.toThrow();
116+
});
117+
118+
test("should not fall back to default config when explicit config fails", async () => {
119+
const configPath = path.join(tempDir, "bad-config.yaml");
120+
121+
// Create a bad config file
122+
fs.writeFileSync(configPath, "invalid: yaml: content: [");
123+
124+
const promise = initializeWithOnboarding(mockAuthConfig, configPath);
125+
126+
await expect(promise).rejects.toThrow();
127+
128+
try {
129+
await promise;
130+
} catch (error) {
131+
const message = error instanceof Error ? error.message : String(error);
132+
133+
// CRITICAL: Must have our specific error format from the fix
134+
expect(message).toMatch(/^Failed to load config from ".*": .+/);
135+
136+
// Error should be about the specific config file we provided
137+
expect(message).toContain(configPath);
138+
139+
// Should NOT mention falling back to default config (this was the bug!)
140+
expect(message).not.toContain("~/.continue/config.yaml");
141+
expect(message).not.toContain("default config");
142+
expect(message).not.toContain("fallback");
143+
}
144+
});
145+
146+
test("demonstrates the fix: explicit config failure vs no config provided", async () => {
147+
const badConfigPath = path.join(tempDir, "bad.yaml");
148+
fs.writeFileSync(badConfigPath, "invalid yaml [");
149+
150+
// Case 1: Explicit --config that fails should throw our specific error
151+
await expect(
152+
initializeWithOnboarding(mockAuthConfig, badConfigPath),
153+
).rejects.toThrow(/^Failed to load config from "/);
154+
155+
// Case 2: No explicit config should follow different logic
156+
try {
157+
await initializeWithOnboarding(mockAuthConfig, undefined);
158+
// If it succeeds, that's fine - the point is it's different behavior
159+
} catch (error) {
160+
const errorMessage =
161+
error instanceof Error ? error.message : String(error);
162+
// This should NOT have our "Failed to load config from" prefix
163+
expect(errorMessage).not.toMatch(/^Failed to load config from "/);
164+
}
165+
});
166+
});
4167

5168
// Separate describe block with its own mocking for BEDROCK tests
6169
describe("CONTINUE_USE_BEDROCK environment variable", () => {

extensions/cli/src/onboarding.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import * as path from "path";
33

44
import chalk from "chalk";
55

6-
import { login } from "./auth/workos.js";
6+
import { AuthConfig, login } from "./auth/workos.js";
7+
import { getApiClient } from "./config.js";
8+
import { loadConfiguration } from "./configLoader.js";
79
import { env } from "./env.js";
810
import {
911
getApiKeyValidationError,
@@ -131,9 +133,27 @@ export async function markOnboardingComplete(): Promise<void> {
131133
fs.writeFileSync(flagPath, new Date().toISOString());
132134
}
133135

134-
export async function initializeWithOnboarding(configPath: string | undefined) {
136+
export async function initializeWithOnboarding(
137+
authConfig: AuthConfig,
138+
configPath: string | undefined,
139+
) {
135140
const firstTime = await isFirstTime();
136141

142+
if (configPath !== undefined) {
143+
// throw an early error is configPath is invalid or has errors
144+
try {
145+
await loadConfiguration(
146+
authConfig,
147+
configPath,
148+
getApiClient(authConfig?.accessToken),
149+
);
150+
} catch (errorMessage) {
151+
throw new Error(
152+
`Failed to load config from "${configPath}": ${errorMessage}`,
153+
);
154+
}
155+
}
156+
137157
if (!firstTime) return;
138158

139159
const wasOnboarded = await runOnboardingFlow(configPath);

extensions/cli/src/services/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { loadAuthConfig } from "../auth/workos.js";
12
import { initializeWithOnboarding } from "../onboarding.js";
23
import { logger } from "../util/logger.js";
34

@@ -53,7 +54,8 @@ export async function initializeServices(initOptions: ServiceInitOptions = {}) {
5354

5455
// Handle onboarding for TUI mode (headless: false) unless explicitly skipped
5556
if (!initOptions.headless && !initOptions.skipOnboarding) {
56-
await initializeWithOnboarding(commandOptions.config);
57+
const authConfig = loadAuthConfig();
58+
await initializeWithOnboarding(authConfig, commandOptions.config);
5759
}
5860

5961
// Handle ANTHROPIC_API_KEY in headless mode when no config path is provided

0 commit comments

Comments
 (0)