fix(pr-create): 錶沒在跑時 409 與 500 都不算失敗
Gitea 回「cannot stop a non-existent stopwatch」時用的狀態碼隨站台而異:這台回 409, 而腳本只認 500。結果是 PR 已經開出去了,卻以 exit 1 與 HTTP_ERROR 收場——照它自己 寫下的理由,那會讓人以為 PR 沒開成而重跑一次。三次重現(議題 #41、#50、#42)。 認的是「狀態碼在 409/500 這一組 **且** 訊息說的是碼錶」:只看訊息會把真的伺服器錯誤 一起吞掉,只看狀態碼會把別的衝突也當成沒錶。兩種狀態碼各一條測試,另加一條 「訊息對不上的 409 照常拋出」。 README 與 AGENTS.md 的「六個流程正本尚未到齊」也一併改掉——六份都在了,那句話會讓 使用者以為裝了也沒指令可用,在 AGENTS.md 裡還會誤導下一個 agent。並補一條測試把說法 與 prompts/ 的實際份數釘在一起,免得下次又走鐘。 議題 #54 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@
|
|||||||
本 repo 以 npm 佈署:`npm i -g <git url>` 裝出 `tea-sdlc` 指令,`tea-sdlc install` 產生各平台轉接檔。
|
本 repo 以 npm 佈署:`npm i -g <git url>` 裝出 `tea-sdlc` 指令,`tea-sdlc install` 產生各平台轉接檔。
|
||||||
轉接檔裡沒有路徑,只有一句 `tea-sdlc prompt --name <指令名>`,正本在哪由入口自己回推。
|
轉接檔裡沒有路徑,只有一句 `tea-sdlc prompt --name <指令名>`,正本在哪由入口自己回推。
|
||||||
|
|
||||||
> 六個流程正本尚未到齊,安裝器只佈署 `prompts/` 裡已經存在的指令。進度見
|
> 六個流程正本都到齊了;安裝器佈署的就是 `prompts/` 裡的那六份。進度見
|
||||||
> [議題 #1](https://gitea.jsc.idv.tw/plugins/tea-sdlc/issues/1) 底下的工作包。
|
> [議題 #1](https://gitea.jsc.idv.tw/plugins/tea-sdlc/issues/1) 底下的工作包。
|
||||||
|
|
||||||
## 模組邊界
|
## 模組邊界
|
||||||
|
|||||||
@@ -7,8 +7,7 @@
|
|||||||
- **副作用集中**:所有對 Gitea 與 git 的呼叫下沉到 `scripts/` 的零相依 Node 腳本,統一 JSON 輸入輸出。
|
- **副作用集中**:所有對 Gitea 與 git 的呼叫下沉到 `scripts/` 的零相依 Node 腳本,統一 JSON 輸入輸出。
|
||||||
- **產出有固定形狀**:議題、PR、報表一律套 `templates/` 的模板。
|
- **產出有固定形狀**:議題、PR、報表一律套 `templates/` 的模板。
|
||||||
|
|
||||||
> **目前仍在實作中。** 安裝、佈署與腳本已經可用,六個流程正本尚未到齊——
|
> 六個流程正本都到齊了,`tea-sdlc install` 會把它們一次佈署到偵測到的平台。
|
||||||
> `tea-sdlc install` 只會佈署 `prompts/` 裡已經存在的指令。
|
|
||||||
> 完整需求見[議題 #1](https://gitea.jsc.idv.tw/plugins/tea-sdlc/issues/1),進度見其底下的工作包。
|
> 完整需求見[議題 #1](https://gitea.jsc.idv.tw/plugins/tea-sdlc/issues/1),進度見其底下的工作包。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+13
-5
@@ -213,16 +213,24 @@ function checkTestResult(body) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 停錶。錶沒在跑時 Gitea 回 500,那不算失敗——PR 已經開出去了,
|
* 「這顆議題上沒有碼錶在跑」的回法不只一種:看過 500,也看過 409。
|
||||||
* 把整件事報成失敗只會讓人以為 PR 沒開成而重跑一次。
|
* 狀態碼隨站台版本而異,所以認的是「狀態碼在這一組裡 **且** 訊息說的是碼錶」——
|
||||||
|
* 只看訊息會把真的伺服器錯誤一起吞掉,只看狀態碼會把別的衝突也當成沒錶。
|
||||||
|
*/
|
||||||
|
const NO_STOPWATCH_STATUS = [409, 500];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停錶。錶沒在跑時不算失敗——PR 已經開出去了,把整件事報成失敗只會讓人以為
|
||||||
|
* PR 沒開成而重跑一次。
|
||||||
*/
|
*/
|
||||||
async function stopStopwatch(login, path) {
|
async function stopStopwatch(login, path) {
|
||||||
const response = await giteaRequest(login, 'POST', path, { body: {} });
|
const response = await giteaRequest(login, 'POST', path, { body: {} });
|
||||||
if (response.status >= 200 && response.status < 300) return true;
|
if (response.status >= 200 && response.status < 300) return true;
|
||||||
|
|
||||||
// Gitea 對「這顆議題上沒有碼錶在跑」回的是 500。那不是失敗——
|
if (
|
||||||
// 只有這一種 500 能這樣看待,訊息對不上就照常拋,免得把真的伺服器錯誤吞掉。
|
NO_STOPWATCH_STATUS.includes(response.status) &&
|
||||||
if (response.status === 500 && /stopwatch/i.test(response.body?.message ?? '')) {
|
/stopwatch/i.test(response.body?.message ?? '')
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
expectOk(response, `POST ${path}`);
|
expectOk(response, `POST ${path}`);
|
||||||
|
|||||||
+37
-8
@@ -303,21 +303,50 @@ test('PR 開失敗時不停錶:工時要記在真的有做事的那段時間
|
|||||||
assert.match(json.error.message, /422|already exists/);
|
assert.match(json.error.message, /422|already exists/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('錶本來就沒在跑時不算失敗:PR 已經開出去了', async (t) => {
|
/**
|
||||||
// Gitea 對「沒有碼錶在跑」回 500;這時 PR 已經建立,不該把整件事報成失敗
|
* 「沒有碼錶在跑」這件事,Gitea 不只用一種狀態碼回。
|
||||||
|
* 站台版本不同回法就不同,而這時 PR 已經建立——把整件事報成失敗,使用者會以為 PR
|
||||||
|
* 沒開成而重跑一次。
|
||||||
|
*/
|
||||||
|
const NO_STOPWATCH = [
|
||||||
|
{ status: 500, message: 'cannot stop a non existent stopwatch' },
|
||||||
|
{ status: 409, message: 'cannot stop a non-existent stopwatch' },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { status, message } of NO_STOPWATCH) {
|
||||||
|
test(`錶本來就沒在跑時不算失敗(HTTP ${status}):PR 已經開出去了`, async (t) => {
|
||||||
|
const stub = await withStub(t, {
|
||||||
|
[`POST /api/v1/repos/${ISSUE_REPO}/issues/${INDEX}/stopwatch/stop`]: {
|
||||||
|
status,
|
||||||
|
body: { message },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const file = bodyFile(`nowatch-${status}`, BODY);
|
||||||
|
|
||||||
|
const { code, json } = await runFull(file, stub);
|
||||||
|
|
||||||
|
assert.equal(code, 0, JSON.stringify(json));
|
||||||
|
assert.equal(json.data.碼錶已停, false);
|
||||||
|
assert.match(json.data.note ?? '', /碼錶/);
|
||||||
|
assert.ok(json.data.url, 'PR 的網址照常回報:它真的開出去了');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('訊息對不上碼錶的 409 照常拋出,不被一起吞掉', async (t) => {
|
||||||
|
// 只有「沒有碼錶在跑」那一種能被當成不算失敗;其餘的 409 是真的有問題
|
||||||
const stub = await withStub(t, {
|
const stub = await withStub(t, {
|
||||||
[`POST /api/v1/repos/${ISSUE_REPO}/issues/${INDEX}/stopwatch/stop`]: {
|
[`POST /api/v1/repos/${ISSUE_REPO}/issues/${INDEX}/stopwatch/stop`]: {
|
||||||
status: 500,
|
status: 409,
|
||||||
body: { message: 'cannot stop a non existent stopwatch' },
|
body: { message: 'issue is locked' },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const file = bodyFile('nowatch', BODY);
|
const file = bodyFile('locked', BODY);
|
||||||
|
|
||||||
const { code, json } = await runFull(file, stub);
|
const { code, json } = await runFull(file, stub);
|
||||||
|
|
||||||
assert.equal(code, 0, JSON.stringify(json));
|
assert.equal(code, 1);
|
||||||
assert.equal(json.data.碼錶已停, false);
|
assert.equal(json.error.code, 'HTTP_ERROR');
|
||||||
assert.match(json.data.note ?? '', /碼錶/);
|
assert.match(json.error.message, /409/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('沒給 --index 時擋下:停錶是這一步的一部分,忘了給會讓工時算不準', async (t) => {
|
test('沒給 --index 時擋下:停錶是這一步的一部分,忘了給會讓工時算不準', async (t) => {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
import { mkdtempSync, readFileSync, readdirSync, rmSync } from 'node:fs';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { repoRoot, runBin, tmpRoot } from './helpers/run-script.js';
|
import { repoRoot, runBin, tmpRoot } from './helpers/run-script.js';
|
||||||
|
|
||||||
@@ -22,6 +22,21 @@ function commands(text) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
test('說明不得宣稱正本沒到齊——prompts/ 裡有幾份,說明就得跟著', () => {
|
||||||
|
// 「裝了也沒指令可用」這種過時的說法,會讓使用者以為工具還不能用而不去裝;
|
||||||
|
// 同一句話在 AGENTS.md 裡還會誤導下一個 agent。
|
||||||
|
const 正本數 = readdirSync(join(repoRoot, 'prompts')).filter((f) => f.endsWith('.md')).length;
|
||||||
|
|
||||||
|
for (const [名字, 內容] of [['README.md', readme()], ['AGENTS.md', agents()]]) {
|
||||||
|
const 說沒到齊 = /正本尚未到齊|仍在實作中/.test(內容);
|
||||||
|
assert.equal(
|
||||||
|
說沒到齊,
|
||||||
|
正本數 < 6,
|
||||||
|
`${名字} 對正本進度的說法與 prompts/ 的實際份數(${正本數})對不上`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('安裝以 npm 為唯一建議路徑,指令含完整可複製的 git URL', () => {
|
test('安裝以 npm 為唯一建議路徑,指令含完整可複製的 git URL', () => {
|
||||||
const install = commands(readme()).filter((line) => line.startsWith('npm i -g'));
|
const install = commands(readme()).filter((line) => line.startsWith('npm i -g'));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user