fix(resource-rule): 上次裝到一半死掉的帳號要能再裝一次(Arcrun#123)
封測者 1.4.45 實撞: a namespace with this account ID and title already exists ⇒ 那個帳號從此永遠裝不起來,而錯誤訊息對用戶完全無法行動。 根因:rule.mjs 第 2c 段只問「有沒有已部署的 worker 綁著它」, 不問「這個名字在帳號上是不是已經存在」。註解裡「本來就沒有東西可丟」 漏掉一種狀態——資源已建、worker 還沒部署就中斷(逾時/關掉分頁/斷網)。 拆除 youlin 時親眼看到的 8 顆空殼 KV 是同一個形狀。 修法:2c 在 create 之前先查同名。找到同名資源時: · 呼叫端聲明了 createNameIsOurs → 接回那一顆(adopt + reclaimed 標記) · 沒聲明 → 停手,訊息帶 RES-NAME-TAKEN 錯誤碼讓用戶回報 createNameIsOurs 是接管的唯一依據,預設 false(fail-closed)。只有名字 推導自使用者自己的身分時才准聲明——安裝器的 arcrun-rag-<slugFromEmail(email)>-kv-<binding> 合格;acr 從 toml 讀到的 裸 binding 名(WEBHOOKS)不合格,因為用戶自己也可能用那個名字。 這不是把 #97 刪掉的「照名字 ensure」搬回來: ① #97 找不到就新建一顆頂上去(會弄丟資料);這裡找到才沿用, 找不到照舊新建,永遠不拿新的空資源頂替既有的 ② 排在「已部署綁定=事實」之後,名字只在沒有任何綁定可看時才有發言權 ③ #97 無條件相信名字;這裡要呼叫端先證明名字推導自用戶身分 順手補上假帳號的保真度:FakeCloudflare.createKvNamespace 原本不擋同名, 所以半殘帳號在測試裡看起來只是「多幾顆孤兒」,實際上是裝不起來—— 少了那一行,這個 bug 測不出來。fixture-account.mjs 也把 resourcesExist 與 deployed 拆開,才表達得出這個狀態。 驗證(皆為實跑): node shared/resource-rule/tests/half-finished-install.mjs → 全部通過(零依賴) cd cli && npm test → 73/73 pass sync-resource-rule --check → 三份副本皆與原稿一致 ⚠️ 只有規則這一半。安裝器要在 manifestRequirements 聲明 createNameIsOurs 才會生效,那一半在 arcrun-rag(D85)。 Refs: inkstone/Arcrun#123
This commit is contained in:
@@ -45,14 +45,21 @@ export function requirements() {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 安裝器替這台實例算出來的名字前綴(`arcrun-rag-<slugFromEmail(email)>`)。 */
|
||||
export const BASE_NAME = 'arcrun-rag-yuga3bse';
|
||||
|
||||
/**
|
||||
* 三種情境。`titleFor` 決定「使用者帳號上那顆資源實際叫什麼名字」——
|
||||
* 四種情境。`titleFor` 決定「使用者帳號上那顆資源實際叫什麼名字」——
|
||||
* 這正是 #97 的病根所在:規則**不准**拿名字當識別。
|
||||
*
|
||||
* @typedef {'fresh' | 'installed' | 'renamed'} Scenario
|
||||
* `resourcesExist` 與 `deployed` **刻意拆開**:兩者不一致的那一格
|
||||
* (資源在、worker 不在)就是 Arcrun#123 ——「上一次裝到一半死掉」的帳號。
|
||||
* 本檔原本只有 `deployed` 一個旗標,所以那個狀態**表達不出來,也就沒被測到**。
|
||||
*
|
||||
* @typedef {'fresh' | 'installed' | 'renamed' | 'half-finished'} Scenario
|
||||
*/
|
||||
|
||||
/** @type {Record<Scenario, {label: string, deployed: boolean, titleFor: (binding: string) => string}>} */
|
||||
/** @type {Record<Scenario, {label: string, deployed: boolean, resourcesExist?: boolean, titleFor: (binding: string) => string}>} */
|
||||
export const SCENARIOS = {
|
||||
fresh: {
|
||||
label: '沒裝過(全新帳號,一顆 worker 都沒有)',
|
||||
@@ -62,7 +69,7 @@ export const SCENARIOS = {
|
||||
installed: {
|
||||
label: '裝過了(安裝器命名慣例 arcrun-rag-<instance>-kv-<binding>)',
|
||||
deployed: true,
|
||||
titleFor: (b) => `arcrun-rag-yuga3bse-kv-${b.toLowerCase()}`,
|
||||
titleFor: (b) => `${BASE_NAME}-kv-${b.toLowerCase()}`,
|
||||
},
|
||||
renamed: {
|
||||
label: '資源在,但名字與預期完全不同(使用者自己改過/別的安裝器版本取的名)',
|
||||
@@ -70,8 +77,43 @@ export const SCENARIOS = {
|
||||
// 刻意取成跟 binding 名毫無關聯的字串:只要規則有一絲「照名字對號」就會在這裡露餡。
|
||||
titleFor: (b) => `kv-${[...b].reduce((h, c) => (h * 31 + c.charCodeAt(0)) >>> 0, 7).toString(36)}`,
|
||||
},
|
||||
'half-finished': {
|
||||
label: '上一次裝到一半死掉(KV/D1 已建在帳號上,一顆 worker 都還沒部署)— Arcrun#123',
|
||||
deployed: false,
|
||||
resourcesExist: true,
|
||||
titleFor: (b) => `${BASE_NAME}-kv-${b.toLowerCase()}`,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 安裝器那條路的需求清單:`createName` 是**安裝器自己替這台實例算出來的**,
|
||||
* 所以它有資格聲明 `createNameIsOurs`(見 rule.mjs 該欄位的推導條件)。
|
||||
*
|
||||
* 對照 `requirements()`(走 wrangler.toml,createName 是裸 binding 名 ⇒ **不得**聲明)。
|
||||
*
|
||||
* @param {boolean} [claimOwnership] 預設 true;傳 false 就是「安裝器忘了聲明」的對照組。
|
||||
*/
|
||||
export function installerRequirements(claimOwnership = true) {
|
||||
const out = [];
|
||||
for (const [worker, need] of Object.entries(WORKER_NEEDS)) {
|
||||
for (const b of need.kv) {
|
||||
out.push({
|
||||
kind: 'kv_namespace', binding: b, worker,
|
||||
createName: `${BASE_NAME}-kv-${b.toLowerCase()}`,
|
||||
...(claimOwnership ? { createNameIsOurs: true } : {}),
|
||||
});
|
||||
}
|
||||
for (const d of need.d1) {
|
||||
out.push({
|
||||
kind: 'd1', binding: d.binding, worker,
|
||||
createName: `${BASE_NAME}-kbdb`,
|
||||
...(claimOwnership ? { createNameIsOurs: true } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 建一個假帳號 + 對應的 `fetch` 替身。
|
||||
*
|
||||
@@ -109,15 +151,18 @@ export function makeAccount(scenario) {
|
||||
const kvIdByBinding = new Map();
|
||||
const D1_ID = 'd1id-kbdb-REAL';
|
||||
|
||||
if (spec.deployed) {
|
||||
// 資源存不存在,與 worker 部署了沒,是**兩件事**(#123:中斷的安裝會讓前者為真、後者為假)。
|
||||
if (spec.resourcesExist ?? spec.deployed) {
|
||||
// 帳號上已經有的資源(名字照該情境的慣例取,id 才是身分)
|
||||
for (const b of KV_BINDINGS) {
|
||||
const id = `kvid-${b.toLowerCase()}-REAL`;
|
||||
kv.set(spec.titleFor(b), id);
|
||||
kvIdByBinding.set(b, id);
|
||||
}
|
||||
d1.set('arcrun-rag-yuga3bse-kbdb', D1_ID);
|
||||
d1.set(`${BASE_NAME}-kbdb`, D1_ID);
|
||||
}
|
||||
|
||||
if (spec.deployed) {
|
||||
// 已部署的 worker 上綁著它們——**這才是規則要看的事實**
|
||||
for (const [script, need] of Object.entries(WORKER_NEEDS)) {
|
||||
const bindings = [];
|
||||
|
||||
Reference in New Issue
Block a user