发布用 packageAll 落定的权威表单合成 metadata

- useTaonierExport 维护 effectiveFormRef,refresh 采纳新表单与打包前 flush 成功时同步
- 暴露 getEffectiveForm;publishGame 在 await packageAll() 之后用它,避免旧快照与宿主读盘不一致
- 补两条单测:采纳新表单后权威表单更新、未保存输入先落盘再作为权威表单
This commit is contained in:
2026-10-07 11:41:27 +08:00
parent 800c456804
commit ab2af60d30
3 changed files with 86 additions and 1 deletions
@@ -261,6 +261,14 @@ export function useTaonierExport({
/** 上一次抛进错误池的故障指纹:同一个只抛一次,见 [useTaonierExport.throwToErrorPool]。 */
const reportedFailureRef = useRef('');
const formRef = useRef<TaonierExportForm | null>(null);
/**
* 已经落定到磁盘的权威表单:`refresh` 采纳新内容、`packageAll` flush 成功后同步。
*
* 发布链路在 `await packageAll()` 之后必须用它合成 metadata:`packageAll` 内部的 `refresh`
* 可能在用户无未保存输入时采纳一份更新的注册表表单,而宿主发布命令读的正是这份磁盘表单;
* 继续用 await 之前的快照会让文字字段与媒体字节对不上。
*/
const effectiveFormRef = useRef<TaonierExportForm | null>(null);
/**
* 当前项目路径的即时值。异步结果(读内容、取指纹、保存、构建、读线上值)回来时用它判断结果还
* 属不属于当前项目:面板根跨项目不卸载,旧请求回来时绝不能把上一个项目的表单 / 基线 / 线上值写
@@ -309,6 +317,7 @@ export function useTaonierExport({
initialReadSettledRef.current = false;
reportedFailureRef.current = '';
formRef.current = null;
effectiveFormRef.current = null;
}, [projectPath]);
// 脚本出现之后就把提示重新武装:它要是又被删掉,用户该再看到一次阻断提示。
@@ -379,6 +388,7 @@ export function useTaonierExport({
return content.hasScript;
}
setForm(content.form);
effectiveFormRef.current = content.form;
setFieldErrors([]);
setConflict(null);
// 这里不再清 `failure`:上面的函数式清理只清读来源的失败,构建类失败是那次构建的现场,
@@ -766,6 +776,8 @@ export function useTaonierExport({
if (dirty && form && !conflict && fieldErrors.length === 0) {
const flushed = await persist(form, baselineRef.current);
if (!flushed) return false;
// flush 成功后磁盘上的就是这份;发布链路要用它而不是调用前的旧快照。
effectiveFormRef.current = form;
}
// 没解决的冲突 / 字段红字:盘上的内容和用户看到的不一致,先别打包。
if (conflict || fieldErrors.length > 0) return false;
@@ -907,6 +919,8 @@ export function useTaonierExport({
adoptOnlineText,
resolveConflict,
packageAll,
/** `await packageAll()` 之后取「这次真正落定的表单」;还没落定时返回 `null`。 */
getEffectiveForm: () => effectiveFormRef.current,
adapt,
repair,
fillForm,
@@ -235,7 +235,10 @@ export function useTaonierTab({
// 订阅失败只影响阶段条:发布本身照常进行,按钮上仍显示「正在发布…」。
}
try {
const metadata = buildTaonierPublishMetadata(form, online);
// `packageAll` 内部可能采纳一份更新的注册表表单(并已落盘),宿主发布命令读的正是那份;
// 拿它合成 metadata,避免文字字段来自 await 之前的旧快照、与媒体字节对不上。
const effectiveForm = taonier.getEffectiveForm() ?? form;
const metadata = buildTaonierPublishMetadata(effectiveForm, online);
const next = await publishLocalProjectGame({
invoke,
projectPath,
@@ -511,6 +511,74 @@ describe('hook 状态', () => {
expect(result.current.failure?.type).toBe('commandFailed');
});
it('packageAll 采纳新表单后,发布取到的权威表单跟着更新', async () => {
let registryForm: TaonierExportForm = FORM;
const invoke = vi.fn(async (command: string) => {
switch (command) {
case 'read_taonier_export':
return { form: registryForm, hasScript: true };
case 'read_taonier_export_hash':
return CONTENT_HASH;
case 'read_game_distribution_publication':
return ONLINE_FIRST_PUBLISH;
case 'save_taonier_export_form':
return 'hash-2';
case 'run_taonier_export_build':
return { outputTail: '', omittedCharacters: 0 };
default:
return undefined;
}
});
installTauriRuntime({ core: { invoke } });
const { result } = renderHook(() =>
useTaonierExport({
enabled: true,
projectPath: PROJECT_PATH,
submitTurn: vi.fn(async () => true),
}),
);
await waitFor(() => expect(result.current.form).toEqual(FORM));
// 面板背后注册表被改(例如 agent 重写 .export/taonier.json)。用户没有未保存输入时,
// packageAll 的 refresh 会采纳这份新表单;发布必须拿同一份合成 metadata,否则文字字段
// 会停在 await 之前的旧快照,与宿主读盘拿到的媒体字节对不上。
registryForm = { ...FORM, title: '项目里的新名字' };
await act(async () => {
await result.current.packageAll();
});
expect(result.current.form?.title).toBe('项目里的新名字');
expect(result.current.getEffectiveForm()?.title).toBe('项目里的新名字');
});
it('有未保存输入时 packageAll 先落盘,权威表单就是这份输入', async () => {
const invoke = createInvoke();
installTauriRuntime({ core: { invoke } });
const { result } = renderHook(() =>
useTaonierExport({
enabled: true,
projectPath: PROJECT_PATH,
submitTurn: vi.fn(async () => true),
}),
);
await waitFor(() => expect(result.current.form).toEqual(FORM));
await act(async () => {
result.current.updateField('title', '刚敲的名字');
});
await act(async () => {
await result.current.packageAll();
});
expect(invoke).toHaveBeenCalledWith(
'save_taonier_export_form',
expect.objectContaining({
form: expect.objectContaining({ title: '刚敲的名字' }),
}),
);
expect(result.current.getEffectiveForm()?.title).toBe('刚敲的名字');
});
it('校验不过时停在表单:不落盘、不跑构建(校验不过不触宿主)', async () => {
const invoke = createInvoke();
installTauriRuntime({ core: { invoke } });