修复 AGC 生成准备依赖全量素材库的问题 (#592)
Project CI / AI game creator shell Rust crates (push) Successful in 1m47s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 5m1s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 5m18s
Project CI / Frontend tests (push) Successful in 2m27s
Project CI / Backend tests (push) Successful in 5m49s
Project CI / Native shell tests (push) Successful in 6m21s
Project CI / Repository checks (push) Successful in 3m36s
Project CI / AI game creator shell web tests (push) Successful in 2m54s
Project CI / AI game creator shell Rust crates (push) Successful in 1m47s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 5m1s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 5m18s
Project CI / Frontend tests (push) Successful in 2m27s
Project CI / Backend tests (push) Successful in 5m49s
Project CI / Native shell tests (push) Successful in 6m21s
Project CI / Repository checks (push) Successful in 3m36s
Project CI / AI game creator shell web tests (push) Successful in 2m54s
修复已有项目和素材目录绑定时,AGC 生成准备仍读取完整账户素材库、导致无关素材读取失败阻断生成的问题。 - 新增站内与 External v1 的单目录 GET,按主键读取并校验账号归属;复用目录 DTO,不读取素材列表或创建默认目录。 - 已有 binding 和 partial 恢复改为确认目标目录;首次建绑直接沿用稳定幂等创建,不再读取全库。 - 目录缺失、越权、超时、响应身份不符或会话切换时停止,保留恢复证据,不自动重建、不换键重发生成;已有生成账本继续沿用冻结快照恢复。 - 既有素材库与目录创建、修改、删除 API 合同保持不变,持久化表结构不变。同步 OpenAPI、生成绑定及技术文档。 验证: - AGC Rust:目录绑定 7 项、生成 94 项、资源编辑 60 项、抠图 5 项通过(目录绑定用例包含在生成组中)。 - api-server:External Editor 30 项、受保护路由矩阵 3 项通过。 - 真实隔离 SpacetimeDB:目录读取成功、空目录、缺失、越权、删除后缺失、只读不变,以及既有幂等/并发/孤儿检查通过。 - smoke 脚本测试 4 项通过;module 编译、生成绑定、87 张表 schema guard、DDD、编码、文档索引、定向 Rust 格式与 diff 检查通过。 - 已比较 OpenAPI:除新增单目录 GET 外,原有定义逐项保持一致。 验证限制:未完成独立 API `/healthz` 联调,本地隔离数据库与授权配置未就绪;启动尝试已停止并清理。未运行全量测试或真实付费生成。 发布顺序:先发布后端模块与 API,再发布新客户端;不提供全库回退。#574 的素材分页、筛选及按素材 ID 读取不在本 PR 范围内。 Closes #550 Reviewed-on: #592 Co-authored-by: Linghong <ink29535@proton.me> Co-committed-by: Linghong <ink29535@proton.me>
This commit was merged in pull request #592.
This commit is contained in:
@@ -384,6 +384,28 @@ function assertDeletedConflict(result, recordLabel) {
|
||||
}
|
||||
|
||||
async function runSmoke(configPath, serverUrl) {
|
||||
const readFolder = (folderId, ownerUserId = 'receipt-owner') =>
|
||||
callProcedure(configPath, serverUrl, 'get_editor_asset_folder_and_return', {
|
||||
folder_id: folderId,
|
||||
owner_user_id: ownerUserId,
|
||||
});
|
||||
const missingBeforeCreate = await readFolder('missing-folder', 'empty-owner');
|
||||
assert(
|
||||
missingBeforeCreate.ok === false &&
|
||||
missingBeforeCreate.errorMessage === '素材文件夹不存在',
|
||||
'Missing folder read did not return the missing-folder error.',
|
||||
);
|
||||
assert(
|
||||
(
|
||||
await sqlRows(
|
||||
configPath,
|
||||
serverUrl,
|
||||
'SELECT folder_id FROM editor_asset_folder',
|
||||
'folders after missing read',
|
||||
)
|
||||
).length === 0,
|
||||
'Missing folder read created a default folder.',
|
||||
);
|
||||
const projectA = projectInput('project-a', 'First');
|
||||
assertOk(
|
||||
await callProcedure(
|
||||
@@ -471,6 +493,73 @@ async function runSmoke(configPath, serverUrl) {
|
||||
'asset folder drift replay',
|
||||
);
|
||||
|
||||
const folderRowsBeforeRead = await sqlRows(
|
||||
configPath,
|
||||
serverUrl,
|
||||
'SELECT * FROM editor_asset_folder',
|
||||
'folders before read',
|
||||
);
|
||||
const receiptsBeforeRead = await sqlRows(
|
||||
configPath,
|
||||
serverUrl,
|
||||
'SELECT * FROM editor_idempotent_create_receipt',
|
||||
'receipts before read',
|
||||
);
|
||||
assert(
|
||||
(
|
||||
await sqlRows(
|
||||
configPath,
|
||||
serverUrl,
|
||||
'SELECT asset_id FROM editor_asset',
|
||||
'assets before folder read',
|
||||
)
|
||||
).length === 0,
|
||||
'Folder read fixture must have no assets.',
|
||||
);
|
||||
const emptyFolder = await readFolder('folder-a');
|
||||
assertOk(emptyFolder, 'empty asset folder read');
|
||||
assert(
|
||||
emptyFolder.record[0] === 'folder-a' &&
|
||||
emptyFolder.record[1] === 'Folder A',
|
||||
'Folder read returned a different folder.',
|
||||
);
|
||||
const foreignFolder = await readFolder('folder-a', 'other-owner');
|
||||
assert(
|
||||
foreignFolder.ok === false &&
|
||||
foreignFolder.record === null &&
|
||||
foreignFolder.errorMessage === '无权访问该素材文件夹',
|
||||
'Foreign-owner folder read did not reject access.',
|
||||
);
|
||||
const missingFolder = await readFolder('missing-folder');
|
||||
assert(
|
||||
missingFolder.ok === false &&
|
||||
missingFolder.record === null &&
|
||||
missingFolder.errorMessage === '素材文件夹不存在',
|
||||
'Missing folder read did not fail closed.',
|
||||
);
|
||||
assert(
|
||||
JSON.stringify(
|
||||
await sqlRows(
|
||||
configPath,
|
||||
serverUrl,
|
||||
'SELECT * FROM editor_asset_folder',
|
||||
'folders after read',
|
||||
),
|
||||
) === JSON.stringify(folderRowsBeforeRead),
|
||||
'Folder reads changed folder rows or timestamps.',
|
||||
);
|
||||
assert(
|
||||
JSON.stringify(
|
||||
await sqlRows(
|
||||
configPath,
|
||||
serverUrl,
|
||||
'SELECT * FROM editor_idempotent_create_receipt',
|
||||
'receipts after read',
|
||||
),
|
||||
) === JSON.stringify(receiptsBeforeRead),
|
||||
'Folder reads changed durable receipts.',
|
||||
);
|
||||
|
||||
const sameRaceInput = projectInput('project-same-race', 'Same Race');
|
||||
const sameRace = await Promise.all([
|
||||
callProcedure(
|
||||
@@ -641,6 +730,12 @@ async function runSmoke(configPath, serverUrl) {
|
||||
},
|
||||
);
|
||||
assertOk(deleteFolderResult, 'asset folder delete');
|
||||
const deletedFolder = await readFolder('folder-a');
|
||||
assert(
|
||||
deletedFolder.ok === false &&
|
||||
deletedFolder.errorMessage === '素材文件夹不存在',
|
||||
'Deleted folder read did not return the missing-folder error.',
|
||||
);
|
||||
assertDeletedConflict(
|
||||
await callProcedure(
|
||||
configPath,
|
||||
@@ -757,7 +852,7 @@ export async function main() {
|
||||
|
||||
await runSmoke(configPath, serverUrl);
|
||||
console.log(
|
||||
'[editor-idempotency-smoke] Passed exact replay, body conflicts, deletion fail-close, concurrent calls, and orphan checks.',
|
||||
'[editor-idempotency-smoke] Passed folder reads (empty, missing, foreign owner, no writes), exact replay, body conflicts, deletion fail-close, concurrent calls, and orphan checks.',
|
||||
);
|
||||
} catch (error) {
|
||||
const standaloneOutput = sanitizeDiagnostic(
|
||||
|
||||
Reference in New Issue
Block a user