feat(conversation): replicate in-place user message editing (TODO 03)
This commit is contained in:
1 parent
ed31a7c1b4
commit
ee51208931
18 files changed
+992
-67
No files matched your search
@@ -355,6 +355,60 @@ async function testActionBranchFailure(bundle: TestBundle, state: TestState): Pr
|
||||
'failed action branch should restore the prior marker')
|
||||
}
|
||||
|
||||
async function testEditArtifactRollback(bundle: TestBundle, state: TestState, workspace: string): Promise<void> {
|
||||
const invoke = invokeFor(state)
|
||||
// 有产物:编辑提交先回退文件,再从该消息处重新开始对话(原版 userMessage.edit.hint)。
|
||||
const rolledPath = join(workspace, 'edit-rollback.txt')
|
||||
const rolledSession = await seedWriteHistory(bundle, state, 'edit-rollback', rolledPath, 'edit-rollback-native-target', 'after')
|
||||
await invoke('chat:send', { sessionId: rolledSession, text: 'edited prompt', action: 'edit', fromMessageId: 'edit-rollback-user' })
|
||||
const rolledMessages = await waitForAssistant(invoke, rolledSession)
|
||||
assert.equal(await readFile(rolledPath, 'utf8'), 'before',
|
||||
'edit submit must roll the artifact back to its version before the message')
|
||||
assert.equal(rolledMessages.length, 2, 'edit keeps the prefix before the edited message and appends the replacement')
|
||||
assert.equal(rolledMessages[0].content, 'edited prompt')
|
||||
assert.ok(rolledMessages[0].nativeEntryId?.startsWith('native-prompt-'), 'replacement turn should receive the new native entryId')
|
||||
|
||||
// 分支失败:撤销文件回退,保持会话不变。
|
||||
const failedPath = join(workspace, 'edit-rollback-branch-failure.txt')
|
||||
const failedSession = await seedWriteHistory(bundle, state, 'edit-rollback-failed', failedPath, 'edit-rollback-failed-native', 'after')
|
||||
const failedBefore = await bundle.sessions.getMessages(failedSession)
|
||||
state.failBranchIds.add('edit-rollback-failed-native')
|
||||
await assert.rejects(invoke('chat:send', {
|
||||
sessionId: failedSession, text: 'edited failed', action: 'edit', fromMessageId: 'edit-rollback-failed-user'
|
||||
}), /branch failure/)
|
||||
state.failBranchIds.delete('edit-rollback-failed-native')
|
||||
assert.equal(await readFile(failedPath, 'utf8'), 'after', 'failed native branch must undo the artifact rollback')
|
||||
assert.deepEqual(await bundle.sessions.getMessages(failedSession), failedBefore,
|
||||
'failed edit must leave the persisted conversation untouched')
|
||||
|
||||
// 无产物改动:直接重开对话,不因缺少文件记录而报错。
|
||||
const plainSession = await invoke('session:create', 'edit-plain') as { id: string }
|
||||
await bundle.sessions.appendMessage(plainSession.id, message('edit-plain-user', 'user', 'plain prompt', { nativeEntryId: 'edit-plain-native' }))
|
||||
await bundle.sessions.appendMessage(plainSession.id, message('edit-plain-assistant', 'assistant', 'plain reply'))
|
||||
seedNativeHistory(state, plainSession.id, [['edit-plain-native', 'plain prompt']])
|
||||
await invoke('chat:send', { sessionId: plainSession.id, text: 'edited plain', action: 'edit', fromMessageId: 'edit-plain-user' })
|
||||
const plainMessages = await waitForAssistant(invoke, plainSession.id)
|
||||
assert.equal(plainMessages.length, 2)
|
||||
assert.equal(plainMessages[0].content, 'edited plain')
|
||||
|
||||
// 记录不完整(不可回滚工具):拒绝编辑,会话与文件都不变。
|
||||
const blockedPath = join(workspace, 'edit-blocked.txt')
|
||||
await writeFile(blockedPath, 'after', 'utf8')
|
||||
const blockedSession = await invoke('session:create', 'edit-blocked') as { id: string }
|
||||
await bundle.sessions.appendMessage(blockedSession.id, message('edit-blocked-user', 'user', 'blocked prompt', { nativeEntryId: 'edit-blocked-native' }))
|
||||
await bundle.sessions.appendMessage(blockedSession.id, message('edit-blocked-assistant', 'assistant', 'ran shell', {
|
||||
tools: [{ id: 'shell-tool', name: 'run_command', args: {}, argsRaw: '{}', status: 'ok' }]
|
||||
}))
|
||||
seedNativeHistory(state, blockedSession.id, [['edit-blocked-native', 'blocked prompt']])
|
||||
const blockedBefore = await bundle.sessions.getMessages(blockedSession.id)
|
||||
await assert.rejects(invoke('chat:send', {
|
||||
sessionId: blockedSession.id, text: 'edited blocked', action: 'edit', fromMessageId: 'edit-blocked-user'
|
||||
}), /无法完整回滚/)
|
||||
assert.equal(await readFile(blockedPath, 'utf8'), 'after', 'rejected edit must not touch artifacts')
|
||||
assert.deepEqual(await bundle.sessions.getMessages(blockedSession.id), blockedBefore,
|
||||
'rejected edit must leave history untouched')
|
||||
}
|
||||
|
||||
async function testInterruptedResend(bundle: TestBundle, state: TestState): Promise<void> {
|
||||
const invoke = invokeFor(state)
|
||||
for (const nativeAccepted of [false, true]) {
|
||||
@@ -684,6 +738,7 @@ async function main(): Promise<void> {
|
||||
await testConcurrency(bundle, state, invoke)
|
||||
for (const action of ['edit', 'resend', 'regenerate'] as const) await testActionBranch(bundle, state, action)
|
||||
await testActionBranchFailure(bundle, state)
|
||||
await testEditArtifactRollback(bundle, state, workspace)
|
||||
await testInterruptedResend(bundle, state)
|
||||
await testAttachments(bundle, state, tempRoot)
|
||||
await testSpeech(bundle, state)
|
||||
|
||||
Reference in new issue
Block a user