test(ai-review 同步): 更新只提交問題檔的測試
This commit is contained in:
+25
-118
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { commitAndPush, cloneRepo, verifyRemoteAccess, SYNC_PATHS, BOT_COMMIT_MARKER, getHeadCommitMessage, isBotAutoCommit, mergeInstructionText } from './git.js';
|
import { commitAndPush, cloneRepo, verifyRemoteAccess, BOT_COMMIT_MARKER, getHeadCommitMessage, isBotAutoCommit } from './git.js';
|
||||||
|
|
||||||
// --- helpers ---
|
// --- helpers ---
|
||||||
function makeTmpWorkspace() {
|
function makeTmpWorkspace() {
|
||||||
@@ -13,13 +13,7 @@ function makeTmpWorkspace() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function makeActionSource() {
|
function makeActionSource() {
|
||||||
const sourceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'git-source-'));
|
return fs.mkdtempSync(path.join(os.tmpdir(), 'git-source-'));
|
||||||
for (const relPath of SYNC_PATHS) {
|
|
||||||
const fullPath = path.join(sourceRoot, relPath);
|
|
||||||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
||||||
fs.writeFileSync(fullPath, relPath);
|
|
||||||
}
|
|
||||||
return sourceRoot;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default stub: all commands succeed, status returns changes
|
// Default stub: all commands succeed, status returns changes
|
||||||
@@ -125,7 +119,7 @@ describe('commitAndPush', () => {
|
|||||||
assert.equal(commitCalled, false, 'commit should not run when there are no changes');
|
assert.equal(commitCalled, false, 'commit should not run when there are no changes');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds skill and entry files together with findings', async () => {
|
it('adds only generated review files', async () => {
|
||||||
const repoDir = path.join(workspace, 'repo');
|
const repoDir = path.join(workspace, 'repo');
|
||||||
fs.mkdirSync(path.join(workspace, '.gitea/ai-review'), { recursive: true });
|
fs.mkdirSync(path.join(workspace, '.gitea/ai-review'), { recursive: true });
|
||||||
fs.writeFileSync(path.join(workspace, '.gitea/ai-review/findings.json'), '[]\n');
|
fs.writeFileSync(path.join(workspace, '.gitea/ai-review/findings.json'), '[]\n');
|
||||||
@@ -136,123 +130,36 @@ describe('commitAndPush', () => {
|
|||||||
const spawn = makeSpawn();
|
const spawn = makeSpawn();
|
||||||
await commitAndPush(workspace, repoDir, spawn, sourceRoot);
|
await commitAndPush(workspace, repoDir, spawn, sourceRoot);
|
||||||
const addCalls = spawn.calls.filter(c => c.args[0] === 'add');
|
const addCalls = spawn.calls.filter(c => c.args[0] === 'add');
|
||||||
const skillAddCall = addCalls.find(c => c.args.includes('.github/skills/triage-findings/SKILL.md'));
|
|
||||||
const generatedAddCall = addCalls.find(c => c.args.includes('.gitea/ai-review/exclusions.json'));
|
const generatedAddCall = addCalls.find(c => c.args.includes('.gitea/ai-review/exclusions.json'));
|
||||||
assert.ok(skillAddCall, 'expected git add for synced skill files');
|
|
||||||
assert.ok(generatedAddCall, 'expected git add for generated review files');
|
assert.ok(generatedAddCall, 'expected git add for generated review files');
|
||||||
assert.ok(skillAddCall.args.includes('.codex/skills/triage-findings/SKILL.md'));
|
|
||||||
assert.ok(skillAddCall.args.includes('.codex/skills/triage-findings/agents/openai.yaml'));
|
|
||||||
assert.ok(skillAddCall.args.includes('.agents/skills/triage-findings/SKILL.md'));
|
|
||||||
assert.ok(skillAddCall.args.includes('.claude/skills/triage-findings/SKILL.md'));
|
|
||||||
assert.ok(skillAddCall.args.includes('.gemini/skills/triage-findings/SKILL.md'));
|
|
||||||
assert.ok(skillAddCall.args.includes('.antigravity/skills/triage-findings/SKILL.md'));
|
|
||||||
assert.ok(skillAddCall.args.includes('.github/copilot-instructions.md'));
|
|
||||||
assert.ok(skillAddCall.args.includes('.amazonq/rules/triage-findings.md'));
|
|
||||||
assert.ok(skillAddCall.args.includes('AGENTS.md'));
|
|
||||||
assert.ok(skillAddCall.args.includes('ANTIGRAVITY.md'));
|
|
||||||
assert.ok(skillAddCall.args.includes('CLAUDE.md'));
|
|
||||||
assert.ok(skillAddCall.args.includes('GEMINI.md'));
|
|
||||||
assert.ok(!skillAddCall.args.includes('README.md'));
|
|
||||||
assert.ok(generatedAddCall.args.includes('.gitea/ai-review/findings.json'));
|
assert.ok(generatedAddCall.args.includes('.gitea/ai-review/findings.json'));
|
||||||
assert.ok(generatedAddCall.args.includes('.gitea/ai-review/exclusions.json'));
|
assert.ok(generatedAddCall.args.includes('.gitea/ai-review/exclusions.json'));
|
||||||
|
assert.equal(addCalls.length, 1, 'expected only generated review files to be staged');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps repo copies when the source sync file is missing', async () => {
|
it('does not overwrite or add action source files', async () => {
|
||||||
const missingPath = path.join(sourceRoot, '.amazonq/rules/triage-findings.md');
|
const repoDir = path.join(workspace, 'repo');
|
||||||
fs.rmSync(missingPath, { force: true });
|
const sourceDocPath = path.join(sourceRoot, 'docs/source-only.md');
|
||||||
const repoPath = path.join(workspace, 'repo', '.amazonq/rules/triage-findings.md');
|
const repoDocPath = path.join(repoDir, 'docs/source-only.md');
|
||||||
fs.writeFileSync(repoPath, 'stale');
|
const repoConfigPath = path.join(repoDir, 'project-notes.md');
|
||||||
|
fs.mkdirSync(path.join(workspace, '.gitea/ai-review'), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(workspace, '.gitea/ai-review/findings.json'), '[]\n');
|
||||||
|
fs.writeFileSync(path.join(workspace, '.gitea/ai-review/exclusions.json'), '[]\n');
|
||||||
|
|
||||||
|
fs.mkdirSync(path.dirname(sourceDocPath), { recursive: true });
|
||||||
|
fs.mkdirSync(path.dirname(repoDocPath), { recursive: true });
|
||||||
|
fs.writeFileSync(sourceDocPath, 'fresh action source doc');
|
||||||
|
fs.writeFileSync(repoDocPath, 'existing repo doc');
|
||||||
|
fs.writeFileSync(repoConfigPath, 'existing repo notes');
|
||||||
|
|
||||||
const spawn = makeSpawn();
|
const spawn = makeSpawn();
|
||||||
|
await commitAndPush(workspace, repoDir, spawn, sourceRoot);
|
||||||
|
const addedArgs = spawn.calls.filter(c => c.args[0] === 'add').flatMap(c => c.args);
|
||||||
|
|
||||||
await commitAndPush(workspace, path.join(workspace, 'repo'), spawn, sourceRoot);
|
assert.equal(fs.readFileSync(repoDocPath, 'utf8'), 'existing repo doc');
|
||||||
|
assert.equal(fs.readFileSync(repoConfigPath, 'utf8'), 'existing repo notes');
|
||||||
const rmCall = spawn.calls.find(c => c.args[0] === 'rm');
|
assert.ok(!addedArgs.includes('docs/source-only.md'));
|
||||||
assert.equal(rmCall, undefined, 'git rm should not run for missing source files');
|
assert.ok(!addedArgs.includes('project-notes.md'));
|
||||||
assert.equal(fs.readFileSync(repoPath, 'utf8'), 'stale');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('merges existing repo copies with workspace files', async () => {
|
|
||||||
const repoDir = path.join(workspace, 'repo');
|
|
||||||
fs.writeFileSync(path.join(repoDir, 'AGENTS.md'), 'repo agents doc');
|
|
||||||
fs.writeFileSync(path.join(repoDir, 'ANTIGRAVITY.md'), 'repo antigravity doc');
|
|
||||||
fs.writeFileSync(path.join(repoDir, 'CLAUDE.md'), 'repo claude doc');
|
|
||||||
fs.writeFileSync(path.join(repoDir, 'GEMINI.md'), 'repo gemini doc');
|
|
||||||
|
|
||||||
await commitAndPush(workspace, repoDir, makeSpawn(), sourceRoot);
|
|
||||||
|
|
||||||
const agentsDoc = fs.readFileSync(path.join(repoDir, 'AGENTS.md'), 'utf8');
|
|
||||||
const antigravityDoc = fs.readFileSync(path.join(repoDir, 'ANTIGRAVITY.md'), 'utf8');
|
|
||||||
const claudeDoc = fs.readFileSync(path.join(repoDir, 'CLAUDE.md'), 'utf8');
|
|
||||||
const geminiDoc = fs.readFileSync(path.join(repoDir, 'GEMINI.md'), 'utf8');
|
|
||||||
|
|
||||||
assert.ok(agentsDoc.includes('repo agents doc'));
|
|
||||||
assert.ok(agentsDoc.includes('AGENTS.md'));
|
|
||||||
assert.ok(antigravityDoc.includes('repo antigravity doc'));
|
|
||||||
assert.ok(antigravityDoc.includes('ANTIGRAVITY.md'));
|
|
||||||
assert.ok(claudeDoc.includes('repo claude doc'));
|
|
||||||
assert.ok(claudeDoc.includes('CLAUDE.md'));
|
|
||||||
assert.ok(geminiDoc.includes('repo gemini doc'));
|
|
||||||
assert.ok(geminiDoc.includes('GEMINI.md'));
|
|
||||||
assert.ok(agentsDoc.includes('repo agents doc'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('accepts AI merged instruction text when all unique blocks are preserved', async () => {
|
|
||||||
const calls = [];
|
|
||||||
const aiMergeAssistant = async payload => {
|
|
||||||
calls.push(payload);
|
|
||||||
return ['repo block', 'source block', 'extra block'].join('\n\n');
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await mergeInstructionText('repo block', 'source block', 'AGENTS.md', aiMergeAssistant);
|
|
||||||
|
|
||||||
assert.equal(calls.length, 1);
|
|
||||||
assert.ok(result.includes('repo block'));
|
|
||||||
assert.ok(result.includes('source block'));
|
|
||||||
assert.ok(result.includes('extra block'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('uses deterministic instruction merge when AI returns no usable result', async () => {
|
|
||||||
const aiMergeAssistant = async () => null;
|
|
||||||
|
|
||||||
const result = await mergeInstructionText('repo block', 'source block', 'AGENTS.md', aiMergeAssistant);
|
|
||||||
|
|
||||||
assert.ok(result.includes('repo block'));
|
|
||||||
assert.ok(result.includes('source block'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('exits when AI output drops a block', async () => {
|
|
||||||
const originalExit = process.exit;
|
|
||||||
let exitCode = null;
|
|
||||||
process.exit = code => { exitCode = code; };
|
|
||||||
try {
|
|
||||||
const aiMergeAssistant = async () => 'source block only';
|
|
||||||
await assert.rejects(() => mergeInstructionText('repo block', 'source block', 'AGENTS.md', aiMergeAssistant));
|
|
||||||
assert.equal(exitCode, 1);
|
|
||||||
} finally {
|
|
||||||
process.exit = originalExit;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('overwrites non-merge sync files with workspace files', async () => {
|
|
||||||
const repoDir = path.join(workspace, 'repo');
|
|
||||||
const sourceSkillPath = path.join(sourceRoot, '.github/skills/triage-findings/SKILL.md');
|
|
||||||
const repoSkillPath = path.join(repoDir, '.github/skills/triage-findings/SKILL.md');
|
|
||||||
const sourceNestedPath = path.join(sourceRoot, '.codex/skills/triage-findings/assets/example.txt');
|
|
||||||
const repoNestedPath = path.join(repoDir, '.codex/skills/triage-findings/assets/example.txt');
|
|
||||||
|
|
||||||
fs.writeFileSync(sourceSkillPath, 'fresh github skill');
|
|
||||||
fs.writeFileSync(repoSkillPath, 'stale github skill');
|
|
||||||
fs.mkdirSync(path.dirname(sourceNestedPath), { recursive: true });
|
|
||||||
fs.writeFileSync(sourceNestedPath, 'fresh nested');
|
|
||||||
fs.mkdirSync(path.dirname(repoNestedPath), { recursive: true });
|
|
||||||
fs.writeFileSync(repoNestedPath, 'stale nested');
|
|
||||||
fs.writeFileSync(path.join(repoDir, '.github/copilot-instructions.md'), 'stale copilot');
|
|
||||||
|
|
||||||
await commitAndPush(workspace, repoDir, makeSpawn(), sourceRoot);
|
|
||||||
|
|
||||||
assert.equal(fs.readFileSync(repoSkillPath, 'utf8'), 'fresh github skill');
|
|
||||||
assert.equal(fs.readFileSync(repoNestedPath, 'utf8'), 'fresh nested');
|
|
||||||
assert.equal(fs.readFileSync(path.join(repoDir, '.github/copilot-instructions.md'), 'utf8'), '.github/copilot-instructions.md');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not throw when git command fails', async () => {
|
it('does not throw when git command fails', async () => {
|
||||||
|
|||||||
+5
-5
@@ -129,10 +129,10 @@ describe('filterDiff', () => {
|
|||||||
const block = (file) => `diff --git a/${file} b/${file}\n--- a/${file}\n+++ b/${file}\n@@ -1 +1 @@\n-old\n+new\n`;
|
const block = (file) => `diff --git a/${file} b/${file}\n--- a/${file}\n+++ b/${file}\n@@ -1 +1 @@\n-old\n+new\n`;
|
||||||
|
|
||||||
it('filters out configured folder blocks', () => {
|
it('filters out configured folder blocks', () => {
|
||||||
const diff = block('.gitea/workflows/review.yaml') + block('.amazonq/rules/triage-findings.md') + block('src/index.js');
|
const diff = block('.gitea/workflows/review.yaml') + block('.github/workflows/review.yaml') + block('src/index.js');
|
||||||
const result = filterDiff(diff, ['.gitea/', '.amazonq/']);
|
const result = filterDiff(diff, ['.gitea/', '.github/']);
|
||||||
assert.ok(!result.includes('.gitea/'));
|
assert.ok(!result.includes('.gitea/'));
|
||||||
assert.ok(!result.includes('.amazonq/'));
|
assert.ok(!result.includes('.github/'));
|
||||||
assert.ok(result.includes('src/index.js'));
|
assert.ok(result.includes('src/index.js'));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -144,8 +144,8 @@ describe('filterDiff', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns empty string when all blocks are excluded', () => {
|
it('returns empty string when all blocks are excluded', () => {
|
||||||
const diff = block('.gitea/workflows/review.yaml') + block('.gitea/ai-review/findings.json') + block('.agents/skills/triage-findings/SKILL.md');
|
const diff = block('.gitea/workflows/review.yaml') + block('.gitea/ai-review/findings.json');
|
||||||
const result = filterDiff(diff, ['.gitea/', '.agents/']);
|
const result = filterDiff(diff, ['.gitea/']);
|
||||||
assert.equal(result, '');
|
assert.equal(result, '');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user