Skip to content
Open
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
6 changes: 5 additions & 1 deletion src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import { installPrepareCommitMsgHook } from '../git/hook.js';

const CUSTOM_KEY = '__custom__';

export function normalizeBaseUrl(value: string): string {
return value.replace(/\/+$/, '');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM RISK

Suggestion: The current implementation fails to remove trailing slashes if the string ends with whitespace. Trimming the input first ensures robust normalization.

Suggested change
return value.replace(/\/+$/, '');
return value.trim().replace(/\/+$/, '');

}

export function buildApiKeyPrompt(existingKey: string, apiKeyEnv: string) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM RISK

This function is significantly over complexity and length limits (CCN 47), which makes the initialization flow difficult to maintain and test. Consider breaking down the various prompt configurations into a map or separate factory functions based on the provider.

return {
message: `Enter your API key (will be stored in config), or leave blank to use ${pc.cyan(`$${apiKeyEnv}`)} env var:`,
Expand Down Expand Up @@ -78,7 +82,7 @@ export async function initCommand(options: { installHook?: boolean } = {}): Prom
outro('Setup cancelled.');
return;
}
baseUrl = urlResult;
baseUrl = normalizeBaseUrl(urlResult);
apiKeyEnv = 'CUSTOM_API_KEY';
needsApiKey = true;
} else {
Expand Down
56 changes: 56 additions & 0 deletions tests/init-base-url.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import assert from 'node:assert/strict';
import test from 'node:test';

import { initCommand, normalizeBaseUrl } from '../dist/commands/init.js';

test('handles single trailing slash', () => {
assert.equal(normalizeBaseUrl('https://api.example.com/v1/'), 'https://api.example.com/v1');
});

test('trims multiple trailing slashes from custom base URLs', () => {
assert.equal(normalizeBaseUrl('https://api.example.com/v1///'), 'https://api.example.com/v1');
});

test('preserves custom base URLs without trailing slashes', () => {
assert.equal(normalizeBaseUrl('https://api.example.com/v1'), 'https://api.example.com/v1');
});

test('initCommand saves normalized custom provider base URL', async () => {
const savedConfigs = [];
const selectedValues = ['__custom__', 'custom-model'];
const textValues = ['https://api.example.com/v1/', 'test-api-key', '50'];
const spinnerStub = {
start() {},
stop() {},
};

await initCommand(
{},
{
intro() {},
outro() {},
select: async () => selectedValues.shift(),
text: async () => textValues.shift(),
confirm: async () => false,
spinner: () => spinnerStub,
isCancel: () => false,
configExists: () => false,
fetchModels: async (provider, baseUrl) => {
assert.equal(provider, '__custom__');
assert.equal(baseUrl, 'https://api.example.com/v1');
return ['custom-model'];
},
saveConfig: async (config) => {
savedConfigs.push(config);
},
testConnection: async (config) => {
assert.equal(config.baseUrl, 'https://api.example.com/v1');
return config.model;
},
},
Comment on lines +27 to +50

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e

echo "== initCommand signature =="
ast-grep outline src/commands/init.ts --view expanded | sed -n '/initCommand/,+6p'
echo
sed -n '22,90p' src/commands/init.ts

echo
echo "== test call site =="
sed -n '18,56p' tests/init-base-url.test.mjs

Repository: 404-PF/commit-echo

Length of output: 3366


🏁 Script executed:

#!/bin/bash
set -e

echo "== init.ts imports and helper usage =="
ast-grep outline src/commands/init.ts --view expanded | sed -n '1,220p'

echo
echo "== init.ts relevant sections =="
sed -n '1,260p' src/commands/init.ts

echo
echo "== tests that mock command dependencies =="
rg -n "initCommand\\(|fetchModels: async|saveConfig: async|testConnection: async|configExists:" tests src -g '!**/node_modules/**'

Repository: 404-PF/commit-echo

Length of output: 8995


🏁 Script executed:

#!/bin/bash
set -e

echo "== init-base-url.test.mjs =="
sed -n '1,220p' tests/init-base-url.test.mjs

echo
echo "== any module-mocking helpers used in tests =="
rg -n "mock\\(|unstable_mockModule|proxyquire|sinon|vi\\.mock|jest\\.mock" tests src -g '!**/node_modules/**'

Repository: 404-PF/commit-echo

Length of output: 2070


Use a real mocking seam for initCommand The second argument here is ignored: src/commands/init.ts only accepts an options object and imports select, text, fetchModels, saveConfig, and testConnection directly. This test never exercises the custom-provider flow it asserts; either add an explicit dependency-injection seam or mock the imported modules.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/init-base-url.test.mjs` around lines 27 - 50, The custom stubs passed
as the second argument to initCommand are ignored because src/commands/init.ts
reads select, text, fetchModels, saveConfig, and testConnection from direct
imports instead of injected dependencies. Update initCommand to accept a real
dependency-injection seam (or mock the imported modules in the test) so the test
can actually control the custom-provider flow, and make sure the assertions in
the init-base-url test run through the same
fetchModels/testConnection/saveConfig path used by initCommand.

);
Comment on lines +27 to +51

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 HIGH RISK

The initCommand function does not support the dependency injection pattern used in this test. It only accepts a single options argument, so the mocks passed as the second argument are completely ignored. This causes the test to execute with real side effects (terminal I/O and filesystem writes) and fails to actually verify the normalization logic. Refactor initCommand in src/commands/init.ts to accept an optional dependencies object for its external utilities.


assert.equal(savedConfigs.length, 1);
assert.equal(savedConfigs[0].provider, '__custom__');
assert.equal(savedConfigs[0].baseUrl, 'https://api.example.com/v1');
});