feat: 添加 napi 包测试覆盖与 stub 改进

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
unraid
2026-04-22 22:38:10 +08:00
parent fb41513b32
commit eec961352b
4 changed files with 215 additions and 9 deletions

View File

@@ -0,0 +1,112 @@
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
let ffiShouldThrow = false
let nativeFlags = 0
let dlopenCalls = 0
mock.module('bun:ffi', () => ({
FFIType: {
i32: 0,
u64: 0,
},
dlopen: () => {
dlopenCalls++
if (ffiShouldThrow) {
throw new Error('ffi load failed')
}
return {
symbols: {
CGEventSourceFlagsState: () => nativeFlags,
},
}
},
}))
const originalPlatform = process.platform
async function loadModule() {
return import(`../index.ts?case=${Math.random()}`)
}
beforeEach(() => {
ffiShouldThrow = false
nativeFlags = 0
dlopenCalls = 0
Object.defineProperty(process, 'platform', {
value: originalPlatform,
configurable: true,
})
})
afterEach(() => {
Object.defineProperty(process, 'platform', {
value: originalPlatform,
configurable: true,
})
})
describe('modifiers-napi', () => {
test('returns false for non-darwin platforms', async () => {
Object.defineProperty(process, 'platform', {
value: 'win32',
configurable: true,
})
const mod = await loadModule()
await mod.prewarm()
expect(dlopenCalls).toBe(0)
expect(mod.isModifierPressed('shift')).toBe(false)
expect(mod.isModifierPressed('command')).toBe(false)
})
test('prewarm is idempotent on darwin', async () => {
Object.defineProperty(process, 'platform', {
value: 'darwin',
configurable: true,
})
const mod = await loadModule()
await mod.prewarm()
await mod.prewarm()
expect(dlopenCalls).toBe(1)
})
test('returns false when ffi loading fails on darwin', async () => {
Object.defineProperty(process, 'platform', {
value: 'darwin',
configurable: true,
})
ffiShouldThrow = true
const mod = await loadModule()
await mod.prewarm()
expect(mod.isModifierPressed('shift')).toBe(false)
})
test('returns false for unknown modifier names on darwin', async () => {
Object.defineProperty(process, 'platform', {
value: 'darwin',
configurable: true,
})
nativeFlags = 0x20000
const mod = await loadModule()
await mod.prewarm()
expect(mod.isModifierPressed('unknown')).toBe(false)
})
test('uses native flag bits for known modifiers on darwin', async () => {
Object.defineProperty(process, 'platform', {
value: 'darwin',
configurable: true,
})
nativeFlags = 0x20000 | 0x40000
const mod = await loadModule()
await mod.prewarm()
expect(mod.isModifierPressed('shift')).toBe(true)
expect(mod.isModifierPressed('control')).toBe(true)
expect(mod.isModifierPressed('option')).toBe(false)
})
})

View File

@@ -14,14 +14,16 @@ const modifierFlags: Record<string, number> = {
const kCGEventSourceStateCombinedSessionState = 0;
let cgEventSourceFlagsState: ((stateID: number) => number) | null = null;
let ffiLoadAttempted = false;
function loadFFI(): void {
if (cgEventSourceFlagsState !== null || process.platform !== "darwin") {
async function loadFFI(): Promise<void> {
if (ffiLoadAttempted || process.platform !== "darwin") {
return;
}
ffiLoadAttempted = true;
try {
const ffi = require("bun:ffi") as typeof import("bun:ffi");
const ffi = await import("bun:ffi");
const lib = ffi.dlopen(
`/System/Library/Frameworks/Carbon.framework/Carbon`,
{
@@ -35,13 +37,12 @@ function loadFFI(): void {
return Number(lib.symbols.CGEventSourceFlagsState(stateID));
};
} catch {
// If loading fails, keep the function null so isModifierPressed returns false
cgEventSourceFlagsState = null;
}
}
export function prewarm(): void {
loadFFI();
export async function prewarm(): Promise<void> {
await loadFFI();
}
export function isModifierPressed(modifier: string): boolean {
@@ -49,8 +50,6 @@ export function isModifierPressed(modifier: string): boolean {
return false;
}
loadFFI();
if (cgEventSourceFlagsState === null) {
return false;
}

View File

@@ -0,0 +1,50 @@
import { afterEach, describe, expect, test } from 'bun:test'
import { waitForUrlEvent } from '../index'
const originalEnv = {
CLAUDE_CODE_URL_EVENT: process.env.CLAUDE_CODE_URL_EVENT,
CLAUDE_CODE_DEEP_LINK_URL: process.env.CLAUDE_CODE_DEEP_LINK_URL,
CLAUDE_CODE_URL: process.env.CLAUDE_CODE_URL,
}
const originalArgv = process.argv.slice()
afterEach(() => {
for (const [key, value] of Object.entries(originalEnv)) {
if (value === undefined) {
delete process.env[key]
} else {
process.env[key] = value
}
}
process.argv = originalArgv.slice()
})
describe('waitForUrlEvent', () => {
test('resolves to null without a timeout', async () => {
await expect(waitForUrlEvent()).resolves.toBeNull()
})
test('resolves to null with an explicit timeout', async () => {
await expect(waitForUrlEvent(1)).resolves.toBeNull()
})
test('returns a Claude URL from environment variables', async () => {
process.env.CLAUDE_CODE_URL_EVENT = 'claude-cli://prompt?q=hello'
await expect(waitForUrlEvent()).resolves.toBe(
'claude-cli://prompt?q=hello',
)
})
test('returns a Claude URL from argv', async () => {
process.argv = [...originalArgv, 'claude://prompt?q=hello']
await expect(waitForUrlEvent()).resolves.toBe('claude://prompt?q=hello')
})
test('rejects URLs exceeding the maximum length', async () => {
process.env.CLAUDE_CODE_URL_EVENT = `claude-cli://${'x'.repeat(2048)}`
await expect(waitForUrlEvent()).resolves.toBeNull()
})
})

View File

@@ -1,3 +1,48 @@
const MAX_URL_LENGTH = 2048
/**
* Check for a pending URL event from environment variables or CLI arguments.
*
* This is a synchronous snapshot check, not an event listener. The optional
* timeout parameter is retained for API compatibility but has no practical
* effect since process.env and process.argv do not change at runtime.
* Callers that need to wait for an OS-level deep link activation should use
* an IPC channel or platform-specific event listener instead.
*/
export async function waitForUrlEvent(timeoutMs?: number): Promise<string | null> {
return null
return findUrlEvent()
}
/**
* Checks three env var sources (set by the OS URL scheme handler or installer)
* and then CLI arguments for a claude:// deep link URL.
*
* Priority order:
* 1. CLAUDE_CODE_URL_EVENT — set by the OS URL scheme handler on activation
* 2. CLAUDE_CODE_DEEP_LINK_URL — set by the desktop app launcher
* 3. CLAUDE_CODE_URL — legacy / manual override
* 4. CLI arguments — e.g. `claude claude://...`
*/
function findUrlEvent(): string | null {
for (const key of [
'CLAUDE_CODE_URL_EVENT',
'CLAUDE_CODE_DEEP_LINK_URL',
'CLAUDE_CODE_URL',
]) {
const value = process.env[key]
if (isClaudeUrl(value)) {
return value
}
}
const arg = process.argv.find(isClaudeUrl)
return arg ?? null
}
function isClaudeUrl(value: unknown): value is string {
return (
typeof value === 'string' &&
value.length <= MAX_URL_LENGTH &&
(value.startsWith('claude-cli://') || value.startsWith('claude://'))
)
}