返回 oh-my-ppt
git-history-style-snapshot.test.ts
根目录 / tests / unit / history / git-history-style-snapshot.test.ts
1 import { describe, expect, it, vi } from 'vitest'
2
3 const logMocks = vi.hoisted(() => ({
4 debug: vi.fn(),
5 info: vi.fn(),
6 warn: vi.fn(),
7 error: vi.fn()
8 }))
9
10 vi.mock('electron-log/main.js', () => ({ default: logMocks }))
11
12 import { GitHistoryService } from '../../../src/main/history/git-history-service'
13 import type { SessionStyleSnapshotRow } from '../../../src/main/db/database'
14
15 const styleSnapshot = (
16 sessionId: string,
17 styleId: string,
18 styleName: string
19 ): SessionStyleSnapshotRow => ({
20 id: `snapshot-${styleId}`,
21 sessionId,
22 styleId,
23 styleKey: styleId,
24 styleName,
25 styleNameZh: styleName,
26 styleNameEn: styleName,
27 description: `${styleName} description`,
28 category: 'test',
29 aliases: '[]',
30 source: 'custom',
31 version: '1.0.0',
32 styleCase: '',
33 packageDir: `user/${styleId}`,
34 styleSkill: `${styleName} skill`,
35 createdAt: 1
36 })
37
38 describe('git history style snapshot', () => {
39 it('captures the exact session style state in operation metadata', async () => {
40 const snapshot = styleSnapshot('session-1', 'style-old', '旧风格')
41 const db = {
42 getSession: vi.fn().mockResolvedValue({
43 id: 'session-1',
44 styleId: 'style-old',
45 metadata: JSON.stringify({ locale: 'zh' }),
46 designContract: JSON.stringify({ theme: 'old-theme' })
47 }),
48 getSessionStyleSnapshot: vi.fn().mockResolvedValue(snapshot)
49 }
50 const service = new GitHistoryService(db as never)
51
52 const metadata = await (service as any).buildOperationMetadata({
53 sessionId: 'session-1',
54 metadata: { reason: 'test' }
55 })
56
57 expect(metadata).toMatchObject({
58 reason: 'test',
59 sessionMetadata: { locale: 'zh' },
60 sessionStyleState: {
61 styleId: 'style-old',
62 snapshot,
63 designContract: { theme: 'old-theme' }
64 }
65 })
66 })
67
68 it('restores the target version style id and snapshot during rollback', async () => {
69 const currentSnapshot = styleSnapshot('session-1', 'style-new', '新风格')
70 const targetSnapshot = styleSnapshot('session-1', 'style-old', '旧风格')
71 const restoreSessionStyleState = vi.fn().mockResolvedValue(undefined)
72 const db = {
73 getSession: vi.fn().mockResolvedValue({
74 id: 'session-1',
75 status: 'completed',
76 styleId: 'style-new',
77 metadata: '{}',
78 designContract: JSON.stringify({ theme: 'new-theme' }),
79 currentOperationId: 'operation-new',
80 currentCommit: 'commit-new'
81 }),
82 getSessionOperation: vi.fn().mockResolvedValue({
83 id: 'operation-old',
84 session_id: 'session-1',
85 status: 'completed',
86 after_commit: 'commit-old',
87 tracked_files_json: JSON.stringify(['index.html', 'page-1.html']),
88 metadata_json: JSON.stringify({
89 sessionMetadata: { locale: 'zh' },
90 sessionStyleState: {
91 styleId: 'style-old',
92 snapshot: targetSnapshot,
93 designContract: { theme: 'old-theme' }
94 }
95 })
96 }),
97 listSessionPages: vi.fn().mockResolvedValue([]),
98 getSessionStyleSnapshot: vi.fn().mockResolvedValue(currentSnapshot),
99 updateSessionHistoryPointer: vi.fn().mockResolvedValue(undefined),
100 updateSessionMetadata: vi.fn().mockResolvedValue(undefined),
101 updateSessionDesignContract: vi.fn().mockResolvedValue(undefined),
102 restoreSessionStyleState
103 }
104 const service = new GitHistoryService(db as never)
105 const internals = service as any
106 internals.ensureRepository = vi.fn().mockResolvedValue(undefined)
107 internals.resolveHead = vi.fn().mockResolvedValue('commit-new')
108 internals.assertCommitExists = vi.fn().mockResolvedValue(undefined)
109 internals.listTrackedFiles = vi.fn().mockResolvedValue(['index.html', 'page-1.html'])
110 internals.restoreCommitFiles = vi.fn().mockResolvedValue(undefined)
111 internals.syncSessionPagesForRestoredVersion = vi.fn().mockResolvedValue(undefined)
112 internals.moveHeadToCommit = vi.fn().mockResolvedValue(undefined)
113
114 await service.rollbackToVersion({
115 sessionId: 'session-1',
116 projectDir: '/tmp/session-1',
117 versionId: 'operation-old'
118 })
119
120 expect(restoreSessionStyleState).toHaveBeenCalledTimes(1)
121 expect(restoreSessionStyleState).toHaveBeenCalledWith('session-1', 'style-old', targetSnapshot)
122 expect(db.updateSessionDesignContract).toHaveBeenCalledWith('session-1', {
123 theme: 'old-theme'
124 })
125 expect(logMocks.info).toHaveBeenCalledWith(
126 '[history] restored session style snapshot',
127 expect.objectContaining({
128 sessionId: 'session-1',
129 versionId: 'operation-old',
130 styleId: 'style-old'
131 })
132 )
133 })
134
135 it('backfills the current history version before a style switch', async () => {
136 const snapshot = styleSnapshot('session-1', 'style-old', '旧风格')
137 const updateSessionOperationMetadata = vi.fn().mockResolvedValue(undefined)
138 const db = {
139 getSession: vi.fn().mockResolvedValue({
140 id: 'session-1',
141 styleId: 'style-old',
142 currentOperationId: 'operation-current',
143 designContract: JSON.stringify({ theme: 'old-theme' })
144 }),
145 getSessionOperation: vi.fn().mockResolvedValue({
146 id: 'operation-current',
147 session_id: 'session-1',
148 metadata_json: JSON.stringify({ runId: 'run-old' })
149 }),
150 getSessionStyleSnapshot: vi.fn().mockResolvedValue(snapshot),
151 updateSessionOperationMetadata
152 }
153 const service = new GitHistoryService(db as never)
154
155 await service.captureCurrentVersionStyleState('session-1')
156
157 expect(updateSessionOperationMetadata).toHaveBeenCalledWith('operation-current', {
158 runId: 'run-old',
159 sessionStyleState: {
160 styleId: 'style-old',
161 snapshot,
162 designContract: { theme: 'old-theme' }
163 }
164 })
165 })
166
167 it('compensates a committed path-scoped page operation without touching other page work', async () => {
168 const completeSessionOperation = vi.fn().mockResolvedValue(undefined)
169 const updateSessionHistoryPointer = vi.fn().mockResolvedValue(undefined)
170 const db = { completeSessionOperation, updateSessionHistoryPointer }
171 const service = new GitHistoryService(db as never)
172 const internals = service as any
173 internals.moveHeadToCommit = vi.fn().mockResolvedValue(undefined)
174 internals.restoreCommitPaths = vi.fn().mockResolvedValue(undefined)
175
176 await service.rollbackCommittedOperation({
177 sessionId: 'session-1',
178 projectDir: '/tmp/session-1',
179 operation: {
180 id: 'operation-style-page',
181 session_id: 'session-1',
182 parent_operation_id: 'operation-before',
183 before_commit: 'commit-before',
184 after_commit: 'commit-after',
185 metadata_json: JSON.stringify({ pageId: 'page-1', jobType: 'style-switch' })
186 } as never,
187 allowedPaths: ['page-1.html'],
188 reason: 'generation page update failed'
189 })
190
191 expect(internals.moveHeadToCommit).toHaveBeenCalledWith('/tmp/session-1', 'commit-before')
192 expect(internals.restoreCommitPaths).toHaveBeenCalledWith('/tmp/session-1', 'commit-before', [
193 'page-1.html'
194 ])
195 expect(completeSessionOperation).toHaveBeenCalledWith(
196 expect.objectContaining({
197 id: 'operation-style-page',
198 status: 'failed',
199 afterCommit: 'commit-before',
200 metadata: expect.objectContaining({
201 compensation: 'rolled_back_after_page_finalization_failure',
202 pageId: 'page-1'
203 })
204 })
205 )
206 expect(updateSessionHistoryPointer).toHaveBeenCalledWith({
207 sessionId: 'session-1',
208 operationId: 'operation-before',
209 commit: 'commit-before'
210 })
211 })
212 })
213
213 lines TYPESCRIPT