fix(腳本契約): 長輸出不再被截斷,查重翻頁加上上限
process.stdout.write 之後立刻 process.exit 會截斷輸出——stdout 接到 pipe 時 寫入是非同步的。改為等 write 的 callback 回來再退出。實測舊寫法在約 83KB 處 被切斷,新增的回歸測試以 8000 筆標籤覆蓋這條路徑。 findIssueByTitle 的翻頁原本沒有上限,Gitea 若持續回滿一頁就會無限打下去。 加上 200 頁上限,超過即以 DEDUPE_LIMIT 報錯而非無聲回 null——無聲回 null 會 讓呼叫端把既有議題再建一次,正好是冪等查重要防的事。 parseFlags 取值時不再於三元運算式內遞增迴圈變數,改為獨立敘述。 測試工具的 maxBuffer 調高到 64MB:預設 1MB 會在長輸出時砍掉子行程,那是測試 工具的限制而非腳本的問題。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+20
-8
@@ -72,7 +72,8 @@ export function parseFlags(argv, spec = {}) {
|
|||||||
flags[name] = true;
|
flags[name] = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const value = eq === -1 ? argv[(i += 1)] : arg.slice(eq + 1);
|
if (eq === -1) i += 1;
|
||||||
|
const value = eq === -1 ? argv[i] : arg.slice(eq + 1);
|
||||||
if (value === undefined || value.startsWith('--')) {
|
if (value === undefined || value.startsWith('--')) {
|
||||||
throw new ScriptError('MISSING_FLAG', `--${name} 需要一個值`);
|
throw new ScriptError('MISSING_FLAG', `--${name} 需要一個值`);
|
||||||
}
|
}
|
||||||
@@ -110,18 +111,21 @@ export function parseRepo(value) {
|
|||||||
export async function main(run) {
|
export async function main(run) {
|
||||||
try {
|
try {
|
||||||
const data = await run();
|
const data = await run();
|
||||||
write({ ok: true, data });
|
write({ ok: true, data }, 0);
|
||||||
process.exit(0);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const code = error instanceof ScriptError ? error.code : 'UNEXPECTED';
|
const code = error instanceof ScriptError ? error.code : 'UNEXPECTED';
|
||||||
const message = error?.message ?? String(error);
|
const message = error?.message ?? String(error);
|
||||||
write({ ok: false, error: { code, message } });
|
write({ ok: false, error: { code, message } }, 1);
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function write(payload) {
|
/**
|
||||||
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
* 印出結果後才結束行程。
|
||||||
|
* stdout 接到 pipe 時寫入是非同步的,直接 process.exit 會截斷長輸出,
|
||||||
|
* 所以要等 write 的 callback 回來再退出。
|
||||||
|
*/
|
||||||
|
function write(payload, exitCode) {
|
||||||
|
process.stdout.write(`${JSON.stringify(payload)}\n`, () => process.exit(exitCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 認證來源 ───────────────────────────────────────────────────────
|
// ── 認證來源 ───────────────────────────────────────────────────────
|
||||||
@@ -427,8 +431,11 @@ function checkTimeTracker(info) {
|
|||||||
export async function findIssueByTitle(login, repo, title) {
|
export async function findIssueByTitle(login, repo, title) {
|
||||||
const wanted = title.trim();
|
const wanted = title.trim();
|
||||||
const pageSize = 50;
|
const pageSize = 50;
|
||||||
|
// 翻頁上限。查不完就停手並報錯,不能無聲回 null——那會讓呼叫端把既有議題再建一次,
|
||||||
|
// 正好是冪等查重要防的事。
|
||||||
|
const maxPages = 200;
|
||||||
|
|
||||||
for (let page = 1; ; page += 1) {
|
for (let page = 1; page <= maxPages; page += 1) {
|
||||||
const response = await giteaRequest(login, 'GET', `/repos/${repo}/issues`, {
|
const response = await giteaRequest(login, 'GET', `/repos/${repo}/issues`, {
|
||||||
query: { state: 'all', limit: pageSize, page },
|
query: { state: 'all', limit: pageSize, page },
|
||||||
});
|
});
|
||||||
@@ -437,4 +444,9 @@ export async function findIssueByTitle(login, repo, title) {
|
|||||||
if (hit) return hit;
|
if (hit) return hit;
|
||||||
if (issues.length < pageSize) return null;
|
if (issues.length < pageSize) return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new ScriptError(
|
||||||
|
'DEDUPE_LIMIT',
|
||||||
|
`翻完 ${maxPages * pageSize} 筆議題仍未查完 ${repo},無法確認「${wanted}」是否已存在;請縮小範圍或手動確認`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ export function runScript(name, args = [], opts = {}) {
|
|||||||
execFile(
|
execFile(
|
||||||
process.execPath,
|
process.execPath,
|
||||||
[join(repoRoot, 'scripts', name), ...args],
|
[join(repoRoot, 'scripts', name), ...args],
|
||||||
{ cwd, env: childEnv },
|
// 預設 1MB 會在長輸出時砍掉子行程,那是測試工具的限制而非腳本的問題
|
||||||
|
{ cwd, env: childEnv, maxBuffer: 64 * 1024 * 1024 },
|
||||||
(error, stdout, stderr) => {
|
(error, stdout, stderr) => {
|
||||||
resolve({
|
resolve({
|
||||||
code: typeof error?.code === 'number' ? error.code : error ? 1 : 0,
|
code: typeof error?.code === 'number' ? error.code : error ? 1 : 0,
|
||||||
|
|||||||
@@ -99,3 +99,22 @@ test('Gitea 回非預期狀態碼時,帶著狀態碼失敗而不是硬吞', as
|
|||||||
assert.equal(json.error.code, 'HTTP_ERROR');
|
assert.equal(json.error.code, 'HTTP_ERROR');
|
||||||
assert.match(json.error.message, /500/);
|
assert.match(json.error.message, /500/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('大量標籤不會被截斷:輸出仍是完整且可解析的單行 JSON', async (t) => {
|
||||||
|
// 回歸測試:stdout 接到 pipe 時寫入是非同步的,寫完立刻 process.exit 會截斷長輸出
|
||||||
|
const many = Array.from({ length: 8000 }, (_, i) => ({
|
||||||
|
id: i,
|
||||||
|
name: `label-${i}`,
|
||||||
|
color: 'ffffff',
|
||||||
|
description: '為了把輸出撐大到足以觸發截斷的敘述文字'.repeat(2),
|
||||||
|
}));
|
||||||
|
const stub = await withStub(t, {
|
||||||
|
[`GET /api/v1/repos/${REPO}/labels`]: { status: 200, body: many },
|
||||||
|
});
|
||||||
|
|
||||||
|
const { code, json } = await runScript('labels-list.js', ['--repo', REPO], { env: envFor(stub) });
|
||||||
|
|
||||||
|
assert.equal(code, 0);
|
||||||
|
assert.equal(json.data.count, 8000);
|
||||||
|
assert.equal(json.data.labels.at(-1).name, 'label-7999');
|
||||||
|
});
|
||||||
|
|||||||
@@ -111,3 +111,17 @@ test('plugin 根由自身檔案位置回推,templates 與 references 都在底
|
|||||||
assert.ok(existsSync(templatesDir()));
|
assert.ok(existsSync(templatesDir()));
|
||||||
assert.ok(existsSync(referencesDir()));
|
assert.ok(existsSync(referencesDir()));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('查重:翻頁有上限,查不完時報錯而不是無聲回 null', async (t) => {
|
||||||
|
// 無聲回 null 會讓呼叫端把既有議題再建一次,正好是查重要防的事
|
||||||
|
const full = Array.from({ length: 50 }, (_, i) => ({ number: i, title: `不相干的議題 ${i}` }));
|
||||||
|
const stub = await startStubGitea({
|
||||||
|
[`GET /api/v1/repos/${REPO}/issues`]: { status: 200, body: full },
|
||||||
|
});
|
||||||
|
t.after(() => stub.close());
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
() => findIssueByTitle({ base: stub.base, token: 'x' }, REPO, '永遠找不到的標題'),
|
||||||
|
(error) => error.code === 'DEDUPE_LIMIT',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user