@saeeol/core 7.3.2 → 7.3.5
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/package.json +6 -2
- package/sst-env.d.ts +0 -10
- package/test/effect/cross-spawn-spawner.test.ts +0 -423
- package/test/effect/observability.test.ts +0 -46
- package/test/filesystem/filesystem.test.ts +0 -338
- package/test/fixture/effect-flock-worker.ts +0 -60
- package/test/fixture/flock-worker.ts +0 -72
- package/test/fixture/tmpdir.ts +0 -13
- package/test/global.test.ts +0 -16
- package/test/lib/effect.ts +0 -53
- package/test/npm-config.test.ts +0 -51
- package/test/npm.test.ts +0 -91
- package/test/saeeol/core-utils/filesystem-containment.test.ts +0 -13
- package/test/saeeol/filesystem-containment.test.ts +0 -13
- package/test/util/effect-flock.test.ts +0 -386
- package/test/util/flock.test.ts +0 -426
package/test/npm.test.ts
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import fs from "fs/promises"
|
|
2
|
-
import path from "path"
|
|
3
|
-
import { describe, expect, test } from "bun:test"
|
|
4
|
-
import { NodeFileSystem } from "@effect/platform-node"
|
|
5
|
-
import { Effect, Layer, Option } from "effect"
|
|
6
|
-
import { AppFileSystem } from "@saeeol/core/filesystem"
|
|
7
|
-
import { Global } from "@saeeol/core/global"
|
|
8
|
-
import { Npm } from "@saeeol/core/npm"
|
|
9
|
-
import { EffectFlock } from "@saeeol/core/util/effect-flock"
|
|
10
|
-
import { tmpdir } from "./fixture/tmpdir"
|
|
11
|
-
|
|
12
|
-
const win = process.platform === "win32"
|
|
13
|
-
|
|
14
|
-
const writePackage = (dir: string, pkg: Record<string, unknown>) =>
|
|
15
|
-
Bun.write(
|
|
16
|
-
path.join(dir, "package.json"),
|
|
17
|
-
JSON.stringify({
|
|
18
|
-
version: "1.0.0",
|
|
19
|
-
...pkg,
|
|
20
|
-
}),
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
const npmLayer = (cache: string) =>
|
|
24
|
-
Npm.layer.pipe(
|
|
25
|
-
Layer.provide(EffectFlock.layer),
|
|
26
|
-
Layer.provide(AppFileSystem.layer),
|
|
27
|
-
Layer.provide(Global.layerWith({ cache, state: path.join(cache, "state") })),
|
|
28
|
-
Layer.provide(NodeFileSystem.layer),
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
describe("Npm.sanitize", () => {
|
|
32
|
-
test("keeps normal scoped package specs unchanged", () => {
|
|
33
|
-
expect(Npm.sanitize("@saeeol/acme")).toBe("@saeeol/acme")
|
|
34
|
-
expect(Npm.sanitize("@saeeol/acme@1.0.0")).toBe("@saeeol/acme@1.0.0")
|
|
35
|
-
expect(Npm.sanitize("prettier")).toBe("prettier")
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
test("handles git https specs", () => {
|
|
39
|
-
const spec = "acme@git+https://github.com/saeeol/acme.git"
|
|
40
|
-
const expected = win ? "acme@git+https_//github.com/saeeol/acme.git" : spec
|
|
41
|
-
expect(Npm.sanitize(spec)).toBe(expected)
|
|
42
|
-
})
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
describe("Npm.add", () => {
|
|
46
|
-
test("reifies when package cache directory exists without the package installed", async () => {
|
|
47
|
-
await using tmp = await tmpdir()
|
|
48
|
-
await fs.mkdir(path.join(tmp.path, "fixture-provider"))
|
|
49
|
-
await writePackage(path.join(tmp.path, "fixture-provider"), {
|
|
50
|
-
name: "fixture-provider",
|
|
51
|
-
main: "index.js",
|
|
52
|
-
})
|
|
53
|
-
await Bun.write(path.join(tmp.path, "fixture-provider", "index.js"), "export const fixture = true\n")
|
|
54
|
-
|
|
55
|
-
const spec = `fixture-provider@file:${path.join(tmp.path, "fixture-provider")}`
|
|
56
|
-
await fs.mkdir(path.join(tmp.path, "cache", "packages", Npm.sanitize(spec)), { recursive: true })
|
|
57
|
-
|
|
58
|
-
const entry = await Effect.gen(function* () {
|
|
59
|
-
const npm = yield* Npm.Service
|
|
60
|
-
return yield* npm.add(spec)
|
|
61
|
-
}).pipe(Effect.scoped, Effect.provide(npmLayer(path.join(tmp.path, "cache"))), Effect.runPromise)
|
|
62
|
-
|
|
63
|
-
expect(Option.isSome(entry.entrypoint)).toBe(true)
|
|
64
|
-
})
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
describe("Npm.install", () => {
|
|
68
|
-
test("respects omit from project .npmrc", async () => {
|
|
69
|
-
await using tmp = await tmpdir()
|
|
70
|
-
|
|
71
|
-
await writePackage(tmp.path, {
|
|
72
|
-
name: "fixture",
|
|
73
|
-
dependencies: {
|
|
74
|
-
"prod-pkg": "file:./prod-pkg",
|
|
75
|
-
},
|
|
76
|
-
devDependencies: {
|
|
77
|
-
"dev-pkg": "file:./dev-pkg",
|
|
78
|
-
},
|
|
79
|
-
})
|
|
80
|
-
await Bun.write(path.join(tmp.path, ".npmrc"), "omit=dev\n")
|
|
81
|
-
await fs.mkdir(path.join(tmp.path, "prod-pkg"))
|
|
82
|
-
await fs.mkdir(path.join(tmp.path, "dev-pkg"))
|
|
83
|
-
await writePackage(path.join(tmp.path, "prod-pkg"), { name: "prod-pkg" })
|
|
84
|
-
await writePackage(path.join(tmp.path, "dev-pkg"), { name: "dev-pkg" })
|
|
85
|
-
|
|
86
|
-
await Npm.install(tmp.path)
|
|
87
|
-
|
|
88
|
-
await expect(fs.stat(path.join(tmp.path, "node_modules", "prod-pkg"))).resolves.toBeDefined()
|
|
89
|
-
await expect(fs.stat(path.join(tmp.path, "node_modules", "dev-pkg"))).rejects.toThrow()
|
|
90
|
-
})
|
|
91
|
-
})
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test"
|
|
2
|
-
import { AppFileSystem } from "@saeeol/core/filesystem"
|
|
3
|
-
|
|
4
|
-
describe("saeeol filesystem containment", () => {
|
|
5
|
-
test("keeps dot-prefixed child names internal", () => {
|
|
6
|
-
expect(AppFileSystem.contains("/a/b", "/a/b/..cache/file")).toBe(true)
|
|
7
|
-
})
|
|
8
|
-
|
|
9
|
-
test("rejects cross-drive paths on Windows", () => {
|
|
10
|
-
if (process.platform !== "win32") return
|
|
11
|
-
expect(AppFileSystem.contains("C:\\repo", "D:\\outside\\file.txt")).toBe(false)
|
|
12
|
-
})
|
|
13
|
-
})
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test"
|
|
2
|
-
import { AppFileSystem } from "@saeeol/core/filesystem"
|
|
3
|
-
|
|
4
|
-
describe("saeeol filesystem containment", () => {
|
|
5
|
-
test("keeps dot-prefixed child names internal", () => {
|
|
6
|
-
expect(AppFileSystem.contains("/a/b", "/a/b/..cache/file")).toBe(true)
|
|
7
|
-
})
|
|
8
|
-
|
|
9
|
-
test("rejects cross-drive paths on Windows", () => {
|
|
10
|
-
if (process.platform !== "win32") return
|
|
11
|
-
expect(AppFileSystem.contains("C:\\repo", "D:\\outside\\file.txt")).toBe(false)
|
|
12
|
-
})
|
|
13
|
-
})
|
|
@@ -1,386 +0,0 @@
|
|
|
1
|
-
import { describe, expect } from "bun:test"
|
|
2
|
-
import { spawn } from "child_process"
|
|
3
|
-
import fs from "fs/promises"
|
|
4
|
-
import path from "path"
|
|
5
|
-
import os from "os"
|
|
6
|
-
import { Cause, Effect, Exit, Layer } from "effect"
|
|
7
|
-
import { testEffect } from "../lib/effect"
|
|
8
|
-
import { AppFileSystem } from "@saeeol/core/filesystem"
|
|
9
|
-
import { EffectFlock } from "@saeeol/core/util/effect-flock"
|
|
10
|
-
import { Global } from "@saeeol/core/global"
|
|
11
|
-
import { Hash } from "@saeeol/core/util/hash"
|
|
12
|
-
|
|
13
|
-
function lock(dir: string, key: string) {
|
|
14
|
-
return path.join(dir, Hash.fast(key) + ".lock")
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function sleep(ms: number) {
|
|
18
|
-
return new Promise<void>((resolve) => setTimeout(resolve, ms))
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function exists(file: string) {
|
|
22
|
-
return fs
|
|
23
|
-
.stat(file)
|
|
24
|
-
.then(() => true)
|
|
25
|
-
.catch(() => false)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async function readJson<T>(p: string): Promise<T> {
|
|
29
|
-
return JSON.parse(await fs.readFile(p, "utf8"))
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
// Worker subprocess helpers
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
|
|
36
|
-
type Msg = {
|
|
37
|
-
key: string
|
|
38
|
-
dir: string
|
|
39
|
-
holdMs?: number
|
|
40
|
-
ready?: string
|
|
41
|
-
active?: string
|
|
42
|
-
done?: string
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const root = path.join(import.meta.dir, "../..")
|
|
46
|
-
const worker = path.join(import.meta.dir, "../fixture/effect-flock-worker.ts")
|
|
47
|
-
|
|
48
|
-
function run(msg: Msg) {
|
|
49
|
-
return new Promise<{ code: number; stdout: Buffer; stderr: Buffer }>((resolve) => {
|
|
50
|
-
const proc = spawn(process.execPath, [worker, JSON.stringify(msg)], { cwd: root })
|
|
51
|
-
const stdout: Buffer[] = []
|
|
52
|
-
const stderr: Buffer[] = []
|
|
53
|
-
proc.stdout?.on("data", (data) => stdout.push(Buffer.from(data)))
|
|
54
|
-
proc.stderr?.on("data", (data) => stderr.push(Buffer.from(data)))
|
|
55
|
-
proc.on("close", (code) => {
|
|
56
|
-
resolve({ code: code ?? 1, stdout: Buffer.concat(stdout), stderr: Buffer.concat(stderr) })
|
|
57
|
-
})
|
|
58
|
-
})
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function spawnWorker(msg: Msg) {
|
|
62
|
-
return spawn(process.execPath, [worker, JSON.stringify(msg)], {
|
|
63
|
-
cwd: root,
|
|
64
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
65
|
-
})
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function stopWorker(proc: ReturnType<typeof spawnWorker>) {
|
|
69
|
-
if (proc.exitCode !== null || proc.signalCode !== null) return Promise.resolve()
|
|
70
|
-
if (process.platform !== "win32" || !proc.pid) {
|
|
71
|
-
proc.kill()
|
|
72
|
-
return Promise.resolve()
|
|
73
|
-
}
|
|
74
|
-
return new Promise<void>((resolve) => {
|
|
75
|
-
const killProc = spawn("taskkill", ["/pid", String(proc.pid), "/T", "/F"])
|
|
76
|
-
killProc.on("close", () => {
|
|
77
|
-
proc.kill()
|
|
78
|
-
resolve()
|
|
79
|
-
})
|
|
80
|
-
})
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async function waitForFile(file: string, timeout = 3_000) {
|
|
84
|
-
const stop = Date.now() + timeout
|
|
85
|
-
while (Date.now() < stop) {
|
|
86
|
-
if (await exists(file)) return
|
|
87
|
-
await sleep(20)
|
|
88
|
-
}
|
|
89
|
-
throw new Error(`Timed out waiting for file: ${file}`)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ---------------------------------------------------------------------------
|
|
93
|
-
// Test layer
|
|
94
|
-
// ---------------------------------------------------------------------------
|
|
95
|
-
|
|
96
|
-
const testGlobal = Global.layerWith({
|
|
97
|
-
home: os.homedir(),
|
|
98
|
-
data: os.tmpdir(),
|
|
99
|
-
cache: os.tmpdir(),
|
|
100
|
-
config: os.tmpdir(),
|
|
101
|
-
state: os.tmpdir(),
|
|
102
|
-
bin: os.tmpdir(),
|
|
103
|
-
log: os.tmpdir(),
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
const testLayer = EffectFlock.layer.pipe(Layer.provide(testGlobal), Layer.provide(AppFileSystem.defaultLayer))
|
|
107
|
-
|
|
108
|
-
// ---------------------------------------------------------------------------
|
|
109
|
-
// Tests
|
|
110
|
-
// ---------------------------------------------------------------------------
|
|
111
|
-
|
|
112
|
-
describe("util.effect-flock", () => {
|
|
113
|
-
const it = testEffect(testLayer)
|
|
114
|
-
|
|
115
|
-
it.live(
|
|
116
|
-
"acquire and release via scoped Effect",
|
|
117
|
-
Effect.gen(function* () {
|
|
118
|
-
const flock = yield* EffectFlock.Service
|
|
119
|
-
const tmp = yield* Effect.promise(() => fs.mkdtemp(path.join(os.tmpdir(), "eflock-test-")))
|
|
120
|
-
const dir = path.join(tmp, "locks")
|
|
121
|
-
const lockDir = lock(dir, "eflock:acquire")
|
|
122
|
-
|
|
123
|
-
yield* Effect.scoped(flock.acquire("eflock:acquire", dir))
|
|
124
|
-
|
|
125
|
-
expect(yield* Effect.promise(() => exists(lockDir))).toBe(false)
|
|
126
|
-
yield* Effect.promise(() => fs.rm(tmp, { recursive: true, force: true }))
|
|
127
|
-
}),
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
it.live(
|
|
131
|
-
"withLock data-first",
|
|
132
|
-
Effect.gen(function* () {
|
|
133
|
-
const flock = yield* EffectFlock.Service
|
|
134
|
-
const tmp = yield* Effect.promise(() => fs.mkdtemp(path.join(os.tmpdir(), "eflock-test-")))
|
|
135
|
-
const dir = path.join(tmp, "locks")
|
|
136
|
-
|
|
137
|
-
let hit = false
|
|
138
|
-
yield* flock.withLock(
|
|
139
|
-
Effect.sync(() => {
|
|
140
|
-
hit = true
|
|
141
|
-
}),
|
|
142
|
-
"eflock:df",
|
|
143
|
-
dir,
|
|
144
|
-
)
|
|
145
|
-
expect(hit).toBe(true)
|
|
146
|
-
yield* Effect.promise(() => fs.rm(tmp, { recursive: true, force: true }))
|
|
147
|
-
}),
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
it.live(
|
|
151
|
-
"withLock pipeable",
|
|
152
|
-
Effect.gen(function* () {
|
|
153
|
-
const flock = yield* EffectFlock.Service
|
|
154
|
-
const tmp = yield* Effect.promise(() => fs.mkdtemp(path.join(os.tmpdir(), "eflock-test-")))
|
|
155
|
-
const dir = path.join(tmp, "locks")
|
|
156
|
-
|
|
157
|
-
let hit = false
|
|
158
|
-
yield* Effect.sync(() => {
|
|
159
|
-
hit = true
|
|
160
|
-
}).pipe(flock.withLock("eflock:pipe", dir))
|
|
161
|
-
expect(hit).toBe(true)
|
|
162
|
-
yield* Effect.promise(() => fs.rm(tmp, { recursive: true, force: true }))
|
|
163
|
-
}),
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
it.live(
|
|
167
|
-
"writes owner metadata",
|
|
168
|
-
Effect.gen(function* () {
|
|
169
|
-
const flock = yield* EffectFlock.Service
|
|
170
|
-
const tmp = yield* Effect.promise(() => fs.mkdtemp(path.join(os.tmpdir(), "eflock-test-")))
|
|
171
|
-
const dir = path.join(tmp, "locks")
|
|
172
|
-
const key = "eflock:meta"
|
|
173
|
-
const file = path.join(lock(dir, key), "meta.json")
|
|
174
|
-
|
|
175
|
-
yield* Effect.scoped(
|
|
176
|
-
Effect.gen(function* () {
|
|
177
|
-
yield* flock.acquire(key, dir)
|
|
178
|
-
const json = yield* Effect.promise(() =>
|
|
179
|
-
readJson<{ token?: unknown; pid?: unknown; hostname?: unknown; createdAt?: unknown }>(file),
|
|
180
|
-
)
|
|
181
|
-
expect(typeof json.token).toBe("string")
|
|
182
|
-
expect(typeof json.pid).toBe("number")
|
|
183
|
-
expect(typeof json.hostname).toBe("string")
|
|
184
|
-
expect(typeof json.createdAt).toBe("string")
|
|
185
|
-
}),
|
|
186
|
-
)
|
|
187
|
-
yield* Effect.promise(() => fs.rm(tmp, { recursive: true, force: true }))
|
|
188
|
-
}),
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
it.live(
|
|
192
|
-
"breaks stale lock dirs",
|
|
193
|
-
Effect.gen(function* () {
|
|
194
|
-
const flock = yield* EffectFlock.Service
|
|
195
|
-
const tmp = yield* Effect.promise(() => fs.mkdtemp(path.join(os.tmpdir(), "eflock-test-")))
|
|
196
|
-
const dir = path.join(tmp, "locks")
|
|
197
|
-
const key = "eflock:stale"
|
|
198
|
-
const lockDir = lock(dir, key)
|
|
199
|
-
|
|
200
|
-
yield* Effect.promise(async () => {
|
|
201
|
-
await fs.mkdir(lockDir, { recursive: true })
|
|
202
|
-
const old = new Date(Date.now() - 120_000)
|
|
203
|
-
await fs.utimes(lockDir, old, old)
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
let hit = false
|
|
207
|
-
yield* flock.withLock(
|
|
208
|
-
Effect.sync(() => {
|
|
209
|
-
hit = true
|
|
210
|
-
}),
|
|
211
|
-
key,
|
|
212
|
-
dir,
|
|
213
|
-
)
|
|
214
|
-
expect(hit).toBe(true)
|
|
215
|
-
yield* Effect.promise(() => fs.rm(tmp, { recursive: true, force: true }))
|
|
216
|
-
}),
|
|
217
|
-
)
|
|
218
|
-
|
|
219
|
-
it.live(
|
|
220
|
-
"recovers from stale breaker",
|
|
221
|
-
Effect.gen(function* () {
|
|
222
|
-
const flock = yield* EffectFlock.Service
|
|
223
|
-
const tmp = yield* Effect.promise(() => fs.mkdtemp(path.join(os.tmpdir(), "eflock-test-")))
|
|
224
|
-
const dir = path.join(tmp, "locks")
|
|
225
|
-
const key = "eflock:stale-breaker"
|
|
226
|
-
const lockDir = lock(dir, key)
|
|
227
|
-
const breaker = lockDir + ".breaker"
|
|
228
|
-
|
|
229
|
-
yield* Effect.promise(async () => {
|
|
230
|
-
await fs.mkdir(lockDir, { recursive: true })
|
|
231
|
-
await fs.mkdir(breaker)
|
|
232
|
-
const old = new Date(Date.now() - 120_000)
|
|
233
|
-
await fs.utimes(lockDir, old, old)
|
|
234
|
-
await fs.utimes(breaker, old, old)
|
|
235
|
-
})
|
|
236
|
-
|
|
237
|
-
let hit = false
|
|
238
|
-
yield* flock.withLock(
|
|
239
|
-
Effect.sync(() => {
|
|
240
|
-
hit = true
|
|
241
|
-
}),
|
|
242
|
-
key,
|
|
243
|
-
dir,
|
|
244
|
-
)
|
|
245
|
-
expect(hit).toBe(true)
|
|
246
|
-
expect(yield* Effect.promise(() => exists(breaker))).toBe(false)
|
|
247
|
-
yield* Effect.promise(() => fs.rm(tmp, { recursive: true, force: true }))
|
|
248
|
-
}),
|
|
249
|
-
)
|
|
250
|
-
|
|
251
|
-
it.live(
|
|
252
|
-
"detects compromise when lock dir removed",
|
|
253
|
-
Effect.gen(function* () {
|
|
254
|
-
const flock = yield* EffectFlock.Service
|
|
255
|
-
const tmp = yield* Effect.promise(() => fs.mkdtemp(path.join(os.tmpdir(), "eflock-test-")))
|
|
256
|
-
const dir = path.join(tmp, "locks")
|
|
257
|
-
const key = "eflock:compromised"
|
|
258
|
-
const lockDir = lock(dir, key)
|
|
259
|
-
|
|
260
|
-
const result = yield* flock
|
|
261
|
-
.withLock(
|
|
262
|
-
Effect.promise(() => fs.rm(lockDir, { recursive: true, force: true })),
|
|
263
|
-
key,
|
|
264
|
-
dir,
|
|
265
|
-
)
|
|
266
|
-
.pipe(Effect.exit)
|
|
267
|
-
|
|
268
|
-
expect(Exit.isFailure(result)).toBe(true)
|
|
269
|
-
expect(Exit.isFailure(result) ? Cause.pretty(result.cause) : "").toContain("missing")
|
|
270
|
-
yield* Effect.promise(() => fs.rm(tmp, { recursive: true, force: true }))
|
|
271
|
-
}),
|
|
272
|
-
)
|
|
273
|
-
|
|
274
|
-
it.live(
|
|
275
|
-
"detects token mismatch",
|
|
276
|
-
Effect.gen(function* () {
|
|
277
|
-
const flock = yield* EffectFlock.Service
|
|
278
|
-
const tmp = yield* Effect.promise(() => fs.mkdtemp(path.join(os.tmpdir(), "eflock-test-")))
|
|
279
|
-
const dir = path.join(tmp, "locks")
|
|
280
|
-
const key = "eflock:token"
|
|
281
|
-
const lockDir = lock(dir, key)
|
|
282
|
-
const meta = path.join(lockDir, "meta.json")
|
|
283
|
-
|
|
284
|
-
const result = yield* flock
|
|
285
|
-
.withLock(
|
|
286
|
-
Effect.promise(async () => {
|
|
287
|
-
const json = await readJson<{ token?: string }>(meta)
|
|
288
|
-
json.token = "tampered"
|
|
289
|
-
await fs.writeFile(meta, JSON.stringify(json, null, 2))
|
|
290
|
-
}),
|
|
291
|
-
key,
|
|
292
|
-
dir,
|
|
293
|
-
)
|
|
294
|
-
.pipe(Effect.exit)
|
|
295
|
-
|
|
296
|
-
expect(Exit.isFailure(result)).toBe(true)
|
|
297
|
-
expect(Exit.isFailure(result) ? Cause.pretty(result.cause) : "").toContain("token mismatch")
|
|
298
|
-
expect(yield* Effect.promise(() => exists(lockDir))).toBe(true)
|
|
299
|
-
yield* Effect.promise(() => fs.rm(tmp, { recursive: true, force: true }))
|
|
300
|
-
}),
|
|
301
|
-
)
|
|
302
|
-
|
|
303
|
-
it.live(
|
|
304
|
-
"fails on unwritable lock roots",
|
|
305
|
-
Effect.gen(function* () {
|
|
306
|
-
if (process.platform === "win32") return
|
|
307
|
-
const flock = yield* EffectFlock.Service
|
|
308
|
-
const tmp = yield* Effect.promise(() => fs.mkdtemp(path.join(os.tmpdir(), "eflock-test-")))
|
|
309
|
-
const dir = path.join(tmp, "locks")
|
|
310
|
-
|
|
311
|
-
yield* Effect.promise(async () => {
|
|
312
|
-
await fs.mkdir(dir, { recursive: true })
|
|
313
|
-
await fs.chmod(dir, 0o500)
|
|
314
|
-
})
|
|
315
|
-
|
|
316
|
-
const result = yield* flock.withLock(Effect.void, "eflock:perm", dir).pipe(Effect.exit)
|
|
317
|
-
// oxlint-disable-next-line no-base-to-string -- Exit has a useful toString for test assertions
|
|
318
|
-
expect(String(result)).toContain("PermissionDenied")
|
|
319
|
-
yield* Effect.promise(() => fs.chmod(dir, 0o700).then(() => fs.rm(tmp, { recursive: true, force: true })))
|
|
320
|
-
}),
|
|
321
|
-
)
|
|
322
|
-
|
|
323
|
-
it.live(
|
|
324
|
-
"enforces mutual exclusion under process contention",
|
|
325
|
-
() =>
|
|
326
|
-
Effect.promise(async () => {
|
|
327
|
-
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "eflock-stress-"))
|
|
328
|
-
const dir = path.join(tmp, "locks")
|
|
329
|
-
const done = path.join(tmp, "done.log")
|
|
330
|
-
const active = path.join(tmp, "active")
|
|
331
|
-
const n = 16
|
|
332
|
-
|
|
333
|
-
try {
|
|
334
|
-
const out = await Promise.all(
|
|
335
|
-
Array.from({ length: n }, () => run({ key: "eflock:stress", dir, done, active, holdMs: 30 })),
|
|
336
|
-
)
|
|
337
|
-
|
|
338
|
-
expect(out.map((x) => x.code)).toEqual(Array.from({ length: n }, () => 0))
|
|
339
|
-
expect(out.map((x) => x.stderr.toString()).filter(Boolean)).toEqual([])
|
|
340
|
-
|
|
341
|
-
const lines = (await fs.readFile(done, "utf8"))
|
|
342
|
-
.split("\n")
|
|
343
|
-
.map((x) => x.trim())
|
|
344
|
-
.filter(Boolean)
|
|
345
|
-
expect(lines.length).toBe(n)
|
|
346
|
-
} finally {
|
|
347
|
-
await fs.rm(tmp, { recursive: true, force: true })
|
|
348
|
-
}
|
|
349
|
-
}),
|
|
350
|
-
60_000,
|
|
351
|
-
)
|
|
352
|
-
|
|
353
|
-
it.live(
|
|
354
|
-
"recovers after a crashed lock owner",
|
|
355
|
-
() =>
|
|
356
|
-
Effect.promise(async () => {
|
|
357
|
-
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "eflock-crash-"))
|
|
358
|
-
const dir = path.join(tmp, "locks")
|
|
359
|
-
const ready = path.join(tmp, "ready")
|
|
360
|
-
|
|
361
|
-
const proc = spawnWorker({ key: "eflock:crash", dir, ready, holdMs: 120_000 })
|
|
362
|
-
|
|
363
|
-
try {
|
|
364
|
-
await waitForFile(ready, 5_000)
|
|
365
|
-
await stopWorker(proc)
|
|
366
|
-
await new Promise((resolve) => proc.on("close", resolve))
|
|
367
|
-
|
|
368
|
-
// Backdate lock files so they're past STALE_MS (60s)
|
|
369
|
-
const lockDir = lock(dir, "eflock:crash")
|
|
370
|
-
const old = new Date(Date.now() - 120_000)
|
|
371
|
-
await fs.utimes(lockDir, old, old).catch(() => {})
|
|
372
|
-
await fs.utimes(path.join(lockDir, "heartbeat"), old, old).catch(() => {})
|
|
373
|
-
await fs.utimes(path.join(lockDir, "meta.json"), old, old).catch(() => {})
|
|
374
|
-
|
|
375
|
-
const done = path.join(tmp, "done.log")
|
|
376
|
-
const result = await run({ key: "eflock:crash", dir, done, holdMs: 10 })
|
|
377
|
-
expect(result.code).toBe(0)
|
|
378
|
-
expect(result.stderr.toString()).toBe("")
|
|
379
|
-
} finally {
|
|
380
|
-
await stopWorker(proc).catch(() => {})
|
|
381
|
-
await fs.rm(tmp, { recursive: true, force: true })
|
|
382
|
-
}
|
|
383
|
-
}),
|
|
384
|
-
30_000,
|
|
385
|
-
)
|
|
386
|
-
})
|