|
| 1 | +import * as nodeFs from 'node:fs' |
1 | 2 | import { mkdtempSync, realpathSync } from 'node:fs' |
2 | 3 | import { |
3 | 4 | lstat, |
@@ -31,6 +32,8 @@ const servicePromise = (async () => import('./cliCommandService'))() |
31 | 32 |
|
32 | 33 | describe('cliCommandService', () => { |
33 | 34 | afterEach(async () => { |
| 35 | + // Drop any per-test fs.promises spies so later tests hit the real filesystem. |
| 36 | + vi.restoreAllMocks() |
34 | 37 | await rm(join(sharedHome, '.local'), { recursive: true, force: true }) |
35 | 38 | }) |
36 | 39 |
|
@@ -173,4 +176,116 @@ exec open -b "io.laststance.skills-desktop" |
173 | 176 | }) |
174 | 177 | expect((await lstat(commandPath)).isSymbolicLink()).toBe(true) |
175 | 178 | }) |
| 179 | + |
| 180 | + it('blocks management when a directory squats on the command path', async () => { |
| 181 | + // Arrange |
| 182 | + const { getCliCommandStatus } = await servicePromise |
| 183 | + await mkdir(commandPath, { recursive: true }) |
| 184 | + |
| 185 | + // Act |
| 186 | + const status = await getCliCommandStatus() |
| 187 | + |
| 188 | + // Assert |
| 189 | + expect(status).toEqual({ |
| 190 | + status: 'blocked', |
| 191 | + commandName: 'skills-desktop', |
| 192 | + commandPath, |
| 193 | + message: `${commandPath} is already occupied by another filesystem entry.`, |
| 194 | + }) |
| 195 | + }) |
| 196 | + |
| 197 | + it('blocks management when the command path cannot be inspected', async () => { |
| 198 | + // Arrange |
| 199 | + const { getCliCommandStatus } = await servicePromise |
| 200 | + // Make ~/.local/bin a regular file so lstat-ing a child path throws ENOTDIR. |
| 201 | + await mkdir(join(sharedHome, '.local'), { recursive: true }) |
| 202 | + await writeFile( |
| 203 | + join(sharedHome, '.local', 'bin'), |
| 204 | + 'not a directory', |
| 205 | + 'utf-8', |
| 206 | + ) |
| 207 | + |
| 208 | + // Act |
| 209 | + const status = await getCliCommandStatus() |
| 210 | + |
| 211 | + // Assert |
| 212 | + expect(status.status).toBe('blocked') |
| 213 | + expect(status.commandName).toBe('skills-desktop') |
| 214 | + expect(status.commandPath).toBe(commandPath) |
| 215 | + expect(status.message).toContain(`Could not inspect ${commandPath}:`) |
| 216 | + }) |
| 217 | + |
| 218 | + it('reports success without rewriting when the command is already installed', async () => { |
| 219 | + // Arrange |
| 220 | + const { installCliCommand } = await servicePromise |
| 221 | + await installCliCommand() |
| 222 | + |
| 223 | + // Act |
| 224 | + const result = await installCliCommand() |
| 225 | + |
| 226 | + // Assert |
| 227 | + expect(result).toEqual({ |
| 228 | + ok: true, |
| 229 | + status: { |
| 230 | + status: 'installed', |
| 231 | + commandName: 'skills-desktop', |
| 232 | + commandPath, |
| 233 | + message: 'Command is installed.', |
| 234 | + }, |
| 235 | + message: 'Command is already installed.', |
| 236 | + }) |
| 237 | + }) |
| 238 | + |
| 239 | + it('surfaces a failure message when writing the shim throws', async () => { |
| 240 | + // Arrange |
| 241 | + const { installCliCommand } = await servicePromise |
| 242 | + vi.spyOn(nodeFs.promises, 'writeFile').mockRejectedValueOnce( |
| 243 | + new Error('disk full'), |
| 244 | + ) |
| 245 | + |
| 246 | + // Act |
| 247 | + const result = await installCliCommand() |
| 248 | + |
| 249 | + // Assert |
| 250 | + expect(result.ok).toBe(false) |
| 251 | + expect(result.status.status).toBe('not-installed') |
| 252 | + expect(result.message).toBe('Could not install command: disk full') |
| 253 | + }) |
| 254 | + |
| 255 | + it('reports success when the command is already absent', async () => { |
| 256 | + // Arrange |
| 257 | + const { removeCliCommand } = await servicePromise |
| 258 | + |
| 259 | + // Act |
| 260 | + const result = await removeCliCommand() |
| 261 | + |
| 262 | + // Assert |
| 263 | + expect(result).toEqual({ |
| 264 | + ok: true, |
| 265 | + status: { |
| 266 | + status: 'not-installed', |
| 267 | + commandName: 'skills-desktop', |
| 268 | + commandPath, |
| 269 | + message: 'Command is not installed.', |
| 270 | + }, |
| 271 | + message: 'Command is already removed.', |
| 272 | + }) |
| 273 | + }) |
| 274 | + |
| 275 | + it('surfaces a failure message when deleting the managed shim throws', async () => { |
| 276 | + // Arrange |
| 277 | + const { installCliCommand, removeCliCommand } = await servicePromise |
| 278 | + await installCliCommand() |
| 279 | + vi.spyOn(nodeFs.promises, 'unlink').mockRejectedValueOnce( |
| 280 | + new Error('disk full'), |
| 281 | + ) |
| 282 | + |
| 283 | + // Act |
| 284 | + const result = await removeCliCommand() |
| 285 | + |
| 286 | + // Assert |
| 287 | + expect(result.ok).toBe(false) |
| 288 | + expect(result.status.status).toBe('installed') |
| 289 | + expect(result.message).toBe('Could not remove command: disk full') |
| 290 | + }) |
176 | 291 | }) |
0 commit comments