feat: 導入 AI 程式碼審查 action 並修正進入點與參數接線 #1
@@ -4,7 +4,7 @@ import axios from 'axios';
|
||||
import { mkdtemp, writeFile, chmod, rm } from 'fs/promises';
|
||||
import { tmpdir } from 'os';
|
||||
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 = [
|
||||
'AI_ASSISTANT_CLI', 'MODEL', 'OPENCODE_MODEL', 'PATH',
|
||||
@@ -130,18 +130,52 @@ describe('verifyLLM', () => {
|
||||
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();
|
||||
await installFakeCLI('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.provider, '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 () => {
|
||||
@@ -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', () => {
|
||||
function makeDeps(overrides = {}) {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user