@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.
Files changed (156) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/commands-core.d.ts +1 -1
  3. package/dist/commands-init.js +9 -6
  4. package/dist/commands-init.js.map +1 -1
  5. package/dist/commands-send-support.js +3 -0
  6. package/dist/commands-send-support.js.map +1 -1
  7. package/dist/commands-skill.js +72 -54
  8. package/dist/commands-skill.js.map +1 -1
  9. package/dist/invocation-context.d.ts +1 -0
  10. package/dist/invocation-context.js +9 -1
  11. package/dist/invocation-context.js.map +1 -1
  12. package/dist/native-skills.d.ts +13 -3
  13. package/dist/native-skills.js +51 -12
  14. package/dist/native-skills.js.map +1 -1
  15. package/dist/node-floor.d.ts +2 -1
  16. package/dist/node-floor.js +9 -5
  17. package/dist/node-floor.js.map +1 -1
  18. package/dist/orca-session-title.d.ts +5 -4
  19. package/dist/orca-session-title.js +32 -19
  20. package/dist/orca-session-title.js.map +1 -1
  21. package/dist/platform.d.ts +11 -17
  22. package/dist/platform.js +20 -23
  23. package/dist/platform.js.map +1 -1
  24. package/dist/release.d.ts +5 -5
  25. package/dist/release.js +5 -5
  26. package/dist/setup-destinations.js +1 -1
  27. package/dist/setup-destinations.js.map +1 -1
  28. package/dist/skill-integrity.d.ts +49 -0
  29. package/dist/skill-integrity.js +154 -0
  30. package/dist/skill-integrity.js.map +1 -0
  31. package/dist/skill-source/manifest.json +16 -0
  32. package/dist/skill-source/notifai/SKILL.md +390 -0
  33. package/dist/skill-source/notifai/references/harness-setup.md +220 -0
  34. package/package.json +11 -14
  35. package/src/atomic-file.ts +0 -198
  36. package/src/claude-wake.test.ts +0 -321
  37. package/src/claude-wake.ts +0 -499
  38. package/src/cli-bin.test.ts +0 -176
  39. package/src/cli-bin.ts +0 -233
  40. package/src/cli-release.test.ts +0 -46
  41. package/src/cli-release.ts +0 -78
  42. package/src/cli-update-contract.ts +0 -9
  43. package/src/cli-update.test.ts +0 -125
  44. package/src/client.test.ts +0 -390
  45. package/src/client.ts +0 -352
  46. package/src/codex-wake.test.ts +0 -254
  47. package/src/codex-wake.ts +0 -301
  48. package/src/command-session.ts +0 -38
  49. package/src/commands-agent-sessions.ts +0 -72
  50. package/src/commands-auth.ts +0 -238
  51. package/src/commands-config.ts +0 -336
  52. package/src/commands-core.ts +0 -312
  53. package/src/commands-devices.ts +0 -180
  54. package/src/commands-doctor.ts +0 -1501
  55. package/src/commands-guidance.ts +0 -181
  56. package/src/commands-harness-context.ts +0 -207
  57. package/src/commands-hook-shape.ts +0 -69
  58. package/src/commands-hooks.ts +0 -2318
  59. package/src/commands-init.ts +0 -1055
  60. package/src/commands-io.ts +0 -108
  61. package/src/commands-logs.ts +0 -243
  62. package/src/commands-project.ts +0 -47
  63. package/src/commands-send-support.test.ts +0 -156
  64. package/src/commands-send-support.ts +0 -311
  65. package/src/commands-send.ts +0 -875
  66. package/src/commands-setup-proof.test.ts +0 -72
  67. package/src/commands-setup-proof.ts +0 -124
  68. package/src/commands-skill.test.ts +0 -44
  69. package/src/commands-skill.ts +0 -204
  70. package/src/commands-sounds.test.ts +0 -106
  71. package/src/commands-sounds.ts +0 -49
  72. package/src/commands-update.ts +0 -180
  73. package/src/commands.test.ts +0 -8297
  74. package/src/commands.ts +0 -77
  75. package/src/config-schema.test.ts +0 -153
  76. package/src/config-schema.ts +0 -349
  77. package/src/config.test.ts +0 -581
  78. package/src/config.ts +0 -475
  79. package/src/credentials.test.ts +0 -403
  80. package/src/credentials.ts +0 -403
  81. package/src/file-lock-process-worker.test.ts +0 -64
  82. package/src/file-lock.test.ts +0 -320
  83. package/src/file-lock.ts +0 -393
  84. package/src/fixtures/hermes-0.20.6-classic-cli-local.json +0 -50
  85. package/src/fixtures/openclaw-2026.7.1-2-agent-local.json +0 -57
  86. package/src/generated-session-label.ts +0 -155
  87. package/src/guidance-content.ts +0 -278
  88. package/src/guidance-render.ts +0 -56
  89. package/src/guidance.test.ts +0 -327
  90. package/src/guidance.ts +0 -233
  91. package/src/harness-integration.test.ts +0 -294
  92. package/src/harnesses.test.ts +0 -149
  93. package/src/harnesses.ts +0 -164
  94. package/src/hook-adapter.test.ts +0 -496
  95. package/src/hook-adapter.ts +0 -424
  96. package/src/hook-input.test.ts +0 -39
  97. package/src/hook-input.ts +0 -72
  98. package/src/hooks.test.ts +0 -5824
  99. package/src/hooks.ts +0 -4011
  100. package/src/install-hooks.test.ts +0 -1614
  101. package/src/install-hooks.ts +0 -1501
  102. package/src/interactive.test.ts +0 -387
  103. package/src/interactive.ts +0 -858
  104. package/src/invocation-context.test.ts +0 -352
  105. package/src/invocation-context.ts +0 -197
  106. package/src/logging-process-worker.test.ts +0 -45
  107. package/src/logging.test.ts +0 -544
  108. package/src/logging.ts +0 -698
  109. package/src/main-run.ts +0 -45
  110. package/src/main.ts +0 -12
  111. package/src/native-skills.test.ts +0 -115
  112. package/src/native-skills.ts +0 -162
  113. package/src/node-floor.test.ts +0 -27
  114. package/src/node-floor.ts +0 -37
  115. package/src/onboarding-docs.test.ts +0 -25
  116. package/src/openclaw-plugin.ts +0 -409
  117. package/src/opencode-plugin.ts +0 -298
  118. package/src/orca-session-title.test.ts +0 -54
  119. package/src/orca-session-title.ts +0 -103
  120. package/src/pack-feedback-slice.test.ts +0 -212
  121. package/src/pack-feedback-slice.ts +0 -187
  122. package/src/platform.test.ts +0 -152
  123. package/src/platform.ts +0 -173
  124. package/src/program.test.ts +0 -317
  125. package/src/program.ts +0 -842
  126. package/src/project-enablement.test.ts +0 -55
  127. package/src/project-enablement.ts +0 -99
  128. package/src/question-caps.test.ts +0 -49
  129. package/src/question-settlement-process.ts +0 -47
  130. package/src/question-timing.ts +0 -56
  131. package/src/readiness.test.ts +0 -25
  132. package/src/readiness.ts +0 -212
  133. package/src/release.ts +0 -51
  134. package/src/send-attempts.test.ts +0 -143
  135. package/src/send-attempts.ts +0 -188
  136. package/src/send.test.ts +0 -28
  137. package/src/send.ts +0 -436
  138. package/src/session-activation.ts +0 -61
  139. package/src/session-end-log-lock-worker.test.ts +0 -35
  140. package/src/session-labels.test.ts +0 -490
  141. package/src/session-labels.ts +0 -474
  142. package/src/setup-destinations.test.ts +0 -41
  143. package/src/setup-destinations.ts +0 -50
  144. package/src/skill.test.ts +0 -426
  145. package/src/sound-ref.test.ts +0 -31
  146. package/src/sound-ref.ts +0 -34
  147. package/src/trust-policy.test.ts +0 -701
  148. package/src/ui/banner.ts +0 -95
  149. package/src/ui/config-view.test.ts +0 -90
  150. package/src/ui/config-view.ts +0 -138
  151. package/src/ui/help.test.ts +0 -16
  152. package/src/ui/help.ts +0 -128
  153. package/src/ui/theme.test.ts +0 -92
  154. package/src/ui/theme.ts +0 -265
  155. package/src/url-policy.ts +0 -399
  156. package/tsconfig.json +0 -10
@@ -1,403 +0,0 @@
1
- import { execFileSync, spawnSync } from 'node:child_process'
2
- import { existsSync, readFileSync, rmSync } from 'node:fs'
3
- import path from 'node:path'
4
- import { atomicWriteFileSync } from './atomic-file.js'
5
- import { globalConfigDir } from './config.js'
6
-
7
- export interface MachineCredential {
8
- machineId: string
9
- secret: string
10
- baseUrl: string
11
- machineName: string
12
- }
13
-
14
- export interface CredentialStore {
15
- load(): MachineCredential | null
16
- save(credential: MachineCredential): void
17
- clear(): void
18
- /** Where the secret lives, for `doctor` and docs. */
19
- describe(): string
20
- }
21
-
22
- export interface RunCommandSpec {
23
- command: string
24
- args: readonly string[]
25
- input: Buffer
26
- timeoutMs?: number
27
- }
28
-
29
- export interface RunCommandResult {
30
- status: number
31
- stdout: Buffer
32
- }
33
-
34
- /** Injected process seam. Production uses `defaultProcessRunner`. */
35
- export interface ProcessRunner {
36
- run(spec: RunCommandSpec): RunCommandResult
37
- }
38
-
39
- export interface CredentialStoreOptions {
40
- env?: NodeJS.ProcessEnv
41
- platform?: NodeJS.Platform
42
- run?: ProcessRunner
43
- keychainAvailable?: boolean
44
- }
45
-
46
- const SERVICE = 'io.notifai.cli'
47
- const CREDENTIAL_FORMAT = 'notifai.machine-credential.v1'
48
- const DPAPI_FORMAT = 'notifai.dpapi.current-user.v1'
49
- const DPAPI_ENTROPY = 'io.notifai.cli'
50
- // Inbox Windows PowerShell can have a slow first start on a newly provisioned
51
- // or heavily loaded machine. Keep the helper bounded without rejecting a
52
- // valid DPAPI operation during that cold-start window.
53
- const DPAPI_TIMEOUT_MS = 30_000
54
- const DPAPI_FILE = 'credentials.dpapi'
55
- const KEYCHAIN_TIMEOUT_MS = 15_000
56
-
57
- const PROTECT_SCRIPT = [
58
- "$ErrorActionPreference = 'Stop'",
59
- 'Add-Type -AssemblyName System.Security',
60
- '$in = New-Object System.IO.MemoryStream',
61
- '[Console]::OpenStandardInput().CopyTo($in)',
62
- `$entropy = [System.Text.Encoding]::UTF8.GetBytes('${DPAPI_ENTROPY}')`,
63
- '$protected = [System.Security.Cryptography.ProtectedData]::Protect($in.ToArray(), $entropy, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)',
64
- '$bytes = [System.Text.Encoding]::ASCII.GetBytes([Convert]::ToBase64String($protected))',
65
- '[Console]::OpenStandardOutput().Write($bytes, 0, $bytes.Length)',
66
- ].join('; ')
67
-
68
- const UNPROTECT_SCRIPT = [
69
- "$ErrorActionPreference = 'Stop'",
70
- 'Add-Type -AssemblyName System.Security',
71
- '$b64 = [System.Text.Encoding]::ASCII.GetString((& { $in = New-Object System.IO.MemoryStream; [Console]::OpenStandardInput().CopyTo($in); $in.ToArray() })).Trim()',
72
- '$protected = [Convert]::FromBase64String($b64)',
73
- `$entropy = [System.Text.Encoding]::UTF8.GetBytes('${DPAPI_ENTROPY}')`,
74
- '$plain = [System.Security.Cryptography.ProtectedData]::Unprotect($protected, $entropy, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)',
75
- '[Console]::OpenStandardOutput().Write($plain, 0, $plain.Length)',
76
- ].join('; ')
77
-
78
- const PROTECT_ENCODED = encodePowerShell(PROTECT_SCRIPT)
79
- const UNPROTECT_ENCODED = encodePowerShell(UNPROTECT_SCRIPT)
80
-
81
- function encodePowerShell(script: string): string {
82
- return Buffer.from(script, 'utf16le').toString('base64')
83
- }
84
-
85
- function powerShellInvocation(encoded: string, env: NodeJS.ProcessEnv): { command: string; args: string[] } {
86
- const root = env['SystemRoot'] || env['SYSTEMROOT'] || 'C:\\Windows'
87
- return {
88
- command: path.win32.join(root, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'),
89
- args: ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', encoded],
90
- }
91
- }
92
-
93
- /** Which DPAPI operation a process invocation asked for. Args never carry secrets. */
94
- export function windowsDpapiOperation(args: readonly string[]): 'protect' | 'unprotect' | null {
95
- const index = args.indexOf('-EncodedCommand')
96
- const encoded = index >= 0 ? args[index + 1] : undefined
97
- if (encoded === PROTECT_ENCODED) return 'protect'
98
- if (encoded === UNPROTECT_ENCODED) return 'unprotect'
99
- return null
100
- }
101
-
102
- export const defaultProcessRunner: ProcessRunner = {
103
- run(spec) {
104
- const result = spawnSync(spec.command, [...spec.args], {
105
- input: spec.input,
106
- timeout: spec.timeoutMs ?? DPAPI_TIMEOUT_MS,
107
- maxBuffer: 1024 * 1024,
108
- windowsHide: true,
109
- })
110
- const stdout = Buffer.isBuffer(result.stdout) ? result.stdout : Buffer.alloc(0)
111
- if (result.error) return { status: 1, stdout: Buffer.alloc(0) }
112
- return { status: result.status ?? 1, stdout }
113
- },
114
- }
115
-
116
- /**
117
- * macOS Keychain via the first-party `security` Security.framework client.
118
- *
119
- * `security add-generic-password -w <value>` puts the value in process argv.
120
- * Interactive mode instead parses a command from stdin, so the credential
121
- * crosses the helper boundary only through the pipe. `-X` keeps arbitrary
122
- * serialized bytes out of the command parser's quoting rules.
123
- */
124
- export class KeychainStore implements CredentialStore {
125
- private readonly run: ProcessRunner
126
-
127
- constructor(options: CredentialStoreOptions = {}) {
128
- this.run = options.run ?? defaultProcessRunner
129
- }
130
-
131
- load(): MachineCredential | null {
132
- try {
133
- const raw = execFileSync(
134
- 'security',
135
- ['find-generic-password', '-s', SERVICE, '-a', 'machine', '-w'],
136
- { stdio: ['ignore', 'pipe', 'ignore'] },
137
- )
138
- .toString()
139
- .trim()
140
- return parseCredentialJson(raw)
141
- } catch {
142
- return null
143
- }
144
- }
145
-
146
- save(credential: MachineCredential): void {
147
- const serialized = Buffer.from(JSON.stringify(serializeCredential(credential)), 'utf8')
148
- const command = Buffer.from(
149
- `add-generic-password -U -s ${SERVICE} -a machine -X ${serialized.toString('hex')}\n`,
150
- 'ascii',
151
- )
152
- const result = this.run.run({
153
- command: 'security',
154
- args: ['-q', '-i'],
155
- input: command,
156
- timeoutMs: KEYCHAIN_TIMEOUT_MS,
157
- })
158
- if (result.status !== 0) {
159
- throw new Error('macOS Keychain credential save failed')
160
- }
161
- }
162
-
163
- clear(): void {
164
- try {
165
- execFileSync('security', ['delete-generic-password', '-s', SERVICE, '-a', 'machine'], {
166
- stdio: 'ignore',
167
- })
168
- } catch {
169
- // nothing stored
170
- }
171
- }
172
-
173
- describe(): string {
174
- return `macOS Keychain (${SERVICE})`
175
- }
176
- }
177
-
178
- /**
179
- * Explicit plaintext file store.
180
- *
181
- * Default on Linux, where mode 0600 is a real ACL. Also the
182
- * `NOTIFAI_CREDENTIALS=file` development and test escape hatch on every
183
- * platform. POSIX mode bits are not an NTFS ACL, so this is not a protected
184
- * Windows store.
185
- */
186
- export class FileStore implements CredentialStore {
187
- private readonly env: NodeJS.ProcessEnv
188
- private readonly platform: NodeJS.Platform
189
-
190
- constructor(env: NodeJS.ProcessEnv = process.env, options: CredentialStoreOptions = {}) {
191
- this.env = env
192
- this.platform = options.platform ?? process.platform
193
- }
194
-
195
- private filePath(): string {
196
- return path.join(globalConfigDir(this.env, this.platform), 'credentials.json')
197
- }
198
-
199
- load(): MachineCredential | null {
200
- const file = this.filePath()
201
- if (!existsSync(file)) return null
202
- try {
203
- return parseCredentialJson(readFileSync(file, 'utf8'))
204
- } catch {
205
- return null
206
- }
207
- }
208
-
209
- save(credential: MachineCredential): void {
210
- atomicWriteFileSync(this.filePath(), `${JSON.stringify(serializeCredential(credential), null, 2)}\n`, {
211
- mode: 0o600,
212
- preserveMode: false,
213
- requireCurrentUserOwner: true,
214
- })
215
- }
216
-
217
- clear(): void {
218
- rmSync(this.filePath(), { force: true })
219
- }
220
-
221
- describe(): string {
222
- const file = this.filePath()
223
- if (this.platform === 'win32') {
224
- return `plaintext file ${file} (NOTIFAI_CREDENTIALS=file; POSIX mode bits are not protection on NTFS)`
225
- }
226
- return `plaintext file ${file} (mode 0600)`
227
- }
228
- }
229
-
230
- /**
231
- * Windows default: DPAPI CurrentUser via first-party PowerShell.
232
- *
233
- * The protected blob lives under LOCALAPPDATA, never roaming APPDATA.
234
- * Plaintext crosses the process boundary only on stdin/stdout — never as a
235
- * command-line argument.
236
- */
237
- export class WindowsDpapiStore implements CredentialStore {
238
- private readonly env: NodeJS.ProcessEnv
239
- private readonly run: ProcessRunner
240
-
241
- constructor(env: NodeJS.ProcessEnv = process.env, options: CredentialStoreOptions = {}) {
242
- this.env = env
243
- this.run = options.run ?? defaultProcessRunner
244
- }
245
-
246
- private filePath(): string | null {
247
- const local = this.env['LOCALAPPDATA']
248
- if (typeof local !== 'string' || local.trim() === '') return null
249
- return path.join(local, 'notifai', DPAPI_FILE)
250
- }
251
-
252
- load(): MachineCredential | null {
253
- const file = this.filePath()
254
- if (file === null || !existsSync(file)) return null
255
- let text: string
256
- try {
257
- text = readFileSync(file, 'utf8')
258
- } catch {
259
- return null
260
- }
261
- const blob = parseDpapiEnvelope(text)
262
- if (blob === null) return null
263
- const plain = this.unprotect(blob)
264
- return plain === null ? null : parseCredentialJson(plain)
265
- }
266
-
267
- save(credential: MachineCredential): void {
268
- const file = this.filePath()
269
- if (file === null) {
270
- throw new Error('Windows credential store path is not configured (LOCALAPPDATA is missing)')
271
- }
272
- const blob = this.protect(Buffer.from(JSON.stringify(serializeCredential(credential)), 'utf8'))
273
- atomicWriteFileSync(file, `${JSON.stringify({ format: DPAPI_FORMAT, data: blob })}\n`, {
274
- mode: 0o600,
275
- preserveMode: false,
276
- requireCurrentUserOwner: true,
277
- })
278
- }
279
-
280
- clear(): void {
281
- const file = this.filePath()
282
- if (file === null) return
283
- rmSync(file, { force: true })
284
- }
285
-
286
- describe(): string {
287
- const file = this.filePath()
288
- if (file === null) return 'Windows DPAPI current-user (LOCALAPPDATA unset)'
289
- return `Windows DPAPI current-user (${file})`
290
- }
291
-
292
- private protect(plaintext: Buffer): string {
293
- const result = this.invoke('protect', plaintext)
294
- if (result.status !== 0) {
295
- throw new Error('Windows credential protection failed')
296
- }
297
- const encoded = result.stdout.toString('utf8').trim()
298
- if (!isBase64(encoded)) {
299
- throw new Error('Windows credential protection failed')
300
- }
301
- return encoded
302
- }
303
-
304
- private unprotect(blob: string): string | null {
305
- const result = this.invoke('unprotect', Buffer.from(blob, 'utf8'))
306
- if (result.status !== 0) return null
307
- const plain = result.stdout.toString('utf8')
308
- return plain === '' ? null : plain
309
- }
310
-
311
- private invoke(operation: 'protect' | 'unprotect', input: Buffer): RunCommandResult {
312
- const encoded = operation === 'protect' ? PROTECT_ENCODED : UNPROTECT_ENCODED
313
- const invocation = powerShellInvocation(encoded, this.env)
314
- return this.run.run({
315
- command: invocation.command,
316
- args: invocation.args,
317
- input,
318
- timeoutMs: DPAPI_TIMEOUT_MS,
319
- })
320
- }
321
- }
322
-
323
- function serializeCredential(credential: MachineCredential): Record<string, string> {
324
- return { format: CREDENTIAL_FORMAT, ...credential }
325
- }
326
-
327
- function parseCredentialJson(raw: string): MachineCredential | null {
328
- try {
329
- return parseCredential(JSON.parse(raw) as unknown)
330
- } catch {
331
- return null
332
- }
333
- }
334
-
335
- function parseCredential(raw: unknown): MachineCredential | null {
336
- if (raw === null || typeof raw !== 'object') return null
337
- const value = raw as Record<string, unknown>
338
- const format = value['format']
339
- // Unversioned credentials are the pre-epoch v1 representation. A future
340
- // format is left untouched on disk/keychain and treated as unavailable;
341
- // interpreting it as v1 could silently change authentication meaning.
342
- if (format !== undefined && format !== CREDENTIAL_FORMAT) return null
343
- const machineId = value['machineId']
344
- const secret = value['secret']
345
- const baseUrl = value['baseUrl']
346
- const machineName = value['machineName']
347
- if (
348
- typeof machineId !== 'string' ||
349
- machineId === '' ||
350
- typeof secret !== 'string' ||
351
- secret === '' ||
352
- typeof baseUrl !== 'string' ||
353
- baseUrl === '' ||
354
- typeof machineName !== 'string' ||
355
- machineName === ''
356
- ) {
357
- return null
358
- }
359
- return { machineId, secret, baseUrl, machineName }
360
- }
361
-
362
- function parseDpapiEnvelope(text: string): string | null {
363
- let parsed: unknown
364
- try {
365
- parsed = JSON.parse(text)
366
- } catch {
367
- return null
368
- }
369
- if (parsed === null || typeof parsed !== 'object') return null
370
- const value = parsed as Record<string, unknown>
371
- if (value['format'] !== DPAPI_FORMAT) return null
372
- const data = value['data']
373
- if (typeof data !== 'string' || !isBase64(data)) return null
374
- return data
375
- }
376
-
377
- function isBase64(value: string): boolean {
378
- return value.length > 0 && value.length % 4 === 0 && /^[A-Za-z0-9+/]+={0,2}$/.test(value)
379
- }
380
-
381
- function keychainProbeSucceeds(): boolean {
382
- try {
383
- execFileSync('security', ['help'], { stdio: 'ignore' })
384
- return true
385
- } catch {
386
- return false
387
- }
388
- }
389
-
390
- export function defaultCredentialStore(
391
- env: NodeJS.ProcessEnv = process.env,
392
- options: CredentialStoreOptions = {},
393
- ): CredentialStore {
394
- const platform = options.platform ?? process.platform
395
- if (env['NOTIFAI_CREDENTIALS'] === 'file') return new FileStore(env, { platform })
396
- if (platform === 'win32') return new WindowsDpapiStore(env, options)
397
- const keychain =
398
- options.keychainAvailable !== undefined
399
- ? options.keychainAvailable
400
- : platform === 'darwin' && keychainProbeSucceeds()
401
- if (keychain) return new KeychainStore(options)
402
- return new FileStore(env, { platform })
403
- }
@@ -1,64 +0,0 @@
1
- import { existsSync, readFileSync, writeFileSync } from 'node:fs'
2
- import { describe, expect, it } from 'vitest'
3
- import { withFileLock, type FileLockObservation } from './file-lock.js'
4
-
5
- const worker = process.env['NOTIFAI_FILE_LOCK_WORKER']
6
- const sleep = new Int32Array(new SharedArrayBuffer(4))
7
-
8
- function waitFor(file: string): void {
9
- const deadline = Date.now() + 15_000
10
- while (!existsSync(file)) {
11
- if (Date.now() >= deadline) throw new Error(`timed out waiting for ${file}`)
12
- Atomics.wait(sleep, 0, 0, 2)
13
- }
14
- }
15
-
16
- if (worker === undefined) {
17
- describe('file lock process worker', () => {
18
- it('stays dormant outside the cross-process regressions', () => {
19
- expect(worker).toBeUndefined()
20
- })
21
- })
22
- } else {
23
- describe('file lock process worker', () => {
24
- it('runs one instrumented lock contender', () => {
25
- const pauseAt = process.env['NOTIFAI_FILE_LOCK_PAUSE_AT']
26
- const pausePath = process.env['NOTIFAI_FILE_LOCK_PAUSED']!
27
- const continuePath = process.env['NOTIFAI_FILE_LOCK_CONTINUE']!
28
- const enteredPath = process.env['NOTIFAI_FILE_LOCK_ENTERED']!
29
- const releasePath = process.env['NOTIFAI_FILE_LOCK_RELEASE']!
30
- const waitPath = process.env['NOTIFAI_FILE_LOCK_WAITS']!
31
- let waits = 0
32
-
33
- withFileLock(
34
- process.env['NOTIFAI_FILE_LOCK_PATH']!,
35
- () => {
36
- writeFileSync(enteredPath, worker)
37
- if (process.env['NOTIFAI_FILE_LOCK_ACTION'] === 'crash') {
38
- process.kill(process.pid, 'SIGKILL')
39
- throw new Error('SIGKILL did not terminate the worker')
40
- }
41
- waitFor(releasePath)
42
- },
43
- {
44
- waitMs: 15_000,
45
- observe(observation: FileLockObservation) {
46
- if (
47
- observation.phase === 'choosing-published' ||
48
- observation.phase === 'stale-entry'
49
- ) {
50
- writeFileSync(pausePath, observation.entry)
51
- if (pauseAt === observation.phase) waitFor(continuePath)
52
- }
53
- if (observation.phase === 'waiting') {
54
- waits += 1
55
- writeFileSync(waitPath, String(waits))
56
- }
57
- },
58
- },
59
- )
60
-
61
- expect(readFileSync(enteredPath, 'utf8')).toBe(worker)
62
- })
63
- })
64
- }