@dog_world/dak 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,146 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { resolveStorePath, createDefaultConfig, writeConfigIfMissing, validateConfigStore, } from '../src/config.js';
3
- import { readState, writeState, } from '../src/state.js';
4
- import { scanResources, formatResourceList } from '../src/resources.js';
5
- import { mkdtempSync, mkdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
6
- import { join } from 'node:path';
7
- import { tmpdir } from 'node:os';
8
- const TMP_PREFIX = 'dak-config-';
9
- describe('resolveStorePath', () => {
10
- it('默认 store 解析为 <home>/.dog-agents-kit', () => {
11
- const result = resolveStorePath(undefined, '/home/test');
12
- expect(result).toBe('/home/test/.dog-agents-kit');
13
- });
14
- it('--store ~/custom-store 展开 home', () => {
15
- const result = resolveStorePath('~/custom-store', '/home/test');
16
- expect(result).toBe('/home/test/custom-store');
17
- });
18
- it('绝对路径直接返回', () => {
19
- const result = resolveStorePath('/abs/store', '/home/test');
20
- expect(result).toBe('/abs/store');
21
- });
22
- });
23
- describe('createDefaultConfig', () => {
24
- it('写入绝对 store path', () => {
25
- const config = createDefaultConfig('/abs/store', '/home/test');
26
- expect(config.store).toBe('/abs/store');
27
- expect(config.targets['codex'].path).toBe('/home/test/.codex');
28
- expect(config.targets['claude-code'].path).toBe('/home/test/.claude');
29
- });
30
- });
31
- describe('writeConfigIfMissing', () => {
32
- let tmp;
33
- beforeEach(() => {
34
- tmp = mkdtempSync(join(tmpdir(), TMP_PREFIX));
35
- });
36
- afterEach(() => {
37
- rmSync(tmp, { recursive: true });
38
- });
39
- it('store 不存在时创建 config', async () => {
40
- const storePath = join(tmp, 'store');
41
- const config = createDefaultConfig(storePath, '/home/test');
42
- await writeConfigIfMissing(storePath, config);
43
- const content = readFileSync(join(storePath, 'dak.config.json'), 'utf-8');
44
- expect(JSON.parse(content).store).toBe(storePath);
45
- });
46
- it('已存在 dak.config.json 时不覆盖', async () => {
47
- const storePath = join(tmp, 'store');
48
- mkdirSync(storePath);
49
- writeFileSync(join(storePath, 'dak.config.json'), '{"store":"custom"}');
50
- const config = createDefaultConfig(storePath, '/home/test');
51
- await writeConfigIfMissing(storePath, config);
52
- const content = readFileSync(join(storePath, 'dak.config.json'), 'utf-8');
53
- expect(JSON.parse(content).store).toBe('custom');
54
- });
55
- });
56
- describe('validateConfigStore', () => {
57
- it('config.store 和当前 store 不一致时报错', () => {
58
- const config = { store: '/abs/store', targets: {} };
59
- expect(() => validateConfigStore(config, '/other/store')).toThrow('config store mismatch');
60
- });
61
- });
62
- describe('scanResources', () => {
63
- let tmp;
64
- beforeEach(() => {
65
- tmp = mkdtempSync(join(tmpdir(), TMP_PREFIX));
66
- });
67
- afterEach(() => {
68
- rmSync(tmp, { recursive: true });
69
- });
70
- it('只扫一级子项', async () => {
71
- const storePath = join(tmp, 'store');
72
- mkdirSync(join(storePath, 'skills', 'nested'), { recursive: true });
73
- writeFileSync(join(storePath, 'skills', 'top-skill'), 'x');
74
- writeFileSync(join(storePath, 'skills', 'nested', 'deep'), 'x');
75
- const config = createDefaultConfig(storePath, '/home/test');
76
- const resources = await scanResources(storePath, config);
77
- // 一级子项应包含 top-skill(文件)和 nested(目录),但不含 nested/deep
78
- const names = resources.skills.map(i => i.name).sort();
79
- expect(names).toEqual(['nested', 'top-skill']);
80
- expect(resources.skills.find(i => i.name === 'nested')?.kind).toBe('directory');
81
- });
82
- it('hidden item 跳过', async () => {
83
- const storePath = join(tmp, 'store');
84
- mkdirSync(join(storePath, 'skills'), { recursive: true });
85
- writeFileSync(join(storePath, 'skills', '.hidden'), 'x');
86
- writeFileSync(join(storePath, 'skills', 'visible'), 'x');
87
- const config = createDefaultConfig(storePath, '/home/test');
88
- const resources = await scanResources(storePath, config);
89
- expect(resources.skills.map(i => i.name)).toEqual(['visible']);
90
- });
91
- it('文件、目录、symlink 都算 item', async () => {
92
- const storePath = join(tmp, 'store');
93
- mkdirSync(join(storePath, 'skills'), { recursive: true });
94
- writeFileSync(join(storePath, 'skills', 'file-skill'), 'x');
95
- mkdirSync(join(storePath, 'skills', 'dir-skill'));
96
- const linkTarget = join(tmp, 'real-target');
97
- writeFileSync(linkTarget, 'x');
98
- // symlink via fs.symlinkSync
99
- const { symlinkSync } = await import('node:fs');
100
- symlinkSync(linkTarget, join(storePath, 'skills', 'link-skill'));
101
- const config = createDefaultConfig(storePath, '/home/test');
102
- const resources = await scanResources(storePath, config);
103
- const names = resources.skills.map(i => i.name).sort();
104
- expect(names).toEqual(['dir-skill', 'file-skill', 'link-skill']);
105
- });
106
- it('formatResourceList 输出固定标题行', async () => {
107
- const resources = {
108
- skills: [{ name: 'foo', kind: 'file', path: '/s/foo' }],
109
- hooks: [],
110
- agents: [],
111
- };
112
- const output = formatResourceList(resources);
113
- expect(output).toContain('Skills:');
114
- expect(output).toContain('Hooks:');
115
- expect(output).toContain('Agents:');
116
- expect(output).toContain('foo');
117
- });
118
- });
119
- describe('readState / writeState', () => {
120
- let tmp;
121
- beforeEach(() => {
122
- tmp = mkdtempSync(join(tmpdir(), TMP_PREFIX));
123
- });
124
- afterEach(() => {
125
- rmSync(tmp, { recursive: true });
126
- });
127
- it('缺文件时返回空状态', async () => {
128
- const state = await readState(join(tmp, 'no-store'));
129
- expect(state.version).toBe(1);
130
- expect(state.targets).toEqual({});
131
- });
132
- it('只接受 version: 1', async () => {
133
- const storePath = join(tmp, 'store');
134
- mkdirSync(storePath);
135
- writeFileSync(join(storePath, '.dak-state.json'), '{"version":2,"targets":{}}');
136
- await expect(readState(storePath)).rejects.toThrow('unsupported state version');
137
- });
138
- it('写状态后可读回', async () => {
139
- const storePath = join(tmp, 'store');
140
- mkdirSync(storePath);
141
- const state = { version: 1, targets: {} };
142
- await writeState(storePath, state);
143
- const read = await readState(storePath);
144
- expect(read).toEqual(state);
145
- });
146
- });
@@ -1,74 +0,0 @@
1
- import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
- import { execFileSync } from 'node:child_process';
3
- import { mkdirSync, rmSync, writeFileSync, readFileSync, mkdtempSync } from 'node:fs';
4
- import { join } from 'node:path';
5
- import { tmpdir } from 'node:os';
6
- const DAK_CLI = join(process.cwd(), 'dist', 'cli.js');
7
- const TMP_PREFIX = 'dak-e2e-';
8
- function tmpRoot() {
9
- return mkdtempSync(join(tmpdir(), TMP_PREFIX));
10
- }
11
- describe('dak e2e', () => {
12
- let root;
13
- let home;
14
- let store;
15
- beforeAll(() => {
16
- root = tmpRoot();
17
- home = join(root, 'home');
18
- store = join(root, 'dak-store');
19
- mkdirSync(store, { recursive: true });
20
- });
21
- afterAll(() => {
22
- rmSync(root, { recursive: true, force: true });
23
- });
24
- function exec(args) {
25
- return execFileSync(process.execPath, [DAK_CLI, ...args], {
26
- env: { ...process.env, HOME: home },
27
- encoding: 'utf-8',
28
- });
29
- }
30
- it('init --store', () => {
31
- const out = exec(['init', '--store', store]);
32
- expect(out).toContain('Initialized');
33
- expect(exec(['list', '--store', store])).toContain('Skills:');
34
- });
35
- it('写入 skills/foo 后 link codex', () => {
36
- writeFileSync(join(store, 'skills', 'foo'), 'hello');
37
- const out = exec(['link', 'codex', '--store', store, '--on-conflict', 'skip']);
38
- expect(out).toContain('created');
39
- const linkPath = join(home, '.codex', 'skills', 'foo');
40
- expect(execFileSync('readlink', [linkPath], { encoding: 'utf-8' }).trim()).toBe(join(store, 'skills', 'foo'));
41
- });
42
- it('删除 foo 新增 bar 后 update', () => {
43
- rmSync(join(store, 'skills', 'foo'));
44
- writeFileSync(join(store, 'skills', 'bar'), 'world');
45
- const out = exec(['update', '--store', store]);
46
- expect(out).toContain('deleted');
47
- expect(out).toContain('created');
48
- // bar 已链接
49
- const linkPath = join(home, '.codex', 'skills', 'bar');
50
- expect(readFileSync(linkPath, 'utf-8')).toBe('world');
51
- });
52
- it('unlink codex', () => {
53
- const out = exec(['unlink', 'codex', '--store', store]);
54
- expect(out).toContain('deleted');
55
- // 源文件仍在
56
- expect(readFileSync(join(store, 'skills', 'bar'), 'utf-8')).toBe('world');
57
- });
58
- it('--on-conflict backup', () => {
59
- // 先创建真实文件
60
- mkdirSync(join(home, '.codex', 'skills'), { recursive: true });
61
- writeFileSync(join(home, '.codex', 'skills', 'foo'), 'real-file');
62
- writeFileSync(join(store, 'skills', 'foo'), 'store-file');
63
- const out = exec(['link', 'codex', '--store', store, '--on-conflict', 'backup']);
64
- expect(out).toContain('backed-up');
65
- });
66
- it('非交互环境未传 --on-conflict 且目标已有真实文件,默认 skip', () => {
67
- writeFileSync(join(home, '.codex', 'skills', 'baz'), 'existing');
68
- writeFileSync(join(store, 'skills', 'baz'), 'new');
69
- const out = exec(['link', 'codex', '--store', store]);
70
- expect(out).toContain('conflict');
71
- // 真实文件未被覆盖
72
- expect(readFileSync(join(home, '.codex', 'skills', 'baz'), 'utf-8')).toBe('existing');
73
- });
74
- });
@@ -1,234 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { classifyTarget, linkItem, safeDeleteManagedLink } from '../src/linker.js';
3
- import { mkdtempSync, mkdirSync, rmSync, writeFileSync, symlinkSync, readFileSync } from 'node:fs';
4
- import { join } from 'node:path';
5
- import { tmpdir } from 'node:os';
6
- const TMP_PREFIX = 'dak-linker-';
7
- function makeStore(tmp) {
8
- const store = join(tmp, 'store');
9
- mkdirSync(join(store, 'skills'), { recursive: true });
10
- mkdirSync(join(store, 'hooks'), { recursive: true });
11
- mkdirSync(join(store, 'agents'), { recursive: true });
12
- return store;
13
- }
14
- function makeTarget(tmp, name) {
15
- const target = join(tmp, name);
16
- mkdirSync(target, { recursive: true });
17
- return target;
18
- }
19
- describe('classifyTarget', () => {
20
- let tmp;
21
- beforeEach(() => {
22
- tmp = mkdtempSync(join(tmpdir(), TMP_PREFIX));
23
- });
24
- afterEach(() => {
25
- rmSync(tmp, { recursive: true });
26
- });
27
- it('missing 当 target 不存在', async () => {
28
- const result = await classifyTarget(join(tmp, 'no-such-dir', 'item'), '/abs/source');
29
- expect(result).toBe('missing');
30
- });
31
- it('linked 当 symlink 指向 expected source', async () => {
32
- const source = join(tmp, 'store', 'skills', 'foo');
33
- mkdirSync(join(tmp, 'store', 'skills'), { recursive: true });
34
- writeFileSync(source, 'x');
35
- const targetDir = join(tmp, 'codex', 'skills');
36
- mkdirSync(targetDir, { recursive: true });
37
- const targetPath = join(targetDir, 'foo');
38
- symlinkSync(source, targetPath);
39
- const result = await classifyTarget(targetPath, source);
40
- expect(result).toBe('linked');
41
- });
42
- it('conflict 当 target 是真实文件', async () => {
43
- const targetPath = join(tmp, 'real-file');
44
- writeFileSync(targetPath, 'x');
45
- const result = await classifyTarget(targetPath, '/abs/source');
46
- expect(result).toBe('conflict');
47
- });
48
- it('broken 当 symlink 指向不存在目标', async () => {
49
- const targetPath = join(tmp, 'broken-link');
50
- symlinkSync('/no/such/target', targetPath);
51
- const result = await classifyTarget(targetPath, '/abs/source');
52
- expect(result).toBe('broken');
53
- });
54
- });
55
- describe('linkItem', () => {
56
- let tmp;
57
- let store;
58
- let targetRoot;
59
- beforeEach(() => {
60
- tmp = mkdtempSync(join(tmpdir(), TMP_PREFIX));
61
- store = makeStore(tmp);
62
- targetRoot = makeTarget(tmp, 'codex');
63
- mkdirSync(join(targetRoot, 'skills'), { recursive: true });
64
- writeFileSync(join(store, 'skills', 'foo'), 'x');
65
- });
66
- afterEach(() => {
67
- rmSync(tmp, { recursive: true });
68
- });
69
- const now = new Date('2025-01-01T00:00:00.000Z');
70
- it('missing 时创建 symlink', async () => {
71
- const outcome = await linkItem({
72
- sourcePath: join(store, 'skills', 'foo'),
73
- targetPath: join(targetRoot, 'skills', 'foo'),
74
- resourceType: 'skills',
75
- itemName: 'foo',
76
- storePath: store,
77
- policy: 'skip',
78
- now,
79
- });
80
- expect(outcome.status).toBe('created');
81
- expect(readFileSync(join(targetRoot, 'skills', 'foo'), 'utf-8')).toBe('x');
82
- });
83
- it('linked 时直接返回', async () => {
84
- const targetPath = join(targetRoot, 'skills', 'foo');
85
- symlinkSync(join(store, 'skills', 'foo'), targetPath);
86
- const outcome = await linkItem({
87
- sourcePath: join(store, 'skills', 'foo'),
88
- targetPath,
89
- resourceType: 'skills',
90
- itemName: 'foo',
91
- storePath: store,
92
- policy: 'skip',
93
- now,
94
- });
95
- expect(outcome.status).toBe('linked');
96
- });
97
- it('conflict 时 skip 返回 conflict', async () => {
98
- writeFileSync(join(targetRoot, 'skills', 'foo'), 'old');
99
- const outcome = await linkItem({
100
- sourcePath: join(store, 'skills', 'foo'),
101
- targetPath: join(targetRoot, 'skills', 'foo'),
102
- resourceType: 'skills',
103
- itemName: 'foo',
104
- storePath: store,
105
- policy: 'skip',
106
- now,
107
- });
108
- expect(outcome.status).toBe('conflict');
109
- expect(readFileSync(join(targetRoot, 'skills', 'foo'), 'utf-8')).toBe('old');
110
- });
111
- it('backup 移动旧内容后创建 symlink', async () => {
112
- writeFileSync(join(targetRoot, 'skills', 'foo'), 'old');
113
- const outcome = await linkItem({
114
- sourcePath: join(store, 'skills', 'foo'),
115
- targetPath: join(targetRoot, 'skills', 'foo'),
116
- resourceType: 'skills',
117
- itemName: 'foo',
118
- storePath: store,
119
- policy: 'backup',
120
- now,
121
- });
122
- expect(outcome.status).toBe('backed-up');
123
- // symlink 已创建
124
- const stat = await import('node:fs/promises').then(m => m.stat(join(targetRoot, 'skills', 'foo')));
125
- expect(stat).toBeDefined();
126
- // 备份存在
127
- const ts = now.toISOString().replace(/[:.]/g, '-');
128
- const backupPath = join(targetRoot, '.dak-backup', ts, 'skills', 'foo');
129
- expect(readFileSync(backupPath, 'utf-8')).toBe('old');
130
- });
131
- it('overwrite 删除旧内容后创建 symlink', async () => {
132
- writeFileSync(join(targetRoot, 'skills', 'foo'), 'old');
133
- const outcome = await linkItem({
134
- sourcePath: join(store, 'skills', 'foo'),
135
- targetPath: join(targetRoot, 'skills', 'foo'),
136
- resourceType: 'skills',
137
- itemName: 'foo',
138
- storePath: store,
139
- policy: 'overwrite',
140
- now,
141
- });
142
- expect(outcome.status).toBe('overwritten');
143
- expect(readFileSync(join(targetRoot, 'skills', 'foo'), 'utf-8')).toBe('x');
144
- });
145
- it('broken symlink 视为 conflict;overwrite 可替换', async () => {
146
- const targetPath = join(targetRoot, 'skills', 'foo');
147
- symlinkSync('/no/target', targetPath);
148
- const outcome = await linkItem({
149
- sourcePath: join(store, 'skills', 'foo'),
150
- targetPath,
151
- resourceType: 'skills',
152
- itemName: 'foo',
153
- storePath: store,
154
- policy: 'overwrite',
155
- now,
156
- });
157
- expect(outcome.status).toBe('overwritten');
158
- expect(readFileSync(targetPath, 'utf-8')).toBe('x');
159
- });
160
- });
161
- describe('safeDeleteManagedLink', () => {
162
- let tmp;
163
- let store;
164
- beforeEach(() => {
165
- tmp = mkdtempSync(join(tmpdir(), TMP_PREFIX));
166
- store = makeStore(tmp);
167
- });
168
- afterEach(() => {
169
- rmSync(tmp, { recursive: true });
170
- });
171
- it('删除正确的 symlink 返回 deleted', async () => {
172
- const source = join(store, 'skills', 'foo');
173
- writeFileSync(source, 'x');
174
- const targetDir = join(tmp, 'codex', 'skills');
175
- mkdirSync(targetDir, { recursive: true });
176
- const targetPath = join(targetDir, 'foo');
177
- symlinkSync(source, targetPath);
178
- const record = { source, target: targetPath, linkedAt: new Date().toISOString() };
179
- const result = await safeDeleteManagedLink(record, store);
180
- expect(result).toBe('deleted');
181
- });
182
- it('target 不存在返回 missing', async () => {
183
- const record = {
184
- source: join(store, 'skills', 'foo'),
185
- target: join(tmp, 'codex', 'skills', 'foo'),
186
- linkedAt: new Date().toISOString(),
187
- };
188
- const result = await safeDeleteManagedLink(record, store);
189
- expect(result).toBe('missing');
190
- });
191
- it('真实文件不删除,返回 conflict', async () => {
192
- const targetDir = join(tmp, 'codex', 'skills');
193
- mkdirSync(targetDir, { recursive: true });
194
- const targetPath = join(targetDir, 'foo');
195
- writeFileSync(targetPath, 'x');
196
- const record = {
197
- source: join(store, 'skills', 'foo'),
198
- target: targetPath,
199
- linkedAt: new Date().toISOString(),
200
- };
201
- const result = await safeDeleteManagedLink(record, store);
202
- expect(result).toBe('conflict');
203
- expect(require('node:fs').existsSync(targetPath)).toBe(true);
204
- });
205
- it('指向其他 store 的 symlink 不删除,返回 conflict', async () => {
206
- const otherStore = join(tmp, 'other-store');
207
- mkdirSync(join(otherStore, 'skills'), { recursive: true });
208
- const source = join(otherStore, 'skills', 'foo');
209
- writeFileSync(source, 'x');
210
- const targetDir = join(tmp, 'codex', 'skills');
211
- mkdirSync(targetDir, { recursive: true });
212
- const targetPath = join(targetDir, 'foo');
213
- symlinkSync(source, targetPath);
214
- const record = { source, target: targetPath, linkedAt: new Date().toISOString() };
215
- const result = await safeDeleteManagedLink(record, store);
216
- expect(result).toBe('conflict');
217
- });
218
- it('symlink 改指向同一 store 另一个 item,返回 conflict', async () => {
219
- const source1 = join(store, 'skills', 'foo');
220
- const source2 = join(store, 'skills', 'bar');
221
- writeFileSync(source1, 'x');
222
- writeFileSync(source2, 'x');
223
- const targetDir = join(tmp, 'codex', 'skills');
224
- mkdirSync(targetDir, { recursive: true });
225
- const targetPath = join(targetDir, 'foo');
226
- symlinkSync(source1, targetPath);
227
- // 用户改成指向 bar
228
- rmSync(targetPath);
229
- symlinkSync(source2, targetPath);
230
- const record = { source: source1, target: targetPath, linkedAt: new Date().toISOString() };
231
- const result = await safeDeleteManagedLink(record, store);
232
- expect(result).toBe('conflict');
233
- });
234
- });
@@ -1,120 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { expandHome, toAbsolutePath, isHiddenItem, assertSafeItemName, isPathInside, realComparablePath, resolveLinkTarget, realParentJoined, } from '../src/paths.js';
3
- import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
4
- import { join } from 'node:path';
5
- import { tmpdir } from 'node:os';
6
- describe('expandHome', () => {
7
- it('展开 ~ 为指定 home', () => {
8
- expect(expandHome('~/foo', '/home/test')).toBe('/home/test/foo');
9
- });
10
- it('~ 本身展开为 home', () => {
11
- expect(expandHome('~', '/home/test')).toBe('/home/test');
12
- });
13
- it('非 ~ 路径原样返回', () => {
14
- expect(expandHome('/abs/path', '/home/test')).toBe('/abs/path');
15
- });
16
- });
17
- describe('toAbsolutePath', () => {
18
- it('相对路径按 baseDir 解析', () => {
19
- expect(toAbsolutePath('foo/bar', '/base', '/home')).toBe('/base/foo/bar');
20
- });
21
- it('~ 路径展开后解析', () => {
22
- expect(toAbsolutePath('~/.config', '/base', '/home')).toBe('/home/.config');
23
- });
24
- it('绝对路径 resolve', () => {
25
- expect(toAbsolutePath('/foo/../bar', '/base', '/home')).toBe('/bar');
26
- });
27
- });
28
- describe('isHiddenItem', () => {
29
- it('.hidden 返回 true', () => {
30
- expect(isHiddenItem('.hidden')).toBe(true);
31
- });
32
- it('.foo 返回 true', () => {
33
- expect(isHiddenItem('.foo')).toBe(true);
34
- });
35
- it('foo 返回 false', () => {
36
- expect(isHiddenItem('foo')).toBe(false);
37
- });
38
- });
39
- describe('assertSafeItemName', () => {
40
- it('隐藏项抛出', () => {
41
- expect(() => assertSafeItemName('.hidden')).toThrow('Hidden resource items are ignored');
42
- });
43
- it('斜杠抛出', () => {
44
- expect(() => assertSafeItemName('group/foo')).toThrow('Invalid resource item name');
45
- });
46
- it('反斜杠抛出', () => {
47
- expect(() => assertSafeItemName('group\\foo')).toThrow('Invalid resource item name');
48
- });
49
- it('. 单独抛出', () => {
50
- expect(() => assertSafeItemName('.')).toThrow('Invalid resource item name');
51
- });
52
- it('.. 抛出', () => {
53
- expect(() => assertSafeItemName('..')).toThrow('Invalid resource item name');
54
- });
55
- it('合法名称不抛', () => {
56
- expect(() => assertSafeItemName('foo')).not.toThrow();
57
- });
58
- });
59
- describe('isPathInside', () => {
60
- it('/tmp/store 内路径返回 true', () => {
61
- expect(isPathInside('/tmp/store/a', '/tmp/store')).toBe(true);
62
- });
63
- it('/tmp/store-other 不被 /tmp/store 骗过', () => {
64
- expect(isPathInside('/tmp/store-other/a', '/tmp/store')).toBe(false);
65
- });
66
- it('/tmp/store 自身返回 true', () => {
67
- expect(isPathInside('/tmp/store', '/tmp/store')).toBe(true);
68
- });
69
- it('/tmp/store 深层返回 true', () => {
70
- expect(isPathInside('/tmp/store/a/b/c', '/tmp/store')).toBe(true);
71
- });
72
- it('/tmp/other 返回 false', () => {
73
- expect(isPathInside('/tmp/other', '/tmp/store')).toBe(false);
74
- });
75
- });
76
- describe('realComparablePath', () => {
77
- it('真实路径文件返回 resolved path', async () => {
78
- const tmp = mkdtempSync(join(tmpdir(), 'dak-paths-'));
79
- const f = join(tmp, 'file.txt');
80
- writeFileSync(f, 'x');
81
- const result = await realComparablePath(f);
82
- expect(result).toBe(f);
83
- rmSync(tmp, { recursive: true });
84
- });
85
- });
86
- describe('resolveLinkTarget', () => {
87
- it('绝对 target 直接 resolve', async () => {
88
- const result = await resolveLinkTarget('/tmp/link', '/abs/target');
89
- expect(result).toBe('/abs/target');
90
- });
91
- it('相对 target 按 link parent 解析', async () => {
92
- const tmp = mkdtempSync(join(tmpdir(), 'dak-paths-'));
93
- const linkPath = join(tmp, 'sub', 'link');
94
- const sourcePath = join(tmp, 'src');
95
- mkdirSync(join(tmp, 'sub'), { recursive: true });
96
- mkdirSync(sourcePath, { recursive: true });
97
- writeFileSync(join(sourcePath, 'item'), 'x');
98
- const result = await resolveLinkTarget(linkPath, '../src/item');
99
- expect(result).toBe(join(sourcePath, 'item'));
100
- rmSync(tmp, { recursive: true });
101
- });
102
- });
103
- describe('realParentJoined', () => {
104
- it('父目录存在时用真实路径拼接', async () => {
105
- const tmp = mkdtempSync(join(tmpdir(), 'dak-paths-'));
106
- const dir = join(tmp, 'real-dir');
107
- mkdirSync(dir);
108
- const result = await realParentJoined(join(dir, 'child'));
109
- expect(result).toBe(join(dir, 'child'));
110
- rmSync(tmp, { recursive: true });
111
- });
112
- it('父目录不存在时向上找祖先', async () => {
113
- const tmp = mkdtempSync(join(tmpdir(), 'dak-paths-'));
114
- const existing = join(tmp, 'existing');
115
- mkdirSync(existing);
116
- const result = await realParentJoined(join(existing, 'missing', 'child'));
117
- expect(result).toBe(join(existing, 'missing', 'child'));
118
- rmSync(tmp, { recursive: true });
119
- });
120
- });