Archived
三面拆分⑰:编辑即其身 —— 保存写回本版,新版本只出自「新建」
用户问:为什么编辑一个版本、保存就自动多一个副本?那不是我要的。
旧机制「保存恒新增版本号」改为:
- savePackage:pkg.version 已存在 → **就地写回该版**(号不变、状态不变);
`asNew: true`(新建按钮)或新流程首版才铸新版本号(服务端算,不生效)
- 底线三道,一道不少:
① **在途钉版不可变** —— 该版有未完结工作流(按 (包,版本) 钉定义)→ 409,
一字不落库;「就地覆盖会改掉在途正在读的编排」由这道闸守,不再靠只插不改一刀切
② **生效版必须始终可运行** —— 写回生效版跑闭合闸,不闭合 412 带整份报告;
未生效版画图不检查、随手存
③ **陈旧编辑不复活** —— 流程存在而版本号已被清理 → 409(实测:停在已删旧版上的
标签页发来保存,把老内容静默复活成了新版本;StaleVersionError 拦死)
- store:新增 updatePackageDefinition(只许命中现存版本)+ countPinnedInFlight
- 前端:保存按钮「保存 v{n}」;新建走 asNew;文案全线「新建才长版本」
- 测试:save-package.test 六例(写回/钉版/热改闭合/asNew/防复活/首版);
过程中揪出双 API 进程并存(旧进程霸端口跑旧代码)——已重启归一
实测:改名保存 → 版本列表仍唯一 v4 生效中,名字已写回;八例端到端全绿;
五闸退出码全过(api:check 提交后转绿)。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* 保存 —— **编辑即其身**(产品决策 2026-07-28 第四轮):
|
||||
* 写回当前版本号不变;新版本只出自显式新建(asNew)或新流程首版。
|
||||
* 两道底线:在途钉版不可变;生效版写回必须闭合(生效的必须始终可运行)。
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { processPackageSchema } from '@contracts';
|
||||
import type { ProcessPackage } from '@contracts';
|
||||
import { NotClosedError, PinnedVersionError, StaleVersionError } from '../contract.ts';
|
||||
import { createProcessService } from '../internal/usecase/process-service.ts';
|
||||
import type { ProcessServiceDeps } from '../internal/usecase/process-service.ts';
|
||||
|
||||
const basePkg = (version: number, state: 'active' | 'retired'): ProcessPackage =>
|
||||
processPackageSchema.parse({
|
||||
id: 'p1',
|
||||
name: '包',
|
||||
version,
|
||||
state,
|
||||
callInterface: { input: { type: 'object' }, output: { type: 'object' } },
|
||||
nodes: [
|
||||
{ id: 'S', kind: 'start', label: '起' },
|
||||
{ id: 'E', kind: 'end', label: '终' },
|
||||
],
|
||||
edges: [{ id: 'E1', from: 'S', to: 'E', mapping: [], reopen: false }],
|
||||
variables: [],
|
||||
});
|
||||
|
||||
function harness(seed: ProcessPackage[], pinned = 0) {
|
||||
const shelf = [...seed];
|
||||
const updated: Array<{ id: string; version: number }> = [];
|
||||
const inserted: ProcessPackage[] = [];
|
||||
const deps: ProcessServiceDeps = {
|
||||
store: {
|
||||
savePackage: async (pkg) => {
|
||||
inserted.push(pkg);
|
||||
shelf.push(pkg);
|
||||
},
|
||||
updatePackageDefinition: async (id, version, pkg) => {
|
||||
const at = shelf.findIndex((p) => p.id === id && p.version === version);
|
||||
if (at < 0) throw new Error('版本不存在');
|
||||
shelf[at] = pkg;
|
||||
updated.push({ id, version });
|
||||
},
|
||||
countPinnedInFlight: async () => pinned,
|
||||
setPackageState: async () => undefined,
|
||||
getPackage: async (id, version) =>
|
||||
version === undefined
|
||||
? [...shelf].sort((a, b) => b.version - a.version).find((p) => p.id === id)
|
||||
: shelf.find((p) => p.id === id && p.version === version),
|
||||
listPackages: async () => shelf,
|
||||
insertWorkflow: async () => undefined,
|
||||
getWorkflow: async () => undefined,
|
||||
listWorkflows: async () => [],
|
||||
updateWorkflow: async () => {
|
||||
throw new Error('测试:不该走到 updateWorkflow');
|
||||
},
|
||||
listInstances: async () => [],
|
||||
insertInstance: async () => undefined,
|
||||
updateInstance: async () => undefined,
|
||||
findInstanceByTask: async () => undefined,
|
||||
},
|
||||
activities: { faces: async () => new Map() },
|
||||
org: {
|
||||
positions: async () => new Set<string>(['PM']),
|
||||
candidates: async () => [],
|
||||
workGroupBinding: async () => undefined,
|
||||
isActive: async () => true,
|
||||
},
|
||||
tasks: { start: async () => ({ id: 't' }) },
|
||||
approvals: { raise: async () => ({ id: 'a' }) },
|
||||
|
||||
sfeel: { condition: () => true, expression: () => undefined },
|
||||
schemas: { validate: () => ({ ok: true, errors: [] }) },
|
||||
events: { publish: async () => undefined },
|
||||
receipts: { notify: async () => undefined },
|
||||
clock: { now: () => new Date('2026-07-28T00:00:00.000Z') },
|
||||
ids: { next: () => 'x' },
|
||||
};
|
||||
return { service: createProcessService(deps), shelf, updated, inserted };
|
||||
}
|
||||
|
||||
describe('savePackage —— 编辑即其身', () => {
|
||||
it('已存在的版本就地写回:号不变、状态不变、不长新版', async () => {
|
||||
const h = harness([basePkg(4, 'retired')]);
|
||||
const edited = { ...basePkg(4, 'retired'), name: '改名了' };
|
||||
const saved = await h.service.savePackage(edited);
|
||||
expect(saved.version).toBe(4);
|
||||
expect(saved.state).toBe('retired');
|
||||
expect(h.updated).toEqual([{ id: 'p1', version: 4 }]);
|
||||
expect(h.inserted).toHaveLength(0);
|
||||
expect(h.shelf).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('在途钉版不可变 —— 有未完结工作流即拒,一字不落库', async () => {
|
||||
const h = harness([basePkg(4, 'retired')], 2);
|
||||
await expect(h.service.savePackage(basePkg(4, 'retired'))).rejects.toBeInstanceOf(PinnedVersionError);
|
||||
expect(h.updated).toHaveLength(0);
|
||||
expect(h.inserted).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('写回生效版须闭合 —— 生效的必须始终可运行', async () => {
|
||||
const h = harness([basePkg(4, 'active')]);
|
||||
// 把唯一一条边砍掉:起点无出边、终点无入边 —— 不闭合
|
||||
const broken = { ...basePkg(4, 'active'), edges: [] };
|
||||
await expect(h.service.savePackage(broken)).rejects.toBeInstanceOf(NotClosedError);
|
||||
expect(h.updated).toHaveLength(0);
|
||||
|
||||
// 闭合的改动照常写回,生效状态保持
|
||||
const ok = { ...basePkg(4, 'active'), name: '热改' };
|
||||
const saved = await h.service.savePackage(ok);
|
||||
expect(saved.state).toBe('active');
|
||||
expect(h.updated).toEqual([{ id: 'p1', version: 4 }]);
|
||||
});
|
||||
|
||||
it('asNew 显式新建:铸下一个版本号,不生效 —— 新版本只出自这条路', async () => {
|
||||
const h = harness([basePkg(4, 'active')]);
|
||||
const saved = await h.service.savePackage(basePkg(4, 'active'), { asNew: true });
|
||||
expect(saved.version).toBe(5);
|
||||
expect(saved.state).toBe('retired');
|
||||
expect(h.inserted).toHaveLength(1);
|
||||
expect(h.updated).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('陈旧编辑拒之门外:流程存在而版本号已不存在 —— 不静默复活旧内容', async () => {
|
||||
const h = harness([basePkg(4, 'active')]);
|
||||
// 停在已清理旧版(v1)上的标签页发来保存
|
||||
await expect(h.service.savePackage(basePkg(1, 'retired'))).rejects.toBeInstanceOf(StaleVersionError);
|
||||
expect(h.inserted).toHaveLength(0);
|
||||
expect(h.updated).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('新流程首版:版本不存在即插入 v1,不生效', async () => {
|
||||
const h = harness([]);
|
||||
const saved = await h.service.savePackage(basePkg(1, 'active'));
|
||||
expect(saved.version).toBe(1);
|
||||
expect(saved.state).toBe('retired');
|
||||
expect(h.inserted).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user