feat: 導入 AI 程式碼審查 action 並修正進入點與參數接線 #1

Merged
admin merged 30 commits from ai-review-resolve/develop-20260702-160700 into develop 2026-07-03 10:04:33 +00:00
Showing only changes of commit 51e9568ccf - Show all commits
+92 -5
View File
@@ -4,7 +4,7 @@ import axios from 'axios';
import { mkdtemp, writeFile, chmod, rm } from 'fs/promises'; import { mkdtemp, writeFile, chmod, rm } from 'fs/promises';
import { tmpdir } from 'os'; import { tmpdir } from 'os';
import { join } from 'path'; import { join } from 'path';
import { checkRequiredEnv, verifyGiteaToken, verifyCommentToken, verifyLLM, runPreflight } from '../preflight.js'; import { checkRequiredEnv, verifyGiteaToken, verifyCommentToken, verifyLLM, fetchCodexModels, runPreflight } from '../preflight.js';
const LLM_ENV_KEYS = [ const LLM_ENV_KEYS = [
'AI_ASSISTANT_CLI', 'MODEL', 'OPENCODE_MODEL', 'PATH', 'AI_ASSISTANT_CLI', 'MODEL', 'OPENCODE_MODEL', 'PATH',
@@ -130,18 +130,52 @@ describe('verifyLLM', () => {
assert.match(result.error, /AI 助理 CLI/); assert.match(result.error, /AI 助理 CLI/);
}); });
it('passes when a supported assistant CLI is detected', async () => { it('passes when a supported assistant CLI is detected and the model is in the codex list', async () => {
clearLLMEnv(); clearLLMEnv();
await installFakeCLI('codex'); await installFakeCLI('codex');
process.env.AI_ASSISTANT_CLI = 'codex'; process.env.AI_ASSISTANT_CLI = 'codex';
process.env.MODEL = 'gpt-5-mini'; process.env.MODEL = 'gpt-5.4-mini';
const result = await verifyLLM(); const result = await verifyLLM({
fetchCodexModelsFn: async () => ({ ok: true, slugs: ['gpt-5.5', 'gpt-5.4-mini'] }),
});
assert.equal(result.ok, true); assert.equal(result.ok, true);
assert.equal(result.provider, 'codex'); assert.equal(result.provider, 'codex');
assert.equal(result.command, 'codex'); assert.equal(result.command, 'codex');
assert.equal(result.model, 'gpt-5-mini'); assert.equal(result.model, 'gpt-5.4-mini');
assert.deepEqual(result.models, ['gpt-5.5', 'gpt-5.4-mini']);
});
it('fails when codex auth is invalid (model list check reports 401)', async () => {
clearLLMEnv();
await installFakeCLI('codex');
process.env.AI_ASSISTANT_CLI = 'codex';
process.env.MODEL = 'gpt-5.4-mini';
const result = await verifyLLM({
fetchCodexModelsFn: async () => ({ ok: false, error: 'codex 認證失效(HTTP 401)——token 已被撤銷或過期,請重新登入 codex 並更新 LLM_OAUTH secret' }),
});
assert.equal(result.ok, false);
assert.equal(result.provider, 'codex');
assert.match(result.error, /HTTP 401/);
assert.match(result.error, /LLM_OAUTH/);
});
it('fails when the configured model is not in the codex available list', async () => {
clearLLMEnv();
await installFakeCLI('codex');
process.env.AI_ASSISTANT_CLI = 'codex';
process.env.MODEL = 'gpt-9-imaginary';
const result = await verifyLLM({
fetchCodexModelsFn: async () => ({ ok: true, slugs: ['gpt-5.5', 'gpt-5.4-mini'] }),
});
assert.equal(result.ok, false);
assert.match(result.error, /不在 codex 可用清單/);
assert.match(result.error, /gpt-5\.4-mini/);
}); });
it('fails when a requested CLI is not installed', async () => { it('fails when a requested CLI is not installed', async () => {
@@ -157,6 +191,59 @@ describe('verifyLLM', () => {
}); });
describe('fetchCodexModels', () => {
async function writeAuth(json) {
tempDir = await mkdtemp(join(tmpdir(), 'codex-auth-test-'));
const authPath = join(tempDir, 'auth.json');
await writeFile(authPath, JSON.stringify(json));
return authPath;
}
it('returns the model slugs on HTTP 200', async () => {
const authPath = await writeAuth({ tokens: { access_token: 'tok', account_id: 'acc' } });
let capturedUrl, capturedHeaders;
const result = await fetchCodexModels({
authPath,
fetchImpl: async (url, opts) => {
capturedUrl = url;
capturedHeaders = opts.headers;
return { status: 200, ok: true, json: async () => ({ models: [{ slug: 'gpt-5.5' }, { slug: 'gpt-5.4-mini' }] }) };
},
});
assert.deepEqual(result, { ok: true, slugs: ['gpt-5.5', 'gpt-5.4-mini'] });
assert.match(capturedUrl, /client_version=/);
assert.equal(capturedHeaders['Authorization'], 'Bearer tok');
assert.equal(capturedHeaders['chatgpt-account-id'], 'acc');
});
it('reports an auth failure on HTTP 401', async () => {
const authPath = await writeAuth({ tokens: { access_token: 'revoked' } });
const result = await fetchCodexModels({
authPath,
fetchImpl: async () => ({ status: 401, ok: false, json: async () => ({}) }),
});
assert.equal(result.ok, false);
assert.match(result.error, /HTTP 401/);
assert.match(result.error, /LLM_OAUTH/);
});
it('fails when the auth file cannot be read', async () => {
const result = await fetchCodexModels({
authPath: join(tmpdir(), 'definitely-missing-codex-auth-xyz.json'),
fetchImpl: async () => ({ status: 200, ok: true, json: async () => ({ models: [] }) }),
});
assert.equal(result.ok, false);
assert.match(result.error, /無法讀取 codex 認證檔/);
});
it('fails when the auth file lacks an access_token', async () => {
const authPath = await writeAuth({ tokens: {} });
const result = await fetchCodexModels({ authPath, fetchImpl: async () => ({ status: 200, ok: true, json: async () => ({}) }) });
assert.equal(result.ok, false);
assert.match(result.error, /缺少 tokens\.access_token/);
});
});
describe('runPreflight', () => { describe('runPreflight', () => {
function makeDeps(overrides = {}) { function makeDeps(overrides = {}) {
return { return {