@@ -45,33 +45,48 @@ describe("backupCreateCommand atomic archive write", () => {
4545 await tempHome . restore ( ) ;
4646 } ) ;
4747
48- it ( "does not leave a partial final archive behind when tar creation fails" , async ( ) => {
48+ async function prepareAtomicBackupScenario ( params : {
49+ archivePrefix : string ;
50+ outputName ?: string ;
51+ } ) {
4952 const stateDir = path . join ( tempHome . home , ".openclaw" ) ;
50- const archiveDir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , "openclaw-backup-failure-" ) ) ;
51- try {
52- await fs . writeFile ( path . join ( stateDir , "openclaw.json" ) , JSON . stringify ( { } ) , "utf8" ) ;
53- await fs . writeFile ( path . join ( stateDir , "state.txt" ) , "state\n" , "utf8" ) ;
53+ const archiveDir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , params . archivePrefix ) ) ;
54+ await fs . writeFile ( path . join ( stateDir , "openclaw.json" ) , JSON . stringify ( { } ) , "utf8" ) ;
55+ await fs . writeFile ( path . join ( stateDir , "state.txt" ) , "state\n" , "utf8" ) ;
56+
57+ const runtime = {
58+ log : vi . fn ( ) ,
59+ error : vi . fn ( ) ,
60+ exit : vi . fn ( ) ,
61+ } ;
62+ const outputPath = path . join ( archiveDir , params . outputName ?? "backup.tar.gz" ) ;
63+
64+ vi . spyOn ( backupShared , "resolveBackupPlanFromDisk" ) . mockResolvedValue (
65+ await resolveBackupPlanFromPaths ( {
66+ stateDir,
67+ configPath : path . join ( stateDir , "openclaw.json" ) ,
68+ oauthDir : path . join ( stateDir , "credentials" ) ,
69+ includeWorkspace : false ,
70+ configInsideState : true ,
71+ oauthInsideState : true ,
72+ nowMs : 123 ,
73+ } ) ,
74+ ) ;
75+
76+ return {
77+ archiveDir,
78+ outputPath,
79+ runtime,
80+ } ;
81+ }
5482
83+ it ( "does not leave a partial final archive behind when tar creation fails" , async ( ) => {
84+ const { archiveDir, outputPath, runtime } = await prepareAtomicBackupScenario ( {
85+ archivePrefix : "openclaw-backup-failure-" ,
86+ } ) ;
87+ try {
5588 tarCreateMock . mockRejectedValueOnce ( new Error ( "disk full" ) ) ;
5689
57- const runtime = {
58- log : vi . fn ( ) ,
59- error : vi . fn ( ) ,
60- exit : vi . fn ( ) ,
61- } ;
62- const outputPath = path . join ( archiveDir , "backup.tar.gz" ) ;
63- vi . spyOn ( backupShared , "resolveBackupPlanFromDisk" ) . mockResolvedValue (
64- await resolveBackupPlanFromPaths ( {
65- stateDir,
66- configPath : path . join ( stateDir , "openclaw.json" ) ,
67- oauthDir : path . join ( stateDir , "credentials" ) ,
68- includeWorkspace : false ,
69- configInsideState : true ,
70- oauthInsideState : true ,
71- nowMs : 123 ,
72- } ) ,
73- ) ;
74-
7590 await expect (
7691 backupCreateCommand ( runtime , {
7792 output : outputPath ,
@@ -87,14 +102,12 @@ describe("backupCreateCommand atomic archive write", () => {
87102 } ) ;
88103
89104 it ( "does not overwrite an archive created after readiness checks complete" , async ( ) => {
90- const stateDir = path . join ( tempHome . home , ".openclaw" ) ;
91- const archiveDir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , "openclaw-backup-race-" ) ) ;
105+ const { archiveDir, outputPath, runtime } = await prepareAtomicBackupScenario ( {
106+ archivePrefix : "openclaw-backup-race-" ,
107+ } ) ;
92108 const realLink = fs . link . bind ( fs ) ;
93109 const linkSpy = vi . spyOn ( fs , "link" ) ;
94110 try {
95- await fs . writeFile ( path . join ( stateDir , "openclaw.json" ) , JSON . stringify ( { } ) , "utf8" ) ;
96- await fs . writeFile ( path . join ( stateDir , "state.txt" ) , "state\n" , "utf8" ) ;
97-
98111 tarCreateMock . mockImplementationOnce ( async ( { file } : { file : string } ) => {
99112 await fs . writeFile ( file , "archive-bytes" , "utf8" ) ;
100113 } ) ;
@@ -103,24 +116,6 @@ describe("backupCreateCommand atomic archive write", () => {
103116 return await realLink ( existingPath , newPath ) ;
104117 } ) ;
105118
106- const runtime = {
107- log : vi . fn ( ) ,
108- error : vi . fn ( ) ,
109- exit : vi . fn ( ) ,
110- } ;
111- const outputPath = path . join ( archiveDir , "backup.tar.gz" ) ;
112- vi . spyOn ( backupShared , "resolveBackupPlanFromDisk" ) . mockResolvedValue (
113- await resolveBackupPlanFromPaths ( {
114- stateDir,
115- configPath : path . join ( stateDir , "openclaw.json" ) ,
116- oauthDir : path . join ( stateDir , "credentials" ) ,
117- includeWorkspace : false ,
118- configInsideState : true ,
119- oauthInsideState : true ,
120- nowMs : 123 ,
121- } ) ,
122- ) ;
123-
124119 await expect (
125120 backupCreateCommand ( runtime , {
126121 output : outputPath ,
@@ -135,38 +130,18 @@ describe("backupCreateCommand atomic archive write", () => {
135130 } ) ;
136131
137132 it ( "falls back to exclusive copy when hard-link publication is unsupported" , async ( ) => {
138- const stateDir = path . join ( tempHome . home , ".openclaw" ) ;
139- const archiveDir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , "openclaw-backup-copy-fallback-" ) ) ;
133+ const { archiveDir, outputPath, runtime } = await prepareAtomicBackupScenario ( {
134+ archivePrefix : "openclaw-backup-copy-fallback-" ,
135+ } ) ;
140136 const linkSpy = vi . spyOn ( fs , "link" ) ;
141137 try {
142- await fs . writeFile ( path . join ( stateDir , "openclaw.json" ) , JSON . stringify ( { } ) , "utf8" ) ;
143- await fs . writeFile ( path . join ( stateDir , "state.txt" ) , "state\n" , "utf8" ) ;
144-
145138 tarCreateMock . mockImplementationOnce ( async ( { file } : { file : string } ) => {
146139 await fs . writeFile ( file , "archive-bytes" , "utf8" ) ;
147140 } ) ;
148141 linkSpy . mockRejectedValueOnce (
149142 Object . assign ( new Error ( "hard links not supported" ) , { code : "EOPNOTSUPP" } ) ,
150143 ) ;
151144
152- const runtime = {
153- log : vi . fn ( ) ,
154- error : vi . fn ( ) ,
155- exit : vi . fn ( ) ,
156- } ;
157- const outputPath = path . join ( archiveDir , "backup.tar.gz" ) ;
158- vi . spyOn ( backupShared , "resolveBackupPlanFromDisk" ) . mockResolvedValue (
159- await resolveBackupPlanFromPaths ( {
160- stateDir,
161- configPath : path . join ( stateDir , "openclaw.json" ) ,
162- oauthDir : path . join ( stateDir , "credentials" ) ,
163- includeWorkspace : false ,
164- configInsideState : true ,
165- oauthInsideState : true ,
166- nowMs : 123 ,
167- } ) ,
168- ) ;
169-
170145 const result = await backupCreateCommand ( runtime , {
171146 output : outputPath ,
172147 } ) ;
0 commit comments