Merge pull request 'feat/comments-merge/main' (#52) from feat/comments-merge/main into master
Reviewed-on: #52 Reviewed-by: 系統管理員 <1+admin@noreply.localhost>
This commit was merged in pull request #52.
This commit is contained in:
@@ -0,0 +1,335 @@
|
||||
/**
|
||||
* 把留言裡的決策整併回議題描述。
|
||||
*
|
||||
* 兩件事錯了都很安靜,所以測試集中在這裡:
|
||||
*
|
||||
* 1. **局部更新。** 只換指定那一段,其餘一字不動。整份重寫會把別人在其他段落的
|
||||
* 編輯一起蓋掉,而議題的編輯紀錄沒有人會去比對。
|
||||
* 2. **標記只給真的整併進去的那幾則。** 略過的要保持未標記,下次才會再被提出來;
|
||||
* 而描述沒寫成功就不該標記——標了就等於這則再也不會被看到。
|
||||
*/
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { runScript, tmpRoot } from './helpers/run-script.js';
|
||||
import { healthyRoutes, stubEnv as envFor, withStubGitea, patchOf } from './helpers/stub-gitea.js';
|
||||
|
||||
const REPO = 'plugins/tea-sdlc';
|
||||
const INDEX = 7;
|
||||
|
||||
/** 一份有多個段落的需求議題 */
|
||||
const BODY = `## 總覽
|
||||
|
||||
把一段口語需求變成結構化議題。
|
||||
|
||||
## 背景
|
||||
|
||||
需求目前寫成散文。
|
||||
|
||||
## 目標
|
||||
|
||||
- 需求議題可被下游腳本機讀
|
||||
- 建立議題的時間從 30 分鐘降到 5 分鐘
|
||||
|
||||
## 非目標
|
||||
|
||||
- 不處理工作包的拆解
|
||||
`;
|
||||
|
||||
/** 把段落內容寫成檔案,回傳路徑 */
|
||||
function contentFile(name, content) {
|
||||
mkdirSync(tmpRoot, { recursive: true });
|
||||
const path = join(tmpRoot, `merge-${name}-${process.hrtime.bigint()}.md`);
|
||||
writeFileSync(path, content);
|
||||
return path;
|
||||
}
|
||||
|
||||
function routes(overrides = {}, { body = BODY, comments = [101, 102] } = {}) {
|
||||
const base = healthyRoutes(REPO, {
|
||||
[`GET /api/v1/repos/${REPO}/issues/${INDEX}`]: {
|
||||
status: 200,
|
||||
body: { number: INDEX, title: '以 sdlc-plan 轉成結構化需求議題', body, html_url: `https://x/${INDEX}` },
|
||||
},
|
||||
[`PATCH /api/v1/repos/${REPO}/issues/${INDEX}`]: (req) => ({
|
||||
status: 200,
|
||||
body: { number: INDEX, ...req.body, html_url: `https://x/${INDEX}` },
|
||||
}),
|
||||
[`GET /api/v1/repos/${REPO}/issues/${INDEX}/comments`]: {
|
||||
status: 200,
|
||||
body: comments.map((id) => ({ id, body: `留言 ${id}`, user: { login: 'someone' } })),
|
||||
},
|
||||
});
|
||||
for (const id of comments) {
|
||||
base[`POST /api/v1/repos/${REPO}/issues/comments/${id}/reactions`] = { status: 201, body: {} };
|
||||
}
|
||||
return { ...base, ...overrides };
|
||||
}
|
||||
|
||||
const withStub = (t, overrides = {}, options) => withStubGitea(t, routes(overrides, options));
|
||||
|
||||
const run = (args, stub) =>
|
||||
runScript('comments-merge.js', ['--repo', REPO, '--index', String(INDEX), ...args], {
|
||||
env: envFor(stub),
|
||||
});
|
||||
|
||||
const reactions = (stub) =>
|
||||
stub.requests.filter((r) => r.method === 'POST' && r.path.includes('/reactions'));
|
||||
|
||||
// ── 局部更新 ───────────────────────────────────────────────────────
|
||||
|
||||
test('只換指定那一段,其餘一字不動', async (t) => {
|
||||
const stub = await withStub(t);
|
||||
const file = contentFile('goals', '- 需求議題可被下游腳本機讀\n- 建立議題的時間降到 5 分鐘\n- 名詞表由需求提出者維護\n');
|
||||
|
||||
const { code, json } = await run(
|
||||
['--section', '目標', '--content-file', file, '--merged', '101'],
|
||||
stub,
|
||||
);
|
||||
|
||||
assert.equal(code, 0, JSON.stringify(json));
|
||||
const written = patchOf(stub).body.body;
|
||||
assert.match(written, /- 名詞表由需求提出者維護/, '新內容要寫進去');
|
||||
assert.match(written, /## 總覽\n\n把一段口語需求變成結構化議題。/, '總覽原封不動');
|
||||
assert.match(written, /## 背景\n\n需求目前寫成散文。/, '背景原封不動');
|
||||
assert.match(written, /## 非目標\n\n- 不處理工作包的拆解/, '非目標原封不動');
|
||||
});
|
||||
|
||||
test('段落標題本身不動,只換它底下的內容', async (t) => {
|
||||
const stub = await withStub(t);
|
||||
const file = contentFile('keep-heading', '- 換掉的內容\n');
|
||||
|
||||
await run(['--section', '目標', '--content-file', file, '--merged', '101'], stub);
|
||||
|
||||
const written = patchOf(stub).body.body;
|
||||
assert.equal((written.match(/^## 目標$/gm) ?? []).length, 1, '標題只有一個,沒有被複製或刪掉');
|
||||
assert.match(written, /## 目標\n\n- 換掉的內容\n\n## 非目標/);
|
||||
});
|
||||
|
||||
test('段落之間的空行維持原本的樣子', async (t) => {
|
||||
const stub = await withStub(t);
|
||||
const file = contentFile('spacing', '- 甲\n');
|
||||
|
||||
await run(['--section', '背景', '--content-file', file, '--merged', '101'], stub);
|
||||
|
||||
assert.match(patchOf(stub).body.body, /## 背景\n\n- 甲\n\n## 目標/);
|
||||
});
|
||||
|
||||
test('最後一段也換得掉', async (t) => {
|
||||
const stub = await withStub(t);
|
||||
const file = contentFile('last', '- 也不處理權限\n');
|
||||
|
||||
await run(['--section', '非目標', '--content-file', file, '--merged', '101'], stub);
|
||||
|
||||
const written = patchOf(stub).body.body;
|
||||
assert.match(written, /## 非目標\n\n- 也不處理權限/);
|
||||
assert.equal(written.includes('不處理工作包的拆解'), false, '舊內容要被換掉');
|
||||
});
|
||||
|
||||
test('段落不存在時擋下,不把內容補到別的地方去', async (t) => {
|
||||
const stub = await withStub(t);
|
||||
const file = contentFile('missing', '- 內容\n');
|
||||
|
||||
const { code, json } = await run(
|
||||
['--section', '沒有這一段', '--content-file', file, '--merged', '101'],
|
||||
stub,
|
||||
);
|
||||
|
||||
assert.equal(code, 1);
|
||||
assert.equal(json.error.code, 'SECTION_NOT_FOUND');
|
||||
assert.equal(patchOf(stub), undefined);
|
||||
assert.deepEqual(reactions(stub), [], '沒寫進去就不該標記');
|
||||
});
|
||||
|
||||
test('圍欄裡的假標題不算段落', async (t) => {
|
||||
const body = '## 流程圖\n\n```\n## 目標\n這不是段落\n```\n\n## 目標\n\n- 真的目標\n';
|
||||
const stub = await withStub(t, {}, { body });
|
||||
const file = contentFile('fenced', '- 換掉的目標\n');
|
||||
|
||||
await run(['--section', '目標', '--content-file', file, '--merged', '101'], stub);
|
||||
|
||||
const written = patchOf(stub).body.body;
|
||||
assert.match(written, /```\n## 目標\n這不是段落\n```/, '圍欄裡的內容原封不動');
|
||||
assert.match(written, /## 目標\n\n- 換掉的目標/);
|
||||
});
|
||||
|
||||
// ── 標記:只給真的整併進去的 ───────────────────────────────────────
|
||||
|
||||
test('只標記 --merged 列出的那幾則', async (t) => {
|
||||
const stub = await withStub(t, {}, { comments: [101, 102, 103] });
|
||||
const file = contentFile('partial', '- 內容\n');
|
||||
|
||||
const { json } = await run(
|
||||
['--section', '目標', '--content-file', file, '--merged', '101,103'],
|
||||
stub,
|
||||
);
|
||||
|
||||
assert.deepEqual(json.data.已標記, [101, 103]);
|
||||
assert.deepEqual(
|
||||
reactions(stub).map((r) => r.path),
|
||||
[
|
||||
`/api/v1/repos/${REPO}/issues/comments/101/reactions`,
|
||||
`/api/v1/repos/${REPO}/issues/comments/103/reactions`,
|
||||
],
|
||||
'沒被整併的 102 要保持未標記,下次才會再被提出來',
|
||||
);
|
||||
});
|
||||
|
||||
test('標記送的是 +1', async (t) => {
|
||||
const stub = await withStub(t);
|
||||
const file = contentFile('thumb', '- 內容\n');
|
||||
|
||||
await run(['--section', '目標', '--content-file', file, '--merged', '101'], stub);
|
||||
|
||||
assert.deepEqual(reactions(stub)[0].body, { content: '+1' });
|
||||
});
|
||||
|
||||
test('先寫描述再標記:標記是「這則已經收進去了」的結論', async (t) => {
|
||||
const stub = await withStub(t);
|
||||
const file = contentFile('order', '- 內容\n');
|
||||
|
||||
await run(['--section', '目標', '--content-file', file, '--merged', '101'], stub);
|
||||
|
||||
const writes = stub.requests
|
||||
.filter((r) => r.method !== 'GET' && !r.path.endsWith('/issues/0'))
|
||||
.map((r) => r.path);
|
||||
assert.ok(
|
||||
writes.indexOf(`/api/v1/repos/${REPO}/issues/${INDEX}`)
|
||||
< writes.indexOf(`/api/v1/repos/${REPO}/issues/comments/101/reactions`),
|
||||
);
|
||||
});
|
||||
|
||||
test('描述寫入失敗時不標記:標了就等於這則再也不會被看到', async (t) => {
|
||||
const stub = await withStub(t, {
|
||||
[`PATCH /api/v1/repos/${REPO}/issues/${INDEX}`]: { status: 403, body: { message: 'forbidden' } },
|
||||
});
|
||||
const file = contentFile('fail', '- 內容\n');
|
||||
|
||||
const { code } = await run(['--section', '目標', '--content-file', file, '--merged', '101'], stub);
|
||||
|
||||
assert.equal(code, 1);
|
||||
assert.deepEqual(reactions(stub), []);
|
||||
});
|
||||
|
||||
test('--merged 指到議題上沒有的留言時擋下', async (t) => {
|
||||
const stub = await withStub(t, {}, { comments: [101] });
|
||||
const file = contentFile('badid', '- 內容\n');
|
||||
|
||||
const { json } = await run(
|
||||
['--section', '目標', '--content-file', file, '--merged', '101,999'],
|
||||
stub,
|
||||
);
|
||||
|
||||
assert.equal(json.error.code, 'COMMENT_NOT_FOUND');
|
||||
assert.match(json.error.message, /999/);
|
||||
assert.equal(patchOf(stub), undefined);
|
||||
});
|
||||
|
||||
// ── 冪等 ───────────────────────────────────────────────────────────
|
||||
|
||||
test('內容與現況相同時不重寫描述,但該標記的還是要標', async (t) => {
|
||||
// 重跑常常是因為上一輪標記那一步斷掉了
|
||||
const stub = await withStub(t);
|
||||
const file = contentFile('same', '- 需求議題可被下游腳本機讀\n- 建立議題的時間從 30 分鐘降到 5 分鐘\n');
|
||||
|
||||
const { code, json } = await run(
|
||||
['--section', '目標', '--content-file', file, '--merged', '101'],
|
||||
stub,
|
||||
);
|
||||
|
||||
assert.equal(code, 0, JSON.stringify(json));
|
||||
assert.equal(json.data.描述已更新, false);
|
||||
assert.equal(patchOf(stub), undefined, '沒變就不要在議題上留一筆空的編輯');
|
||||
assert.equal(reactions(stub).length, 1, '標記照舊');
|
||||
});
|
||||
|
||||
// ── 輸入 ───────────────────────────────────────────────────────────
|
||||
|
||||
test('內容是空的時候擋下:整併不該把一段清空', async (t) => {
|
||||
const stub = await withStub(t);
|
||||
const file = contentFile('empty', ' \n');
|
||||
|
||||
const { json } = await run(
|
||||
['--section', '目標', '--content-file', file, '--merged', '101'],
|
||||
stub,
|
||||
);
|
||||
|
||||
assert.equal(json.error.code, 'FILE_EMPTY');
|
||||
assert.equal(patchOf(stub), undefined);
|
||||
});
|
||||
|
||||
test('內容檔不存在時回可區分的錯誤碼', async (t) => {
|
||||
const stub = await withStub(t);
|
||||
|
||||
const { json } = await run(
|
||||
['--section', '目標', '--content-file', join(tmpRoot, '不存在.md'), '--merged', '101'],
|
||||
stub,
|
||||
);
|
||||
|
||||
assert.equal(json.error.code, 'FILE_NOT_FOUND');
|
||||
});
|
||||
|
||||
test('沒有 --merged 時擋下:整併卻不標記,下次會重複處理同一則', async (t) => {
|
||||
const stub = await withStub(t);
|
||||
const file = contentFile('nomerged', '- 內容\n');
|
||||
|
||||
const { json } = await runScript('comments-merge.js', [
|
||||
'--repo', REPO, '--index', String(INDEX), '--section', '目標', '--content-file', file,
|
||||
], { env: envFor(stub) });
|
||||
|
||||
assert.equal(json.error.code, 'MISSING_FLAG');
|
||||
assert.match(json.error.message, /--merged/);
|
||||
});
|
||||
|
||||
// ── --dry-run ─────────────────────────────────────────────────────
|
||||
|
||||
test('--dry-run 印出改完的描述與將標記的留言,但不寫入', async (t) => {
|
||||
const stub = await withStub(t);
|
||||
const file = contentFile('dry', '- 換掉的目標\n');
|
||||
|
||||
const { code, json } = await run(
|
||||
['--section', '目標', '--content-file', file, '--merged', '101,102', '--dry-run'],
|
||||
stub,
|
||||
);
|
||||
|
||||
assert.equal(code, 0);
|
||||
assert.equal(json.data.dryRun, true);
|
||||
assert.match(json.data.requests[0].body.body, /- 換掉的目標/);
|
||||
assert.deepEqual(
|
||||
json.data.requests.map((r) => `${r.method} ${r.path}`),
|
||||
[
|
||||
`PATCH /repos/${REPO}/issues/${INDEX}`,
|
||||
`POST /repos/${REPO}/issues/comments/101/reactions`,
|
||||
`POST /repos/${REPO}/issues/comments/102/reactions`,
|
||||
],
|
||||
);
|
||||
assert.equal(patchOf(stub), undefined);
|
||||
assert.deepEqual(reactions(stub), []);
|
||||
});
|
||||
|
||||
test('--dry-run 遇到段落不存在一樣報錯,不會等到實跑才發現', async (t) => {
|
||||
const stub = await withStub(t);
|
||||
const file = contentFile('dry-bad', '- 內容\n');
|
||||
|
||||
const { json } = await run(
|
||||
['--section', '沒有這一段', '--content-file', file, '--merged', '101', '--dry-run'],
|
||||
stub,
|
||||
);
|
||||
|
||||
assert.equal(json.error.code, 'SECTION_NOT_FOUND');
|
||||
});
|
||||
|
||||
test('--dry-run 在內容沒變時不預告 PATCH', async (t) => {
|
||||
const stub = await withStub(t);
|
||||
const file = contentFile('dry-same', '- 需求議題可被下游腳本機讀\n- 建立議題的時間從 30 分鐘降到 5 分鐘\n');
|
||||
|
||||
const { json } = await run(
|
||||
['--section', '目標', '--content-file', file, '--merged', '101', '--dry-run'],
|
||||
stub,
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
json.data.requests.map((r) => `${r.method} ${r.path}`),
|
||||
[`POST /repos/${REPO}/issues/comments/101/reactions`],
|
||||
);
|
||||
});
|
||||
@@ -85,7 +85,7 @@ function routes(overrides = {}, { body = FULL_BODY, comments = [] } = {}) {
|
||||
comments.forEach((c, i) => {
|
||||
base[`GET /api/v1/repos/${REPO}/issues/comments/${100 + i}/reactions`] = {
|
||||
status: 200,
|
||||
body: (c.reactions ?? []).map((content) => ({ content })),
|
||||
body: (c.reactions ?? []).map((content) => ({ content, user: { login: c.reactedBy ?? 'tester' } })),
|
||||
};
|
||||
});
|
||||
return { ...base, ...overrides };
|
||||
|
||||
@@ -25,10 +25,14 @@ function routes(overrides = {}, options = {}) {
|
||||
general = [],
|
||||
reviews = [],
|
||||
pull = { number: INDEX, title: 'feat/pr-comments/main', html_url: `https://x/${INDEX}`, state: 'open' },
|
||||
isPull = true,
|
||||
} = options;
|
||||
|
||||
const base = healthyRoutes(REPO, {
|
||||
[`GET /api/v1/repos/${REPO}/pulls/${INDEX}`]: { status: 200, body: pull },
|
||||
[`GET /api/v1/repos/${REPO}/issues/${INDEX}`]: {
|
||||
status: 200,
|
||||
body: { ...pull, ...(isPull ? { pull_request: { merged: false } } : {}) },
|
||||
},
|
||||
[`GET /api/v1/repos/${REPO}/issues/${INDEX}/comments`]: {
|
||||
status: 200,
|
||||
body: general.map((c, i) => ({
|
||||
@@ -357,18 +361,48 @@ test('帶出 PR 的識別資訊,讓回報不必再查一次', async (t) => {
|
||||
assert.equal(json.data.state, 'open');
|
||||
});
|
||||
|
||||
test('PR 不存在時回可區分的錯誤碼', async (t) => {
|
||||
test('議題不存在時回可區分的錯誤碼', async (t) => {
|
||||
const stub = await withStub(t, {
|
||||
[`GET /api/v1/repos/${REPO}/pulls/${INDEX}`]: { status: 404, body: { message: 'not found' } },
|
||||
[`GET /api/v1/repos/${REPO}/issues/${INDEX}`]: { status: 404, body: { message: 'not found' } },
|
||||
}, FULL);
|
||||
|
||||
const { code, json } = await run([], stub);
|
||||
|
||||
assert.equal(code, 1);
|
||||
assert.equal(json.error.code, 'PULL_NOT_FOUND');
|
||||
assert.equal(json.error.code, 'ISSUE_NOT_FOUND');
|
||||
assert.match(json.error.message, new RegExp(String(INDEX)));
|
||||
});
|
||||
|
||||
test('純議題也讀得到:先讀 issue 再決定要不要翻 review', async (t) => {
|
||||
// 每個 PR 都是議題,議題不一定是 PR。先打 /pulls 的話,純議題會 404,
|
||||
// 而 /sdlc-sync 的輸入正是純議題——整個流程在讀到第一則留言之前就斷了
|
||||
const stub = await withStub(t, {}, {
|
||||
isPull: false,
|
||||
general: [{ body: '這顆議題上的決策', reactions: [] }],
|
||||
reviews: [],
|
||||
});
|
||||
|
||||
const { code, json } = await run([], stub);
|
||||
|
||||
assert.equal(code, 0, JSON.stringify(json));
|
||||
assert.equal(json.data.類型, '議題');
|
||||
assert.deepEqual(json.data.留言.map((c) => c.內容), ['這顆議題上的決策']);
|
||||
assert.equal(
|
||||
stub.requests.some((r) => r.path.includes('/pulls/')),
|
||||
false,
|
||||
'純議題不該去打 PR 的端點',
|
||||
);
|
||||
});
|
||||
|
||||
test('是 PR 時類型標成 PR,並照樣讀 review', async (t) => {
|
||||
const stub = await withStub(t, {}, FULL);
|
||||
|
||||
const { json } = await run([], stub);
|
||||
|
||||
assert.equal(json.data.類型, 'PR');
|
||||
assert.ok(json.data.留言.some((c) => c.類型 === '總評'));
|
||||
});
|
||||
|
||||
// ── --dry-run ─────────────────────────────────────────────────────
|
||||
|
||||
test('--dry-run 印出將發出的請求,且不碰 Gitea', async (t) => {
|
||||
@@ -381,11 +415,8 @@ test('--dry-run 印出將發出的請求,且不碰 Gitea', async (t) => {
|
||||
assert.deepEqual(
|
||||
json.data.requests.map((r) => `${r.method} ${r.path}`),
|
||||
[
|
||||
'GET /user',
|
||||
`GET /repos/${REPO}/pulls/${INDEX}`,
|
||||
`GET /repos/${REPO}/issues/${INDEX}`,
|
||||
`GET /repos/${REPO}/issues/${INDEX}/comments`,
|
||||
`GET /repos/${REPO}/pulls/${INDEX}/reviews`,
|
||||
`GET /repos/${REPO}/issues/${INDEX}/timeline`,
|
||||
],
|
||||
);
|
||||
assert.match(json.data.note, /reaction|review/);
|
||||
|
||||
@@ -391,7 +391,7 @@ test('描述檔不存在時回可區分的錯誤碼', async (t) => {
|
||||
stub,
|
||||
);
|
||||
|
||||
assert.equal(json.error.code, 'BODY_FILE_NOT_FOUND');
|
||||
assert.equal(json.error.code, 'FILE_NOT_FOUND');
|
||||
});
|
||||
|
||||
// ── --dry-run ─────────────────────────────────────────────────────
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* /sdlc-sync 的流程正本,以及另外兩份正本的「接回」那一段。
|
||||
*
|
||||
* 這個指令的價值在於「描述不再騙人」,而三件關鍵事只有正本做得到:挑出哪些留言真的是
|
||||
* 決策、寫回去之前讓使用者點頭、以及略過的那幾則要保持未標記。寫漏任何一件,
|
||||
* 描述就會繼續過期,或者有決策被靜靜吞掉。
|
||||
*/
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { assertNeutralPrompt, readPrompt } from './helpers/prompt-doc.js';
|
||||
|
||||
const prompt = readPrompt('sdlc-sync');
|
||||
const steps = prompt.slice(prompt.indexOf('## 1.'), prompt.indexOf('## 邊界'));
|
||||
|
||||
test('正本平台中立,description 前綴正確', () => {
|
||||
assertNeutralPrompt(prompt, 'sdlc-sync');
|
||||
});
|
||||
|
||||
test('兩種議題各自指名對應的抽取腳本', () => {
|
||||
assert.match(steps, /issue-extract/);
|
||||
assert.match(steps, /wp-extract/);
|
||||
assert.match(steps, /需求議題.*工作包議題|工作包議題.*需求議題/s);
|
||||
});
|
||||
|
||||
test('留言內容另外拿,並說明為什麼抽取契約不夠', () => {
|
||||
assert.match(steps, /pr-comments/);
|
||||
assert.match(steps, /只給數字不給內容/);
|
||||
});
|
||||
|
||||
test('說明那支腳本議題與 PR 都收得下,不再宣稱「PR 也是 issue」', () => {
|
||||
assert.match(steps, /議題與 PR 都收得下/);
|
||||
assert.equal(steps.includes('Gitea 的 PR 也是 issue'), false, '反過來說才對:每個 PR 都是議題');
|
||||
});
|
||||
|
||||
test('已處理認的是自己打的 +1', () => {
|
||||
assert.match(steps, /自己打的/);
|
||||
assert.match(steps, /我同意/);
|
||||
});
|
||||
|
||||
test('略過會讓計數停在大於 0,這件事要講明白', () => {
|
||||
const section = steps.slice(steps.indexOf('## 5.'));
|
||||
assert.match(section, /停在大於 0/);
|
||||
assert.match(section, /每次開始時都會再停一次/);
|
||||
assert.match(section, /他選了略過/, '要讓使用者分得出「沒整併乾淨」與「選了略過」');
|
||||
});
|
||||
|
||||
test('已標記過的留言跳過', () => {
|
||||
assert.match(steps, /已經標記過的留言不再處理|已處理.*跳過/s);
|
||||
});
|
||||
|
||||
// ── 挑決策 ─────────────────────────────────────────────────────────
|
||||
|
||||
test('要整併與不整併各有判斷依據,不是只給兩個詞', () => {
|
||||
assert.match(steps, /\*\*要整併\*\* — .{10,}/);
|
||||
assert.match(steps, /\*\*不整併\*\* — .{10,}/);
|
||||
});
|
||||
|
||||
test('判斷不了時當成要整併,並說出漏掉的代價', () => {
|
||||
assert.match(steps, /判斷不了的\*\*當成要整併\*\*/);
|
||||
assert.match(steps, /描述就會繼續騙後面的人/);
|
||||
});
|
||||
|
||||
// ── 先點頭再寫 ─────────────────────────────────────────────────────
|
||||
|
||||
test('寫回去之前要列出方案給使用者看', () => {
|
||||
assert.match(steps, /先列出來再動手/);
|
||||
assert.match(steps, /哪一段/);
|
||||
assert.match(steps, /改完長什麼樣/);
|
||||
});
|
||||
|
||||
test('一次問一題,且略過的會保持未標記', () => {
|
||||
assert.match(steps, /一次問一題/);
|
||||
assert.match(steps, /略過的留言保持未標記/);
|
||||
assert.match(steps, /下次執行還會被提出來/);
|
||||
});
|
||||
|
||||
test('說明了這一步是描述的最後一道關卡', () => {
|
||||
assert.match(steps, /最後一道關卡/);
|
||||
assert.match(steps, /拿它當事實/);
|
||||
});
|
||||
|
||||
// ── 寫回 ───────────────────────────────────────────────────────────
|
||||
|
||||
test('指名 comments-merge,並要求先試跑', () => {
|
||||
assert.match(steps, /comments-merge\.js/);
|
||||
assert.match(steps, /--dry-run/);
|
||||
});
|
||||
|
||||
test('--content-file 的內容是整段,且說明標題那一行不含在內', () => {
|
||||
assert.match(steps, /那一段改完的完整內容/);
|
||||
assert.match(steps, /不含 `## 標題` 那一行/);
|
||||
});
|
||||
|
||||
test('--merged 只放真的併進去的,並說出放錯的後果', () => {
|
||||
assert.match(steps, /只放\*\*真的被併進這一段\*\*/);
|
||||
assert.match(steps, /再也\n?不會被提出來|再也不會被提出來/);
|
||||
});
|
||||
|
||||
test('SECTION_NOT_FOUND 的處置是回頭確認,不是硬塞別的段落', () => {
|
||||
assert.match(steps, /SECTION_NOT_FOUND/);
|
||||
assert.match(steps, /不要改用別的段落硬塞/);
|
||||
});
|
||||
|
||||
// ── 回報 ───────────────────────────────────────────────────────────
|
||||
|
||||
test('回報要列出略過的那幾則', () => {
|
||||
const section = steps.slice(steps.indexOf('## 5.'));
|
||||
assert.match(section, /略過的那幾則是哪些/);
|
||||
assert.match(section, /下次還會出現/);
|
||||
});
|
||||
|
||||
// ── 接回原本的指令 ─────────────────────────────────────────────────
|
||||
|
||||
test('整併完自動接回,不要求使用者重打指令', () => {
|
||||
const section = prompt.slice(prompt.indexOf('## 接回原本的指令'));
|
||||
assert.match(section, /直接接回去/);
|
||||
assert.match(section, /不要要求使用者重打一次/);
|
||||
});
|
||||
|
||||
test('接回之前要重新抽取,並說明為什麼', () => {
|
||||
const section = prompt.slice(prompt.indexOf('## 接回原本的指令'));
|
||||
assert.match(section, /先重跑一次抽取/);
|
||||
assert.match(section, /這一整段就白做了/);
|
||||
});
|
||||
|
||||
// ── 邊界 ───────────────────────────────────────────────────────────
|
||||
|
||||
test('邊界列出不做的事,含不刪改留言', () => {
|
||||
const boundary = prompt.slice(prompt.indexOf('## 邊界'));
|
||||
assert.match(boundary, /不自行決定要不要整併/);
|
||||
assert.match(boundary, /不整份重寫描述/);
|
||||
assert.match(boundary, /不標記略過的留言/);
|
||||
assert.match(boundary, /不刪除、不編輯任何留言/);
|
||||
assert.match(boundary, /誰說過什麼的紀錄/, '要說出為什麼留言不能動');
|
||||
});
|
||||
|
||||
// ── 另外兩份正本的接回 ─────────────────────────────────────────────
|
||||
|
||||
for (const name of ['sdlc-analyze', 'sdlc-feat']) {
|
||||
test(`${name} 偵測到未整併留言時會轉去整併,並自動接回`, () => {
|
||||
const other = readPrompt(name);
|
||||
assert.match(other, /未處理留言數/);
|
||||
assert.match(other, /直接走 `\/sdlc-sync` 的流程/, `${name} 要真的轉過去,不是叫人自己跑`);
|
||||
assert.match(other, /自動接回這裡/);
|
||||
assert.match(other, /不要要求使用者重打指令/);
|
||||
});
|
||||
|
||||
test(`${name} 接回前要重新抽取一次`, () => {
|
||||
assert.match(readPrompt(name), /重新抽取一次拿到更新後的描述/);
|
||||
});
|
||||
}
|
||||
@@ -106,7 +106,7 @@ function routes(overrides = {}, options = {}) {
|
||||
comments.forEach((c, i) => {
|
||||
base[`GET /api/v1/repos/${REPO}/issues/comments/${100 + i}/reactions`] = {
|
||||
status: 200,
|
||||
body: (c.reactions ?? []).map((content) => ({ content })),
|
||||
body: (c.reactions ?? []).map((content) => ({ content, user: { login: c.reactedBy ?? 'tester' } })),
|
||||
};
|
||||
});
|
||||
return { ...base, ...overrides };
|
||||
|
||||
Reference in New Issue
Block a user