@raidiant/notifai 10.1.1 → 10.1.2
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/CHANGELOG.md +20 -0
- package/dist/commands-core.d.ts +1 -1
- package/dist/commands-init.js +9 -6
- package/dist/commands-init.js.map +1 -1
- package/dist/commands-send-support.js +3 -0
- package/dist/commands-send-support.js.map +1 -1
- package/dist/commands-skill.js +72 -54
- package/dist/commands-skill.js.map +1 -1
- package/dist/invocation-context.d.ts +1 -0
- package/dist/invocation-context.js +9 -1
- package/dist/invocation-context.js.map +1 -1
- package/dist/native-skills.d.ts +13 -3
- package/dist/native-skills.js +51 -12
- package/dist/native-skills.js.map +1 -1
- package/dist/node-floor.d.ts +2 -1
- package/dist/node-floor.js +9 -5
- package/dist/node-floor.js.map +1 -1
- package/dist/orca-session-title.d.ts +5 -4
- package/dist/orca-session-title.js +32 -19
- package/dist/orca-session-title.js.map +1 -1
- package/dist/platform.d.ts +11 -17
- package/dist/platform.js +20 -23
- package/dist/platform.js.map +1 -1
- package/dist/release.d.ts +5 -5
- package/dist/release.js +5 -5
- package/dist/setup-destinations.js +1 -1
- package/dist/setup-destinations.js.map +1 -1
- package/dist/skill-integrity.d.ts +49 -0
- package/dist/skill-integrity.js +154 -0
- package/dist/skill-integrity.js.map +1 -0
- package/dist/skill-source/manifest.json +16 -0
- package/dist/skill-source/notifai/SKILL.md +390 -0
- package/dist/skill-source/notifai/references/harness-setup.md +220 -0
- package/package.json +11 -14
- package/src/atomic-file.ts +0 -198
- package/src/claude-wake.test.ts +0 -321
- package/src/claude-wake.ts +0 -499
- package/src/cli-bin.test.ts +0 -176
- package/src/cli-bin.ts +0 -233
- package/src/cli-release.test.ts +0 -46
- package/src/cli-release.ts +0 -78
- package/src/cli-update-contract.ts +0 -9
- package/src/cli-update.test.ts +0 -125
- package/src/client.test.ts +0 -390
- package/src/client.ts +0 -352
- package/src/codex-wake.test.ts +0 -254
- package/src/codex-wake.ts +0 -301
- package/src/command-session.ts +0 -38
- package/src/commands-agent-sessions.ts +0 -72
- package/src/commands-auth.ts +0 -238
- package/src/commands-config.ts +0 -336
- package/src/commands-core.ts +0 -312
- package/src/commands-devices.ts +0 -180
- package/src/commands-doctor.ts +0 -1501
- package/src/commands-guidance.ts +0 -181
- package/src/commands-harness-context.ts +0 -207
- package/src/commands-hook-shape.ts +0 -69
- package/src/commands-hooks.ts +0 -2318
- package/src/commands-init.ts +0 -1055
- package/src/commands-io.ts +0 -108
- package/src/commands-logs.ts +0 -243
- package/src/commands-project.ts +0 -47
- package/src/commands-send-support.test.ts +0 -156
- package/src/commands-send-support.ts +0 -311
- package/src/commands-send.ts +0 -875
- package/src/commands-setup-proof.test.ts +0 -72
- package/src/commands-setup-proof.ts +0 -124
- package/src/commands-skill.test.ts +0 -44
- package/src/commands-skill.ts +0 -204
- package/src/commands-sounds.test.ts +0 -106
- package/src/commands-sounds.ts +0 -49
- package/src/commands-update.ts +0 -180
- package/src/commands.test.ts +0 -8297
- package/src/commands.ts +0 -77
- package/src/config-schema.test.ts +0 -153
- package/src/config-schema.ts +0 -349
- package/src/config.test.ts +0 -581
- package/src/config.ts +0 -475
- package/src/credentials.test.ts +0 -403
- package/src/credentials.ts +0 -403
- package/src/file-lock-process-worker.test.ts +0 -64
- package/src/file-lock.test.ts +0 -320
- package/src/file-lock.ts +0 -393
- package/src/fixtures/hermes-0.20.6-classic-cli-local.json +0 -50
- package/src/fixtures/openclaw-2026.7.1-2-agent-local.json +0 -57
- package/src/generated-session-label.ts +0 -155
- package/src/guidance-content.ts +0 -278
- package/src/guidance-render.ts +0 -56
- package/src/guidance.test.ts +0 -327
- package/src/guidance.ts +0 -233
- package/src/harness-integration.test.ts +0 -294
- package/src/harnesses.test.ts +0 -149
- package/src/harnesses.ts +0 -164
- package/src/hook-adapter.test.ts +0 -496
- package/src/hook-adapter.ts +0 -424
- package/src/hook-input.test.ts +0 -39
- package/src/hook-input.ts +0 -72
- package/src/hooks.test.ts +0 -5824
- package/src/hooks.ts +0 -4011
- package/src/install-hooks.test.ts +0 -1614
- package/src/install-hooks.ts +0 -1501
- package/src/interactive.test.ts +0 -387
- package/src/interactive.ts +0 -858
- package/src/invocation-context.test.ts +0 -352
- package/src/invocation-context.ts +0 -197
- package/src/logging-process-worker.test.ts +0 -45
- package/src/logging.test.ts +0 -544
- package/src/logging.ts +0 -698
- package/src/main-run.ts +0 -45
- package/src/main.ts +0 -12
- package/src/native-skills.test.ts +0 -115
- package/src/native-skills.ts +0 -162
- package/src/node-floor.test.ts +0 -27
- package/src/node-floor.ts +0 -37
- package/src/onboarding-docs.test.ts +0 -25
- package/src/openclaw-plugin.ts +0 -409
- package/src/opencode-plugin.ts +0 -298
- package/src/orca-session-title.test.ts +0 -54
- package/src/orca-session-title.ts +0 -103
- package/src/pack-feedback-slice.test.ts +0 -212
- package/src/pack-feedback-slice.ts +0 -187
- package/src/platform.test.ts +0 -152
- package/src/platform.ts +0 -173
- package/src/program.test.ts +0 -317
- package/src/program.ts +0 -842
- package/src/project-enablement.test.ts +0 -55
- package/src/project-enablement.ts +0 -99
- package/src/question-caps.test.ts +0 -49
- package/src/question-settlement-process.ts +0 -47
- package/src/question-timing.ts +0 -56
- package/src/readiness.test.ts +0 -25
- package/src/readiness.ts +0 -212
- package/src/release.ts +0 -51
- package/src/send-attempts.test.ts +0 -143
- package/src/send-attempts.ts +0 -188
- package/src/send.test.ts +0 -28
- package/src/send.ts +0 -436
- package/src/session-activation.ts +0 -61
- package/src/session-end-log-lock-worker.test.ts +0 -35
- package/src/session-labels.test.ts +0 -490
- package/src/session-labels.ts +0 -474
- package/src/setup-destinations.test.ts +0 -41
- package/src/setup-destinations.ts +0 -50
- package/src/skill.test.ts +0 -426
- package/src/sound-ref.test.ts +0 -31
- package/src/sound-ref.ts +0 -34
- package/src/trust-policy.test.ts +0 -701
- package/src/ui/banner.ts +0 -95
- package/src/ui/config-view.test.ts +0 -90
- package/src/ui/config-view.ts +0 -138
- package/src/ui/help.test.ts +0 -16
- package/src/ui/help.ts +0 -128
- package/src/ui/theme.test.ts +0 -92
- package/src/ui/theme.ts +0 -265
- package/src/url-policy.ts +0 -399
- package/tsconfig.json +0 -10
package/src/file-lock.test.ts
DELETED
|
@@ -1,320 +0,0 @@
|
|
|
1
|
-
import { spawn, type ChildProcess } from 'node:child_process'
|
|
2
|
-
import {
|
|
3
|
-
existsSync,
|
|
4
|
-
mkdirSync,
|
|
5
|
-
mkdtempSync,
|
|
6
|
-
readFileSync,
|
|
7
|
-
readdirSync,
|
|
8
|
-
rmSync,
|
|
9
|
-
statSync,
|
|
10
|
-
symlinkSync,
|
|
11
|
-
writeFileSync,
|
|
12
|
-
} from 'node:fs'
|
|
13
|
-
import os from 'node:os'
|
|
14
|
-
import path from 'node:path'
|
|
15
|
-
import { describe, expect, it } from 'vitest'
|
|
16
|
-
import { withFileLock, withTargetFileLock } from './file-lock.js'
|
|
17
|
-
|
|
18
|
-
interface LockWorker {
|
|
19
|
-
child: ChildProcess
|
|
20
|
-
done: Promise<void>
|
|
21
|
-
pausedPath: string
|
|
22
|
-
continuePath: string
|
|
23
|
-
enteredPath: string
|
|
24
|
-
releasePath: string
|
|
25
|
-
waitsPath: string
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
interface LockWorkerOptions {
|
|
29
|
-
pauseAt?: string
|
|
30
|
-
action?: 'hold' | 'crash'
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function waitCount(file: string): number {
|
|
34
|
-
try {
|
|
35
|
-
return Number(readFileSync(file, 'utf8'))
|
|
36
|
-
} catch {
|
|
37
|
-
return 0
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function waitUntil(predicate: () => boolean, timeoutMs = 10_000): Promise<void> {
|
|
42
|
-
const deadline = Date.now() + timeoutMs
|
|
43
|
-
while (!predicate()) {
|
|
44
|
-
if (Date.now() >= deadline) throw new Error('timed out waiting for file-lock worker')
|
|
45
|
-
await new Promise((resolve) => setTimeout(resolve, 5))
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function lockWorker(
|
|
50
|
-
root: string,
|
|
51
|
-
lockPath: string,
|
|
52
|
-
id: string,
|
|
53
|
-
options: LockWorkerOptions = {},
|
|
54
|
-
): LockWorker {
|
|
55
|
-
const marker = (name: string): string => path.join(root, `${id}-${name}`)
|
|
56
|
-
const pausedPath = marker('paused')
|
|
57
|
-
const continuePath = marker('continue')
|
|
58
|
-
const enteredPath = marker('entered')
|
|
59
|
-
const releasePath = marker('release')
|
|
60
|
-
const waitsPath = marker('waits')
|
|
61
|
-
const vitest = path.join(process.cwd(), 'node_modules', 'vitest', 'vitest.mjs')
|
|
62
|
-
const child = spawn(
|
|
63
|
-
process.execPath,
|
|
64
|
-
[vitest, 'run', 'src/file-lock-process-worker.test.ts', '--reporter=dot', '--maxWorkers=1'],
|
|
65
|
-
{
|
|
66
|
-
cwd: process.cwd(),
|
|
67
|
-
env: {
|
|
68
|
-
...process.env,
|
|
69
|
-
NOTIFAI_FILE_LOCK_WORKER: id,
|
|
70
|
-
NOTIFAI_FILE_LOCK_PATH: lockPath,
|
|
71
|
-
NOTIFAI_FILE_LOCK_PAUSED: pausedPath,
|
|
72
|
-
NOTIFAI_FILE_LOCK_CONTINUE: continuePath,
|
|
73
|
-
NOTIFAI_FILE_LOCK_ENTERED: enteredPath,
|
|
74
|
-
NOTIFAI_FILE_LOCK_RELEASE: releasePath,
|
|
75
|
-
NOTIFAI_FILE_LOCK_WAITS: waitsPath,
|
|
76
|
-
NOTIFAI_FILE_LOCK_ACTION: options.action ?? 'hold',
|
|
77
|
-
...(options.pauseAt === undefined
|
|
78
|
-
? {}
|
|
79
|
-
: { NOTIFAI_FILE_LOCK_PAUSE_AT: options.pauseAt }),
|
|
80
|
-
},
|
|
81
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
82
|
-
},
|
|
83
|
-
)
|
|
84
|
-
const stdout: Buffer[] = []
|
|
85
|
-
const stderr: Buffer[] = []
|
|
86
|
-
child.stdout!.on('data', (chunk: Buffer) => stdout.push(chunk))
|
|
87
|
-
child.stderr!.on('data', (chunk: Buffer) => stderr.push(chunk))
|
|
88
|
-
const done = new Promise<void>((resolve, reject) => {
|
|
89
|
-
child.once('error', reject)
|
|
90
|
-
child.once('exit', (code, signal) => {
|
|
91
|
-
const expectedExit =
|
|
92
|
-
options.action === 'crash' ? code !== 0 || signal !== null : code === 0
|
|
93
|
-
if (expectedExit) resolve()
|
|
94
|
-
else {
|
|
95
|
-
reject(
|
|
96
|
-
new Error(
|
|
97
|
-
`file-lock worker ${id} exited ${String(code)}\n${Buffer.concat(stdout).toString()}\n${Buffer.concat(stderr).toString()}`,
|
|
98
|
-
),
|
|
99
|
-
)
|
|
100
|
-
}
|
|
101
|
-
})
|
|
102
|
-
})
|
|
103
|
-
void done.catch(() => undefined)
|
|
104
|
-
return { child, done, pausedPath, continuePath, enteredPath, releasePath, waitsPath }
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function stop(workers: LockWorker[]): void {
|
|
108
|
-
for (const worker of workers) {
|
|
109
|
-
if (worker.child.exitCode === null && worker.child.signalCode === null) worker.child.kill('SIGKILL')
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
describe('cross-process file lock', () => {
|
|
114
|
-
it('does not expose an ownerless publication window before acquisition is complete', async () => {
|
|
115
|
-
const root = mkdtempSync(path.join(os.tmpdir(), 'notifai-file-lock-'))
|
|
116
|
-
const lockPath = path.join(root, 'shared.lock')
|
|
117
|
-
const workers: LockWorker[] = []
|
|
118
|
-
try {
|
|
119
|
-
const first = lockWorker(root, lockPath, 'first', { pauseAt: 'choosing-published' })
|
|
120
|
-
workers.push(first)
|
|
121
|
-
await waitUntil(() => existsSync(first.pausedPath))
|
|
122
|
-
|
|
123
|
-
const published = readdirSync(lockPath)
|
|
124
|
-
expect(published).toHaveLength(1)
|
|
125
|
-
expect(published[0]).toMatch(/^choosing-\d+-[0-9a-f]+$/)
|
|
126
|
-
expect(statSync(path.join(lockPath, published[0]!)).size).toBe(0)
|
|
127
|
-
|
|
128
|
-
const second = lockWorker(root, lockPath, 'second')
|
|
129
|
-
workers.push(second)
|
|
130
|
-
await waitUntil(() => existsSync(second.pausedPath) && waitCount(second.waitsPath) > 0)
|
|
131
|
-
expect(existsSync(first.enteredPath)).toBe(false)
|
|
132
|
-
expect(existsSync(second.enteredPath)).toBe(false)
|
|
133
|
-
|
|
134
|
-
writeFileSync(first.continuePath, 'continue')
|
|
135
|
-
await waitUntil(() => existsSync(first.enteredPath) !== existsSync(second.enteredPath))
|
|
136
|
-
|
|
137
|
-
const winner = existsSync(first.enteredPath) ? first : second
|
|
138
|
-
const loser = winner === first ? second : first
|
|
139
|
-
const waitsBefore = waitCount(loser.waitsPath)
|
|
140
|
-
await waitUntil(() => {
|
|
141
|
-
if (existsSync(loser.enteredPath)) throw new Error('both lock actions overlapped')
|
|
142
|
-
return waitCount(loser.waitsPath) > waitsBefore
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
writeFileSync(winner.releasePath, 'release')
|
|
146
|
-
await waitUntil(() => existsSync(loser.enteredPath))
|
|
147
|
-
writeFileSync(loser.releasePath, 'release')
|
|
148
|
-
await Promise.all(workers.map(({ done }) => done))
|
|
149
|
-
expect(existsSync(lockPath)).toBe(false)
|
|
150
|
-
} finally {
|
|
151
|
-
stop(workers)
|
|
152
|
-
await Promise.allSettled(workers.map(({ done }) => done))
|
|
153
|
-
rmSync(root, { recursive: true, force: true })
|
|
154
|
-
}
|
|
155
|
-
}, 30_000)
|
|
156
|
-
|
|
157
|
-
it('cannot reap a successor after observing a dead owner', async () => {
|
|
158
|
-
const root = mkdtempSync(path.join(os.tmpdir(), 'notifai-file-lock-'))
|
|
159
|
-
const lockPath = path.join(root, 'shared.lock')
|
|
160
|
-
const workers: LockWorker[] = []
|
|
161
|
-
try {
|
|
162
|
-
const crashed = lockWorker(root, lockPath, 'crashed', { action: 'crash' })
|
|
163
|
-
workers.push(crashed)
|
|
164
|
-
await waitUntil(() => existsSync(crashed.enteredPath))
|
|
165
|
-
await crashed.done
|
|
166
|
-
|
|
167
|
-
const staleEntries = readdirSync(lockPath)
|
|
168
|
-
expect(staleEntries).toHaveLength(1)
|
|
169
|
-
expect(staleEntries[0]).toMatch(/^ticket-\d+-\d+-[0-9a-f]+$/)
|
|
170
|
-
const staleEntry = staleEntries[0]!
|
|
171
|
-
|
|
172
|
-
const delayedReaper = lockWorker(root, lockPath, 'delayed-reaper', {
|
|
173
|
-
pauseAt: 'stale-entry',
|
|
174
|
-
})
|
|
175
|
-
workers.push(delayedReaper)
|
|
176
|
-
await waitUntil(
|
|
177
|
-
() =>
|
|
178
|
-
existsSync(delayedReaper.pausedPath) &&
|
|
179
|
-
readFileSync(delayedReaper.pausedPath, 'utf8') === staleEntry,
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
const successor = lockWorker(root, lockPath, 'successor')
|
|
183
|
-
workers.push(successor)
|
|
184
|
-
await waitUntil(() => existsSync(successor.enteredPath))
|
|
185
|
-
const successorEntry = readdirSync(lockPath).find((entry) => entry.startsWith('ticket-'))
|
|
186
|
-
expect(successorEntry).toBeDefined()
|
|
187
|
-
expect(successorEntry).not.toBe(staleEntry)
|
|
188
|
-
|
|
189
|
-
writeFileSync(delayedReaper.continuePath, 'continue')
|
|
190
|
-
await waitUntil(() => {
|
|
191
|
-
if (existsSync(delayedReaper.enteredPath)) {
|
|
192
|
-
throw new Error('the delayed stale cleanup displaced the successor')
|
|
193
|
-
}
|
|
194
|
-
return waitCount(delayedReaper.waitsPath) > 0
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
writeFileSync(successor.releasePath, 'release')
|
|
198
|
-
await waitUntil(() => existsSync(delayedReaper.enteredPath))
|
|
199
|
-
writeFileSync(delayedReaper.releasePath, 'release')
|
|
200
|
-
await Promise.all([successor.done, delayedReaper.done])
|
|
201
|
-
} finally {
|
|
202
|
-
stop(workers)
|
|
203
|
-
await Promise.allSettled(workers.map(({ done }) => done))
|
|
204
|
-
rmSync(root, { recursive: true, force: true })
|
|
205
|
-
}
|
|
206
|
-
}, 30_000)
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
describe('rendezvous contention', () => {
|
|
210
|
-
// The last contender out removes the rendezvous directory, so a contender
|
|
211
|
-
// registering at that instant races the removal. Both tests drive that race
|
|
212
|
-
// from the registering observation, which is the window the racing releaser
|
|
213
|
-
// would otherwise have to hit by luck — on a loaded machine it does.
|
|
214
|
-
function raceAtRegistration(interfere: (lock: string) => void): void {
|
|
215
|
-
const root = mkdtempSync(path.join(os.tmpdir(), 'notifai-rendezvous-'))
|
|
216
|
-
const lock = path.join(root, 'shared.lock')
|
|
217
|
-
let raced = false
|
|
218
|
-
let entered = 0
|
|
219
|
-
|
|
220
|
-
withFileLock(lock, () => { entered += 1 }, {
|
|
221
|
-
observe(observation) {
|
|
222
|
-
if (observation.phase !== 'registering' || raced) return
|
|
223
|
-
raced = true
|
|
224
|
-
interfere(lock)
|
|
225
|
-
},
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
expect(raced).toBe(true)
|
|
229
|
-
expect(entered).toBe(1)
|
|
230
|
-
// Release still cleans up after itself, whichever rendezvous it ended in.
|
|
231
|
-
expect(existsSync(lock)).toBe(false)
|
|
232
|
-
rmSync(root, { recursive: true, force: true })
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
it('registers again when a releasing contender removed the rendezvous', () => {
|
|
236
|
-
raceAtRegistration((lock) => rmSync(lock, { recursive: true }))
|
|
237
|
-
})
|
|
238
|
-
|
|
239
|
-
it('holds the rendezvous it landed in, not the one it scanned', () => {
|
|
240
|
-
// A third contender recreates the directory before this one publishes, so
|
|
241
|
-
// the entry lands in a directory whose identity nobody has recorded yet.
|
|
242
|
-
// Treating that as tampering used to break the caller outright.
|
|
243
|
-
raceAtRegistration((lock) => {
|
|
244
|
-
rmSync(lock, { recursive: true })
|
|
245
|
-
mkdirSync(lock, { mode: 0o700 })
|
|
246
|
-
})
|
|
247
|
-
})
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
describe('target file transaction lock', () => {
|
|
251
|
-
it('refuses a symlinked target parent before publishing a contender', () => {
|
|
252
|
-
const root = mkdtempSync(path.join(os.tmpdir(), 'notifai-target-lock-'))
|
|
253
|
-
const foreign = path.join(root, 'foreign')
|
|
254
|
-
const linked = path.join(root, 'linked')
|
|
255
|
-
mkdirSync(foreign)
|
|
256
|
-
symlinkSync(foreign, linked)
|
|
257
|
-
|
|
258
|
-
expect(() => withTargetFileLock(path.join(linked, 'settings.json'), () => {})).toThrow(
|
|
259
|
-
/not a regular directory/,
|
|
260
|
-
)
|
|
261
|
-
expect(existsSync(path.join(foreign, '.settings.json.notifai.lock'))).toBe(false)
|
|
262
|
-
})
|
|
263
|
-
|
|
264
|
-
it('refuses a symlink posing as the lock rendezvous', () => {
|
|
265
|
-
const root = mkdtempSync(path.join(os.tmpdir(), 'notifai-target-lock-'))
|
|
266
|
-
const foreign = path.join(root, 'foreign')
|
|
267
|
-
const lock = path.join(root, '.settings.json.notifai.lock')
|
|
268
|
-
mkdirSync(foreign)
|
|
269
|
-
symlinkSync(foreign, lock)
|
|
270
|
-
|
|
271
|
-
expect(() => withTargetFileLock(path.join(root, 'settings.json'), () => {})).toThrow(
|
|
272
|
-
/not a regular directory/,
|
|
273
|
-
)
|
|
274
|
-
})
|
|
275
|
-
|
|
276
|
-
it('does not remove a replacement at its unique owned entry', () => {
|
|
277
|
-
const root = mkdtempSync(path.join(os.tmpdir(), 'notifai-target-lock-'))
|
|
278
|
-
const target = path.join(root, 'settings.json')
|
|
279
|
-
const lock = path.join(root, '.settings.json.notifai.lock')
|
|
280
|
-
let replacement = ''
|
|
281
|
-
|
|
282
|
-
expect(() =>
|
|
283
|
-
withTargetFileLock(target, () => {
|
|
284
|
-
const entry = readdirSync(lock)[0]!
|
|
285
|
-
replacement = path.join(lock, entry)
|
|
286
|
-
rmSync(replacement)
|
|
287
|
-
writeFileSync(replacement, 'replacement')
|
|
288
|
-
}),
|
|
289
|
-
).toThrow(/changed while held/)
|
|
290
|
-
expect(readFileSync(replacement, 'utf8')).toBe('replacement')
|
|
291
|
-
})
|
|
292
|
-
|
|
293
|
-
it('preserves the action error when release also finds a replacement', () => {
|
|
294
|
-
const root = mkdtempSync(path.join(os.tmpdir(), 'notifai-target-lock-'))
|
|
295
|
-
const target = path.join(root, 'settings.json')
|
|
296
|
-
const lock = path.join(root, '.settings.json.notifai.lock')
|
|
297
|
-
const actionError = new Error('merge failed')
|
|
298
|
-
|
|
299
|
-
expect(() =>
|
|
300
|
-
withTargetFileLock(target, () => {
|
|
301
|
-
const entry = path.join(lock, readdirSync(lock)[0]!)
|
|
302
|
-
rmSync(entry)
|
|
303
|
-
writeFileSync(entry, 'replacement')
|
|
304
|
-
throw actionError
|
|
305
|
-
}),
|
|
306
|
-
).toThrow(actionError)
|
|
307
|
-
})
|
|
308
|
-
|
|
309
|
-
it('refuses a symlink masquerading as a protocol entry', () => {
|
|
310
|
-
const root = mkdtempSync(path.join(os.tmpdir(), 'notifai-target-lock-'))
|
|
311
|
-
const lock = path.join(root, 'shared.lock')
|
|
312
|
-
const foreign = path.join(root, 'foreign')
|
|
313
|
-
mkdirSync(lock)
|
|
314
|
-
writeFileSync(foreign, 'leave me')
|
|
315
|
-
symlinkSync(foreign, path.join(lock, 'ticket-1-999999-deadbeef'))
|
|
316
|
-
|
|
317
|
-
expect(() => withFileLock(lock, () => {})).toThrow(/regular file-lock entry/)
|
|
318
|
-
expect(readFileSync(foreign, 'utf8')).toBe('leave me')
|
|
319
|
-
})
|
|
320
|
-
})
|