Files
release-cleanup/app/test/releases.test.js
T
JefferyandClaude Opus 4.8 858e628db6 test(release-cleanup): 驗證相同 created_at 時的決定性排序
新增測試:多筆 created_at 相同時,以 id 大小決定保留/刪除順序。測試共 60 項全數通過。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 11:43:16 +08:00

153 lines
5.6 KiB
JavaScript

import { test } from 'node:test'
import assert from 'node:assert/strict'
import { selectReleasesToDelete, cleanupReleases } from '../releases.js'
const releases = [
{ id: 1, tag_name: 'v1.0.0', created_at: '2024-01-01T00:00:00Z' },
{ id: 3, tag_name: 'v3.0.0', created_at: '2024-03-01T00:00:00Z' },
{ id: 2, tag_name: 'v2.0.0', created_at: '2024-02-01T00:00:00Z' },
]
test('selectReleasesToDelete 保留最新的 keepCount 筆,刪除其餘', () => {
const toDelete = selectReleasesToDelete(releases, 2)
assert.equal(toDelete.length, 1)
assert.equal(toDelete[0].tag_name, 'v1.0.0')
})
test('selectReleasesToDelete 在數量不超過保留數時回傳空陣列', () => {
assert.deepEqual(selectReleasesToDelete(releases, 3), [])
assert.deepEqual(selectReleasesToDelete(releases, 5), [])
})
test('selectReleasesToDelete 在 keepCount 為 0 時刪除全部(由新到舊)', () => {
const toDelete = selectReleasesToDelete(releases, 0)
assert.deepEqual(
toDelete.map((r) => r.tag_name),
['v3.0.0', 'v2.0.0', 'v1.0.0'],
)
})
test('selectReleasesToDelete 在 created_at 相同時以 id 決定順序(具決定性)', () => {
const same = '2024-01-01T00:00:00Z'
const list = [
{ id: 1, created_at: same },
{ id: 3, created_at: same },
{ id: 2, created_at: same },
]
const toDelete = selectReleasesToDelete(list, 1)
// 保留 id 最大者(3),刪除 id 2 與 1(順序固定)
assert.deepEqual(
toDelete.map((r) => r.id),
[2, 1],
)
})
test('selectReleasesToDelete 不改動原陣列', () => {
const original = [...releases]
selectReleasesToDelete(releases, 1)
assert.deepEqual(releases, original)
})
test('selectReleasesToDelete 對無效 created_at 視為最舊,不產生 NaN 排序', () => {
const list = [
{ id: 1, tag_name: 'good', created_at: '2024-05-01T00:00:00Z' },
{ id: 2, tag_name: 'bad', created_at: 'not-a-date' },
{ id: 3, tag_name: 'empty' },
]
const toDelete = selectReleasesToDelete(list, 1)
// 最新的 good 被保留;兩筆無效日期視為最舊,落入待刪除
assert.equal(toDelete.length, 2)
assert.ok(!toDelete.some((r) => r.tag_name === 'good'))
})
// 建立可記錄刪除呼叫的假 client
function fakeClient(releases, statusFor = () => 204) {
const deleted = []
return {
deleted,
async fetchAllPages() {
return releases
},
async deleteResource(url) {
deleted.push(url)
return statusFor(url)
},
}
}
test('cleanupReleases 刪除超出保留數的舊 release', async () => {
const client = fakeClient([
{ id: 1, tag_name: 'v1', name: 'n1', created_at: '2024-01-01T00:00:00Z' },
{ id: 2, tag_name: 'v2', name: 'n2', created_at: '2024-02-01T00:00:00Z' },
{ id: 3, tag_name: 'v3', name: 'n3', created_at: '2024-03-01T00:00:00Z' },
])
await cleanupReleases(client, { releaseApiUrl: 'http://x/releases', keepCount: 1 })
// 保留最新 v3,刪除 v2 與 v1
assert.deepEqual(client.deleted, ['http://x/releases/2', 'http://x/releases/1'])
})
test('cleanupReleases 在未超過保留數時不刪除', async () => {
const client = fakeClient([
{ id: 1, tag_name: 'v1', name: 'n1', created_at: '2024-01-01T00:00:00Z' },
])
await cleanupReleases(client, { releaseApiUrl: 'http://x/releases', keepCount: 2 })
assert.equal(client.deleted.length, 0)
})
test('cleanupReleases 在 deleteResource 回傳非 204 時仍繼續處理其餘項目', async () => {
const client = fakeClient(
[
{ id: 1, tag_name: 'v1', name: 'n1', created_at: '2024-01-01T00:00:00Z' },
{ id: 2, tag_name: 'v2', name: 'n2', created_at: '2024-02-01T00:00:00Z' },
{ id: 3, tag_name: 'v3', name: 'n3', created_at: '2024-03-01T00:00:00Z' },
],
(url) => (url.endsWith('/2') ? 500 : 204), // v2 刪除失敗
)
await cleanupReleases(client, { releaseApiUrl: 'http://x/releases', keepCount: 1 })
// 即使 v2 失敗(500),v1 仍被嘗試刪除
assert.deepEqual(client.deleted, ['http://x/releases/2', 'http://x/releases/1'])
})
test('cleanupReleases 對 id 進行 URL 編碼(防路徑穿越)', async () => {
const client = fakeClient([
{ id: 1, tag_name: 'v2', name: 'n2', created_at: '2024-02-01T00:00:00Z' },
{ id: '../evil', tag_name: 'v1', name: 'n1', created_at: '2024-01-01T00:00:00Z' },
])
await cleanupReleases(client, { releaseApiUrl: 'http://x/releases', keepCount: 1 })
// 保留最新(id=1),刪除較舊者(惡意 id);id 中的 ../ 應被編碼
assert.deepEqual(client.deleted, ['http://x/releases/..%2Fevil'])
})
test('cleanupReleases 在刪除回傳非 204 時記錄錯誤至 stderr', async () => {
const client = fakeClient(
[
{ id: 1, tag_name: 'v1', name: 'n1', created_at: '2024-01-01T00:00:00Z' },
{ id: 2, tag_name: 'v2', name: 'n2', created_at: '2024-02-01T00:00:00Z' },
],
() => 500,
)
const realWrite = process.stderr.write
const chunks = []
process.stderr.write = (chunk) => {
chunks.push(String(chunk))
return true
}
try {
await cleanupReleases(client, { releaseApiUrl: 'http://x/releases', keepCount: 1 })
} finally {
process.stderr.write = realWrite
}
assert.match(chunks.join(''), /\[ERR\]/)
})
test('cleanupReleases 略過沒有 id 的 release', async () => {
const client = fakeClient([
{ id: 1, tag_name: 'v3', name: 'n3', created_at: '2024-03-01T00:00:00Z' },
{ id: null, tag_name: 'v1', name: 'n1', created_at: '2024-01-01T00:00:00Z' },
{ id: 2, tag_name: 'v2', name: 'n2', created_at: '2024-02-01T00:00:00Z' },
])
await cleanupReleases(client, { releaseApiUrl: 'http://x/releases', keepCount: 1 })
// 保留最新 v3;待刪除為 v2 與 (無 id 的) v1 → 無 id 者被略過
assert.deepEqual(client.deleted, ['http://x/releases/2'])
})