claudeup 6.2.1 → 6.3.0
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.
- package/README.md +22 -0
- package/bin/claudeup.js +6 -1
- package/package.json +6 -6
- package/src/__tests__/alias-adopt.test.ts +331 -329
- package/src/__tests__/alias-parser.test.ts +384 -376
- package/src/__tests__/alias-shell-writer.test.ts +638 -625
- package/src/__tests__/alias-store.test.ts +143 -143
- package/src/__tests__/catalog-cache-store.test.ts +265 -265
- package/src/__tests__/catalog-notice.test.ts +123 -123
- package/src/__tests__/cli-apply-seams.test.ts +12 -8
- package/src/__tests__/cli-router.test.ts +132 -3
- package/src/__tests__/cli-update-view.test.ts +27 -8
- package/src/__tests__/community-staleness.test.ts +0 -2
- package/src/__tests__/component-badge.test.ts +4 -4
- package/src/__tests__/content-drift.test.ts +5 -1
- package/src/__tests__/conventions-integration.test.ts +774 -689
- package/src/__tests__/conventions-manager.test.ts +1216 -1152
- package/src/__tests__/doctor-bins.test.ts +14 -6
- package/src/__tests__/doctor.test.ts +438 -425
- package/src/__tests__/dual-write-prevention.test.ts +277 -282
- package/src/__tests__/enabled-not-installed.test.ts +71 -63
- package/src/__tests__/file-locking.test.ts +196 -196
- package/src/__tests__/gap-fill-versions.test.ts +318 -311
- package/src/__tests__/github-budget.test.ts +170 -164
- package/src/__tests__/gitignore-detector.test.ts +160 -152
- package/src/__tests__/gitignore-fixer.test.ts +285 -247
- package/src/__tests__/gitignore-prerun.test.ts +57 -57
- package/src/__tests__/gitignore-resolver.test.ts +173 -146
- package/src/__tests__/gitignore-service.test.ts +121 -119
- package/src/__tests__/install-command.test.ts +3 -3
- package/src/__tests__/install-plan.test.ts +13 -7
- package/src/__tests__/manifest.test.ts +16 -5
- package/src/__tests__/markdown-renderer.test.ts +0 -1
- package/src/__tests__/marketplace-badge.test.ts +111 -90
- package/src/__tests__/marketplace-version.test.ts +64 -61
- package/src/__tests__/plugin-manager-fallback.test.ts +249 -239
- package/src/__tests__/plugin-requires.test.ts +18 -4
- package/src/__tests__/plugin-setup.test.ts +12 -12
- package/src/__tests__/profile-command.test.ts +1 -7
- package/src/__tests__/profile-materializer.test.ts +31 -8
- package/src/__tests__/profile-sync.test.ts +23 -5
- package/src/__tests__/registry-version-resolution.test.ts +61 -47
- package/src/__tests__/resolve-executable.test.ts +1 -1
- package/src/__tests__/resolver.test.ts +38 -13
- package/src/__tests__/scope-action.test.ts +52 -50
- package/src/__tests__/scope-squares.test.tsx +4 -5
- package/src/__tests__/skill-install-files.test.ts +111 -103
- package/src/__tests__/symlink-manager.test.ts +27 -10
- package/src/__tests__/theme-env.test.ts +281 -0
- package/src/__tests__/theme-resolve.test.ts +380 -0
- package/src/__tests__/update-apply.test.ts +1 -3
- package/src/__tests__/useGitignoreModal.test.ts +69 -61
- package/src/__tests__/version-snapshot.test.ts +183 -165
- package/src/cli/ansi.ts +23 -15
- package/src/cli/doctor.ts +10 -9
- package/src/cli/install.ts +3 -3
- package/src/cli/profile.ts +5 -4
- package/src/cli/prompt.ts +0 -1
- package/src/cli/router.ts +69 -2
- package/src/cli/update-view.ts +11 -6
- package/src/cli/update.ts +9 -4
- package/src/cli/upgrade.ts +6 -4
- package/src/data/alias-flags.ts +260 -260
- package/src/data/gitignore-defaults.ts +20 -20
- package/src/data/gitignore-reasons.ts +86 -92
- package/src/data/gitignore-templates.ts +14 -14
- package/src/data/predefined-profiles.ts +1 -2
- package/src/data/skill-repos.ts +43 -35
- package/src/main.tsx +29 -6
- package/src/opentui.d.ts +1 -3
- package/src/prerunner/index.ts +7 -8
- package/src/services/alias-settings.ts +20 -23
- package/src/services/alias-shell-writer.ts +800 -803
- package/src/services/alias-store.ts +77 -77
- package/src/services/claude-settings.ts +27 -18
- package/src/services/content-drift.ts +13 -3
- package/src/services/conventions-manager.ts +692 -617
- package/src/services/doctor.ts +464 -403
- package/src/services/gitignore-detector.ts +113 -103
- package/src/services/gitignore-fixer.ts +204 -189
- package/src/services/gitignore-prerun.ts +26 -26
- package/src/services/gitignore-resolver.ts +162 -144
- package/src/services/gitignore-service.ts +120 -117
- package/src/services/install-plan.ts +4 -4
- package/src/services/manifest.ts +1 -1
- package/src/services/marketplace-catalog-git.ts +5 -2
- package/src/services/marketplace-fetcher.ts +241 -235
- package/src/services/plugin-manager.ts +17 -20
- package/src/services/plugin-mcp-config.ts +2 -2
- package/src/services/plugin-requires.ts +9 -6
- package/src/services/plugin-setup.ts +7 -9
- package/src/services/plugin-version-check.ts +4 -4
- package/src/services/profile-materializer.ts +9 -5
- package/src/services/profile-sync.ts +12 -4
- package/src/services/profiles.ts +3 -3
- package/src/services/resolver.ts +10 -6
- package/src/services/settings-manager.ts +82 -46
- package/src/services/skills-manager.ts +11 -9
- package/src/services/symlink-manager.ts +2 -5
- package/src/services/toolchain.ts +3 -3
- package/src/services/update-cache.ts +1 -1
- package/src/services/update-engine.ts +4 -4
- package/src/services/update-plan.ts +9 -11
- package/src/services/version-snapshot.ts +88 -88
- package/src/types/gitignore.ts +28 -28
- package/src/ui/App.tsx +36 -36
- package/src/ui/adapters/settingsAdapter.ts +2 -2
- package/src/ui/components/CategoryHeader.tsx +1 -1
- package/src/ui/components/FlagDetailEditor.tsx +0 -0
- package/src/ui/components/ScopeIndicator.tsx +3 -1
- package/src/ui/components/ScrollableList.tsx +2 -1
- package/src/ui/components/StyledText.tsx +1 -1
- package/src/ui/components/layout/FooterHints.tsx +5 -3
- package/src/ui/components/layout/Panel.tsx +2 -2
- package/src/ui/components/layout/ScreenLayout.tsx +5 -4
- package/src/ui/components/modals/InputModal.tsx +7 -1
- package/src/ui/components/modals/ModalContainer.tsx +3 -3
- package/src/ui/components/modals/SelectModal.tsx +1 -1
- package/src/ui/components/primitives/DetailSection.tsx +1 -1
- package/src/ui/components/primitives/KeyValueLine.tsx +1 -1
- package/src/ui/components/primitives/ScopeSquares.tsx +2 -2
- package/src/ui/hooks/index.ts +0 -1
- package/src/ui/hooks/useGitignoreModal.ts +1 -5
- package/src/ui/hooks/useMismatchModal.ts +4 -10
- package/src/ui/renderers/cliToolRenderers.tsx +2 -2
- package/src/ui/renderers/gitignoreRenderers.tsx +2 -2
- package/src/ui/renderers/mcpRenderers.tsx +1 -1
- package/src/ui/renderers/pluginRenderers.tsx +3 -9
- package/src/ui/renderers/profileRenderers.tsx +9 -8
- package/src/ui/renderers/settingsRenderers.tsx +4 -4
- package/src/ui/renderers/skillRenderers.tsx +14 -14
- package/src/ui/screens/AliasScreen.tsx +31 -34
- package/src/ui/screens/CliToolsScreen.tsx +14 -13
- package/src/ui/screens/EnvVarsScreen.tsx +8 -8
- package/src/ui/screens/GitignoreScreen.tsx +15 -14
- package/src/ui/screens/McpRegistryScreen.tsx +10 -7
- package/src/ui/screens/McpScreen.tsx +10 -10
- package/src/ui/screens/PluginsScreen.tsx +17 -21
- package/src/ui/screens/ProfilesScreen.tsx +25 -24
- package/src/ui/screens/SkillsScreen.tsx +16 -14
- package/src/ui/state/AppContext.tsx +4 -8
- package/src/ui/state/DimensionsContext.tsx +3 -2
- package/src/ui/theme-mode.ts +20 -38
- package/src/ui/theme-resolve.ts +148 -0
- package/src/utils/clipboard.ts +5 -5
- package/src/utils/command-utils.ts +2 -2
- package/src/utils/config-dir.ts +1 -3
- package/src/utils/file-locking.ts +88 -86
- package/src/ui/hooks/useAsyncData.ts +0 -127
- package/src/ui/state/AnimationContext.tsx +0 -76
|
@@ -13,75 +13,83 @@ import type { PluginInfo } from "../services/plugin-manager.js";
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
const plugin = (over: Partial<PluginInfo>): PluginInfo =>
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
({
|
|
17
|
+
id: "x@magus",
|
|
18
|
+
name: "x",
|
|
19
|
+
version: "1.0.0",
|
|
20
|
+
description: "",
|
|
21
|
+
marketplace: "magus",
|
|
22
|
+
marketplaceDisplay: "Magus",
|
|
23
|
+
enabled: true,
|
|
24
|
+
...over,
|
|
25
|
+
}) as PluginInfo;
|
|
26
26
|
|
|
27
27
|
describe("isEnabledButNotInstalled", () => {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
28
|
+
test("enabled in project scope with no installed version is flagged", () => {
|
|
29
|
+
// The real case: terminal/code-analysis are in this project's enabledPlugins
|
|
30
|
+
// but have no registry entry for it and none at user scope.
|
|
31
|
+
expect(
|
|
32
|
+
isEnabledButNotInstalled(
|
|
33
|
+
plugin({
|
|
34
|
+
projectScope: { enabled: true },
|
|
35
|
+
installedVersion: undefined,
|
|
36
|
+
}),
|
|
37
|
+
),
|
|
38
|
+
).toBe(true);
|
|
39
|
+
});
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
test("a healthy install is not flagged", () => {
|
|
42
|
+
expect(
|
|
43
|
+
isEnabledButNotInstalled(
|
|
44
|
+
plugin({ projectScope: { enabled: true }, installedVersion: "1.0.0" }),
|
|
45
|
+
),
|
|
46
|
+
).toBe(false);
|
|
47
|
+
});
|
|
45
48
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
test('installedVersion "0.0.0" means installed-with-unknown-version, not missing', () => {
|
|
50
|
+
// Anthropic's official plugins record exactly this. Treating it as missing
|
|
51
|
+
// would mislabel every one of them as broken.
|
|
52
|
+
expect(
|
|
53
|
+
isEnabledButNotInstalled(
|
|
54
|
+
plugin({ userScope: { enabled: true }, installedVersion: "0.0.0" }),
|
|
55
|
+
),
|
|
56
|
+
).toBe(false);
|
|
57
|
+
});
|
|
55
58
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
test("not enabled anywhere is not flagged — it is simply available", () => {
|
|
60
|
+
expect(
|
|
61
|
+
isEnabledButNotInstalled(plugin({ installedVersion: undefined })),
|
|
62
|
+
).toBe(false);
|
|
63
|
+
});
|
|
59
64
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
test("orphaned plugins are excluded — they have their own 'deprecated' state", () => {
|
|
66
|
+
expect(
|
|
67
|
+
isEnabledButNotInstalled(
|
|
68
|
+
plugin({
|
|
69
|
+
isOrphaned: true,
|
|
70
|
+
projectScope: { enabled: true },
|
|
71
|
+
installedVersion: undefined,
|
|
72
|
+
}),
|
|
73
|
+
),
|
|
74
|
+
).toBe(false);
|
|
75
|
+
});
|
|
71
76
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
test("any enabled scope counts, not just project", () => {
|
|
78
|
+
for (const scope of ["userScope", "projectScope", "localScope"] as const) {
|
|
79
|
+
expect(
|
|
80
|
+
isEnabledButNotInstalled(plugin({ [scope]: { enabled: true } })),
|
|
81
|
+
).toBe(true);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
79
84
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
test("a scope present but disabled does not count as enabled", () => {
|
|
86
|
+
expect(
|
|
87
|
+
isEnabledButNotInstalled(
|
|
88
|
+
plugin({
|
|
89
|
+
projectScope: { enabled: false },
|
|
90
|
+
installedVersion: undefined,
|
|
91
|
+
}),
|
|
92
|
+
),
|
|
93
|
+
).toBe(false);
|
|
94
|
+
});
|
|
87
95
|
});
|
|
@@ -8,201 +8,201 @@
|
|
|
8
8
|
* - Lock timeout handling
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
12
|
-
import { mkdtemp, rm } from
|
|
13
|
-
import { tmpdir } from
|
|
14
|
-
import { join } from
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
12
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
15
|
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
} from
|
|
21
|
-
|
|
22
|
-
describe(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
16
|
+
forceRemoveLock,
|
|
17
|
+
getLockInfo,
|
|
18
|
+
isFileLocked,
|
|
19
|
+
withFileLock,
|
|
20
|
+
} from "../utils/file-locking.js";
|
|
21
|
+
|
|
22
|
+
describe("file-locking", () => {
|
|
23
|
+
let testDir: string;
|
|
24
|
+
let testFile: string;
|
|
25
|
+
|
|
26
|
+
beforeEach(async () => {
|
|
27
|
+
// Create temporary directory for tests
|
|
28
|
+
testDir = await mkdtemp(join(tmpdir(), "claudeup-test-"));
|
|
29
|
+
testFile = join(testDir, "test-file.json");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(async () => {
|
|
33
|
+
// Clean up test directory
|
|
34
|
+
await rm(testDir, { recursive: true, force: true });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("withFileLock", () => {
|
|
38
|
+
it("should execute operation with lock held - TEST-095", async () => {
|
|
39
|
+
let operationExecuted = false;
|
|
40
|
+
|
|
41
|
+
await withFileLock(testFile, async () => {
|
|
42
|
+
operationExecuted = true;
|
|
43
|
+
expect(isFileLocked(testFile)).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(operationExecuted).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should release lock after operation completes - TEST-095", async () => {
|
|
50
|
+
await withFileLock(testFile, async () => {
|
|
51
|
+
// Operation
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(isFileLocked(testFile)).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should release lock even if operation throws", async () => {
|
|
58
|
+
await expect(
|
|
59
|
+
withFileLock(testFile, async () => {
|
|
60
|
+
throw new Error("Operation failed");
|
|
61
|
+
}),
|
|
62
|
+
).rejects.toThrow("Operation failed");
|
|
63
|
+
|
|
64
|
+
expect(isFileLocked(testFile)).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("should return operation result", async () => {
|
|
68
|
+
const result = await withFileLock(testFile, async () => {
|
|
69
|
+
return "success";
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(result).toBe("success");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("should block concurrent access - TEST-095", async () => {
|
|
76
|
+
const executionOrder: number[] = [];
|
|
77
|
+
|
|
78
|
+
// First operation holds lock for 100ms
|
|
79
|
+
const promise1 = withFileLock(testFile, async () => {
|
|
80
|
+
executionOrder.push(1);
|
|
81
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
82
|
+
executionOrder.push(2);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Wait a bit to ensure first lock is acquired
|
|
86
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
87
|
+
|
|
88
|
+
// Second operation should wait for lock
|
|
89
|
+
const promise2 = withFileLock(testFile, async () => {
|
|
90
|
+
executionOrder.push(3);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await Promise.all([promise1, promise2]);
|
|
94
|
+
|
|
95
|
+
// Second operation should only execute after first completes
|
|
96
|
+
expect(executionOrder).toEqual([1, 2, 3]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("should timeout if lock cannot be acquired", async () => {
|
|
100
|
+
// Skip this test in CI as it takes 10+ seconds
|
|
101
|
+
// The timeout behavior is tested in integration tests
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should handle multiple sequential operations", async () => {
|
|
105
|
+
for (let i = 0; i < 5; i++) {
|
|
106
|
+
await withFileLock(testFile, async () => {
|
|
107
|
+
expect(isFileLocked(testFile)).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
expect(isFileLocked(testFile)).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("isFileLocked", () => {
|
|
116
|
+
it("should return false for unlocked files", () => {
|
|
117
|
+
expect(isFileLocked(testFile)).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("should return true while lock is held", async () => {
|
|
121
|
+
await withFileLock(testFile, async () => {
|
|
122
|
+
expect(isFileLocked(testFile)).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("should return false after lock is released", async () => {
|
|
127
|
+
await withFileLock(testFile, async () => {});
|
|
128
|
+
expect(isFileLocked(testFile)).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("forceRemoveLock", () => {
|
|
133
|
+
it("should remove lock file", async () => {
|
|
134
|
+
// Create lock by starting operation but not finishing
|
|
135
|
+
await withFileLock(testFile, async () => {
|
|
136
|
+
// Lock is held here
|
|
137
|
+
expect(isFileLocked(testFile)).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Lock should be released
|
|
141
|
+
expect(isFileLocked(testFile)).toBe(false);
|
|
142
|
+
|
|
143
|
+
// Force remove should not throw on non-existent lock
|
|
144
|
+
await forceRemoveLock(testFile);
|
|
145
|
+
expect(isFileLocked(testFile)).toBe(false);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("should not throw if lock file does not exist", async () => {
|
|
149
|
+
await forceRemoveLock(testFile);
|
|
150
|
+
expect(isFileLocked(testFile)).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("getLockInfo", () => {
|
|
155
|
+
it("should return null for unlocked files", async () => {
|
|
156
|
+
const info = await getLockInfo(testFile);
|
|
157
|
+
expect(info).toBeNull();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("should return lock information while locked", async () => {
|
|
161
|
+
await withFileLock(testFile, async () => {
|
|
162
|
+
const info = await getLockInfo(testFile);
|
|
163
|
+
expect(info).not.toBeNull();
|
|
164
|
+
expect(info?.pid).toBe(String(process.pid));
|
|
165
|
+
expect(info?.timestamp).toBeDefined();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("should return null after lock is released", async () => {
|
|
170
|
+
await withFileLock(testFile, async () => {});
|
|
171
|
+
const info = await getLockInfo(testFile);
|
|
172
|
+
expect(info).toBeNull();
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("edge cases", () => {
|
|
177
|
+
it("should handle nested lock attempts on different files", async () => {
|
|
178
|
+
const file1 = join(testDir, "file1.json");
|
|
179
|
+
const file2 = join(testDir, "file2.json");
|
|
180
|
+
|
|
181
|
+
await withFileLock(file1, async () => {
|
|
182
|
+
await withFileLock(file2, async () => {
|
|
183
|
+
expect(isFileLocked(file1)).toBe(true);
|
|
184
|
+
expect(isFileLocked(file2)).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
expect(isFileLocked(file1)).toBe(false);
|
|
189
|
+
expect(isFileLocked(file2)).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("should prevent nested lock attempts on same file", async () => {
|
|
193
|
+
// Skip this test in CI as it takes 10+ seconds to timeout
|
|
194
|
+
// The nested lock prevention is tested via other tests
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("should handle rapid lock/unlock cycles", async () => {
|
|
198
|
+
const iterations = 10;
|
|
199
|
+
for (let i = 0; i < iterations; i++) {
|
|
200
|
+
await withFileLock(testFile, async () => {
|
|
201
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
expect(isFileLocked(testFile)).toBe(false);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
208
|
});
|