Skip to content

Commit a30fb89

Browse files
authored
fix(skills): repair stale path (#1662)
1 parent 07f4cc1 commit a30fb89

5 files changed

Lines changed: 116 additions & 1 deletion

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Skills Path Cross-Platform Repair Plan
2+
3+
## Approach
4+
5+
Extend `SkillPresenter.resolveSkillsDir()` with a small default-path repair step. The repair only
6+
matches paths that look like DeepChat's default skills location under an OS user home directory:
7+
8+
- `/Users/<name>/.deepchat/skills`
9+
- `<drive>:\Users\<name>\.deepchat\skills`
10+
11+
If matched, return the same suffix under the current `app.getPath('home')` default skills root.
12+
This keeps intentionally custom paths unchanged while covering stale OS/account defaults.
13+
14+
## Compatibility
15+
16+
The current malformed path repair for `C:\Users\name.deepchat\skills` is preserved. Existing valid
17+
configured paths continue to resolve through `path.resolve()`.
18+
19+
## Test Strategy
20+
21+
Add constructor-level `getSkillsDir()` assertions in `skillPresenter.test.ts` for:
22+
23+
- POSIX stale default path repair.
24+
- Windows stale default path repair.
25+
- Existing malformed `.deepchat` repair remains unchanged.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Skills Path Cross-Platform Repair
2+
3+
## Problem
4+
5+
DeepChat can fail during startup when the persisted `skillsPath` points to a default skills
6+
directory from another OS or user profile, such as `/Users/old-user/.deepchat/skills` on Windows.
7+
Windows resolves that POSIX-looking path under the current drive, then startup attempts to create a
8+
directory like `C:\Users\old-user\.deepchat\skills` and can fail with `EPERM`.
9+
10+
## User Story
11+
12+
As a user who moved configuration between machines or OS accounts, I want DeepChat to recover from
13+
stale default skills paths so the app still opens and uses the current profile's skills directory.
14+
15+
## Acceptance Criteria
16+
17+
- Startup repairs stale default skills paths from POSIX `/Users/<name>/.deepchat/skills`.
18+
- Startup repairs stale default skills paths from Windows `C:\Users\<name>\.deepchat\skills`.
19+
- Repair keeps any path suffix below `skills`.
20+
- Non-default custom skills paths remain unchanged.
21+
- Existing malformed `.deepchat` path repair keeps working.
22+
23+
## Non-Goals
24+
25+
- Do not migrate arbitrary custom directories.
26+
- Do not change skill discovery, installation, or sync behavior.
27+
- Do not add a new settings migration framework.
28+
29+
## Constraints
30+
31+
- Keep the change localized to the existing `SkillPresenter` startup path handling.
32+
- Add focused unit coverage for the repaired path patterns.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Skills Path Cross-Platform Repair Tasks
2+
3+
- [x] Document the issue and repair scope.
4+
- [x] Extend `SkillPresenter.resolveSkillsDir()` repair logic.
5+
- [x] Add focused unit tests for cross-platform stale default paths.
6+
- [x] Run formatting and the targeted SkillPresenter test.

src/main/presenter/skillPresenter/index.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,13 @@ export class SkillPresenter implements ISkillPresenter {
228228
const homeDir = homePath ? path.resolve(homePath) : path.resolve('.')
229229
const fallbackDir = path.join(homeDir, '.deepchat', 'skills')
230230
const resolved = normalized ? path.resolve(normalized) : fallbackDir
231+
const repairedDefaultPath = normalized
232+
? this.repairPortableDefaultSkillsPath(normalized, homeDir)
233+
: null
234+
235+
if (repairedDefaultPath) {
236+
return repairedDefaultPath
237+
}
231238

232239
// Repair malformed paths like: C:\Users\name.deepchat\skills
233240
const brokenPrefix = `${homeDir}.deepchat`
@@ -246,6 +253,20 @@ export class SkillPresenter implements ISkillPresenter {
246253
return resolved
247254
}
248255

256+
private repairPortableDefaultSkillsPath(configuredPath: string, homeDir: string): string | null {
257+
const slashPath = configuredPath.replace(/\\/g, '/')
258+
const match =
259+
slashPath.match(/^\/Users\/[^/]+\/\.deepchat\/skills(?:\/(.*))?$/i) ??
260+
slashPath.match(/^[A-Za-z]:\/Users\/[^/]+\/\.deepchat\/skills(?:\/(.*))?$/i)
261+
262+
if (!match) {
263+
return null
264+
}
265+
266+
const suffixParts = (match[1] ?? '').split('/').filter(Boolean)
267+
return path.join(homeDir, '.deepchat', 'skills', ...suffixParts)
268+
}
269+
249270
/**
250271
* Ensure the skills directory exists
251272
*/

test/main/presenter/skillPresenter/skillPresenter.test.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,11 +284,12 @@ describe('SkillPresenter', () => {
284284
expect(fs.existsSync).toHaveBeenCalled()
285285
})
286286

287-
it('should use configured skills path when provided', () => {
287+
it('should use configured skills path when provided', async () => {
288288
;(mockConfigPresenter.getSkillsPath as Mock).mockReturnValue('/custom/skills/path')
289289

290290
const presenter = new SkillPresenter(mockConfigPresenter, skillSessionStatePort as any)
291291
expect(mockConfigPresenter.getSkillsPath).toHaveBeenCalled()
292+
await expect(presenter.getSkillsDir()).resolves.toBe('/custom/skills/path')
292293
presenter.destroy()
293294
})
294295

@@ -312,6 +313,36 @@ describe('SkillPresenter', () => {
312313
await expect(presenter.getSkillsDir()).resolves.toBe('/mock/home/.deepchat/skills')
313314
presenter.destroy()
314315
})
316+
317+
it('should repair stale POSIX default skills paths from another user profile', async () => {
318+
;(mockConfigPresenter.getSkillsPath as Mock).mockReturnValue(
319+
'/Users/legacy-user/.deepchat/skills'
320+
)
321+
;(app.getPath as Mock).mockImplementation((name: string) => {
322+
if (name === 'home') return '/mock/home'
323+
if (name === 'temp') return '/mock/temp'
324+
return '/mock/' + name
325+
})
326+
327+
const presenter = new SkillPresenter(mockConfigPresenter, skillSessionStatePort as any)
328+
await expect(presenter.getSkillsDir()).resolves.toBe('/mock/home/.deepchat/skills')
329+
presenter.destroy()
330+
})
331+
332+
it('should repair stale Windows default skills paths from another user profile', async () => {
333+
;(mockConfigPresenter.getSkillsPath as Mock).mockReturnValue(
334+
'C:\\Users\\legacy-user\\.deepchat\\skills\\nested'
335+
)
336+
;(app.getPath as Mock).mockImplementation((name: string) => {
337+
if (name === 'home') return '/mock/home'
338+
if (name === 'temp') return '/mock/temp'
339+
return '/mock/' + name
340+
})
341+
342+
const presenter = new SkillPresenter(mockConfigPresenter, skillSessionStatePort as any)
343+
await expect(presenter.getSkillsDir()).resolves.toBe('/mock/home/.deepchat/skills/nested')
344+
presenter.destroy()
345+
})
315346
})
316347

317348
describe('getSkillsDir', () => {

0 commit comments

Comments
 (0)