Skip to content

Commit eec9613

Browse files
unraidclaude
andcommitted
feat: 添加 napi 包测试覆盖与 stub 改进
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent fb41513 commit eec9613

4 files changed

Lines changed: 215 additions & 9 deletions

File tree

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
2+
3+
let ffiShouldThrow = false
4+
let nativeFlags = 0
5+
let dlopenCalls = 0
6+
7+
mock.module('bun:ffi', () => ({
8+
FFIType: {
9+
i32: 0,
10+
u64: 0,
11+
},
12+
dlopen: () => {
13+
dlopenCalls++
14+
if (ffiShouldThrow) {
15+
throw new Error('ffi load failed')
16+
}
17+
return {
18+
symbols: {
19+
CGEventSourceFlagsState: () => nativeFlags,
20+
},
21+
}
22+
},
23+
}))
24+
25+
const originalPlatform = process.platform
26+
27+
async function loadModule() {
28+
return import(`../index.ts?case=${Math.random()}`)
29+
}
30+
31+
beforeEach(() => {
32+
ffiShouldThrow = false
33+
nativeFlags = 0
34+
dlopenCalls = 0
35+
Object.defineProperty(process, 'platform', {
36+
value: originalPlatform,
37+
configurable: true,
38+
})
39+
})
40+
41+
afterEach(() => {
42+
Object.defineProperty(process, 'platform', {
43+
value: originalPlatform,
44+
configurable: true,
45+
})
46+
})
47+
48+
describe('modifiers-napi', () => {
49+
test('returns false for non-darwin platforms', async () => {
50+
Object.defineProperty(process, 'platform', {
51+
value: 'win32',
52+
configurable: true,
53+
})
54+
const mod = await loadModule()
55+
56+
await mod.prewarm()
57+
expect(dlopenCalls).toBe(0)
58+
expect(mod.isModifierPressed('shift')).toBe(false)
59+
expect(mod.isModifierPressed('command')).toBe(false)
60+
})
61+
62+
test('prewarm is idempotent on darwin', async () => {
63+
Object.defineProperty(process, 'platform', {
64+
value: 'darwin',
65+
configurable: true,
66+
})
67+
const mod = await loadModule()
68+
69+
await mod.prewarm()
70+
await mod.prewarm()
71+
72+
expect(dlopenCalls).toBe(1)
73+
})
74+
75+
test('returns false when ffi loading fails on darwin', async () => {
76+
Object.defineProperty(process, 'platform', {
77+
value: 'darwin',
78+
configurable: true,
79+
})
80+
ffiShouldThrow = true
81+
const mod = await loadModule()
82+
83+
await mod.prewarm()
84+
expect(mod.isModifierPressed('shift')).toBe(false)
85+
})
86+
87+
test('returns false for unknown modifier names on darwin', async () => {
88+
Object.defineProperty(process, 'platform', {
89+
value: 'darwin',
90+
configurable: true,
91+
})
92+
nativeFlags = 0x20000
93+
const mod = await loadModule()
94+
95+
await mod.prewarm()
96+
expect(mod.isModifierPressed('unknown')).toBe(false)
97+
})
98+
99+
test('uses native flag bits for known modifiers on darwin', async () => {
100+
Object.defineProperty(process, 'platform', {
101+
value: 'darwin',
102+
configurable: true,
103+
})
104+
nativeFlags = 0x20000 | 0x40000
105+
const mod = await loadModule()
106+
107+
await mod.prewarm()
108+
expect(mod.isModifierPressed('shift')).toBe(true)
109+
expect(mod.isModifierPressed('control')).toBe(true)
110+
expect(mod.isModifierPressed('option')).toBe(false)
111+
})
112+
})

packages/modifiers-napi/src/index.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,16 @@ const modifierFlags: Record<string, number> = {
1414
const kCGEventSourceStateCombinedSessionState = 0;
1515

1616
let cgEventSourceFlagsState: ((stateID: number) => number) | null = null;
17+
let ffiLoadAttempted = false;
1718

18-
function loadFFI(): void {
19-
if (cgEventSourceFlagsState !== null || process.platform !== "darwin") {
19+
async function loadFFI(): Promise<void> {
20+
if (ffiLoadAttempted || process.platform !== "darwin") {
2021
return;
2122
}
23+
ffiLoadAttempted = true;
2224

2325
try {
24-
const ffi = require("bun:ffi") as typeof import("bun:ffi");
26+
const ffi = await import("bun:ffi");
2527
const lib = ffi.dlopen(
2628
`/System/Library/Frameworks/Carbon.framework/Carbon`,
2729
{
@@ -35,22 +37,19 @@ function loadFFI(): void {
3537
return Number(lib.symbols.CGEventSourceFlagsState(stateID));
3638
};
3739
} catch {
38-
// If loading fails, keep the function null so isModifierPressed returns false
3940
cgEventSourceFlagsState = null;
4041
}
4142
}
4243

43-
export function prewarm(): void {
44-
loadFFI();
44+
export async function prewarm(): Promise<void> {
45+
await loadFFI();
4546
}
4647

4748
export function isModifierPressed(modifier: string): boolean {
4849
if (process.platform !== "darwin") {
4950
return false;
5051
}
5152

52-
loadFFI();
53-
5453
if (cgEventSourceFlagsState === null) {
5554
return false;
5655
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { afterEach, describe, expect, test } from 'bun:test'
2+
import { waitForUrlEvent } from '../index'
3+
4+
const originalEnv = {
5+
CLAUDE_CODE_URL_EVENT: process.env.CLAUDE_CODE_URL_EVENT,
6+
CLAUDE_CODE_DEEP_LINK_URL: process.env.CLAUDE_CODE_DEEP_LINK_URL,
7+
CLAUDE_CODE_URL: process.env.CLAUDE_CODE_URL,
8+
}
9+
const originalArgv = process.argv.slice()
10+
11+
afterEach(() => {
12+
for (const [key, value] of Object.entries(originalEnv)) {
13+
if (value === undefined) {
14+
delete process.env[key]
15+
} else {
16+
process.env[key] = value
17+
}
18+
}
19+
process.argv = originalArgv.slice()
20+
})
21+
22+
describe('waitForUrlEvent', () => {
23+
test('resolves to null without a timeout', async () => {
24+
await expect(waitForUrlEvent()).resolves.toBeNull()
25+
})
26+
27+
test('resolves to null with an explicit timeout', async () => {
28+
await expect(waitForUrlEvent(1)).resolves.toBeNull()
29+
})
30+
31+
test('returns a Claude URL from environment variables', async () => {
32+
process.env.CLAUDE_CODE_URL_EVENT = 'claude-cli://prompt?q=hello'
33+
34+
await expect(waitForUrlEvent()).resolves.toBe(
35+
'claude-cli://prompt?q=hello',
36+
)
37+
})
38+
39+
test('returns a Claude URL from argv', async () => {
40+
process.argv = [...originalArgv, 'claude://prompt?q=hello']
41+
42+
await expect(waitForUrlEvent()).resolves.toBe('claude://prompt?q=hello')
43+
})
44+
45+
test('rejects URLs exceeding the maximum length', async () => {
46+
process.env.CLAUDE_CODE_URL_EVENT = `claude-cli://${'x'.repeat(2048)}`
47+
48+
await expect(waitForUrlEvent()).resolves.toBeNull()
49+
})
50+
})
Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,48 @@
1+
const MAX_URL_LENGTH = 2048
2+
3+
/**
4+
* Check for a pending URL event from environment variables or CLI arguments.
5+
*
6+
* This is a synchronous snapshot check, not an event listener. The optional
7+
* timeout parameter is retained for API compatibility but has no practical
8+
* effect since process.env and process.argv do not change at runtime.
9+
* Callers that need to wait for an OS-level deep link activation should use
10+
* an IPC channel or platform-specific event listener instead.
11+
*/
112
export async function waitForUrlEvent(timeoutMs?: number): Promise<string | null> {
2-
return null
13+
return findUrlEvent()
14+
}
15+
16+
/**
17+
* Checks three env var sources (set by the OS URL scheme handler or installer)
18+
* and then CLI arguments for a claude:// deep link URL.
19+
*
20+
* Priority order:
21+
* 1. CLAUDE_CODE_URL_EVENT — set by the OS URL scheme handler on activation
22+
* 2. CLAUDE_CODE_DEEP_LINK_URL — set by the desktop app launcher
23+
* 3. CLAUDE_CODE_URL — legacy / manual override
24+
* 4. CLI arguments — e.g. `claude claude://...`
25+
*/
26+
function findUrlEvent(): string | null {
27+
for (const key of [
28+
'CLAUDE_CODE_URL_EVENT',
29+
'CLAUDE_CODE_DEEP_LINK_URL',
30+
'CLAUDE_CODE_URL',
31+
]) {
32+
const value = process.env[key]
33+
if (isClaudeUrl(value)) {
34+
return value
35+
}
36+
}
37+
38+
const arg = process.argv.find(isClaudeUrl)
39+
return arg ?? null
40+
}
41+
42+
function isClaudeUrl(value: unknown): value is string {
43+
return (
44+
typeof value === 'string' &&
45+
value.length <= MAX_URL_LENGTH &&
46+
(value.startsWith('claude-cli://') || value.startsWith('claude://'))
47+
)
348
}

0 commit comments

Comments
 (0)