qwen-code/packages/core/src/hooks/combinedAbortSignal.test.ts
DennisYu07 b5115e731e
feat(hooks): Add HTTP Hook, Function Hook and Async Hook support (#2827)
* add http/async/function type

* fix url error

* resolve comment

* align cc non blocking error

* fix hookRunner for async

* fix(hooks): update hook type validation to support http and function types

- Change validated hook types from ['command', 'plugin'] to ['command', 'http', 'function']
- Add validation for HTTP hooks requiring url field
- Add validation for function hooks requiring callback field
- Add comprehensive test coverage for all hook type validations

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(hooks): align SSRF protection with Claude Code behavior

- Allow 127.0.0.0/8 (loopback) for local dev hooks
- Allow localhost hostname for local dev hooks
- Allow ::1 (IPv6 loopback) for local dev hooks
- Add 100.64.0.0/10 (CGNAT) to blocked ranges (RFC 6598)
- Update tests to match Claude Code's ssrfGuard.ts behavior

This fixes HTTP hooks failing to connect to local dev servers.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* refactor(hooks): align HTTP hook security with Claude Code behavior

- Add CRLF/NUL sanitization for env var interpolation (header injection)
- Implement combined abort signal (external signal + timeout)
- Upgrade SSRF protection to DNS-level with ssrfGuard
  - Allow loopback (127.0.0.0/8, ::1) for local dev hooks
  - Block CGNAT (100.64.0.0/10) and IPv6 private ranges
- Increase default HTTP hook timeout to 10 minutes
- Fix VS Code hooks schema to support http type
  - Add url, headers, allowedEnvVars, async, once, statusMessage, shell fields
  - Note: "function" type is SDK-only (callback cannot be serialized to JSON)

* feat(hooks): enhance Function Hook with messages, skillRoot, shell, and matcher support

- Add MessagesProvider for automatic conversation history passing to function hooks
- Add FunctionHookContext with messages, toolUseID, and signal
- Add skillRoot support for skill-scoped session hooks
- Add shell parameter support for command hooks (bash/powershell)
- Add regex matcher support for hook pattern matching
- Add statusMessage to CommandHookConfig
- Change default function hook timeout from 60s to 5s
- Add comprehensive unit tests for all new features

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* add session hook for skill

* fix function hook parsing

* refactor ui for http hook/async hook/function hook

* update doc and add integration test

* change telemetryn type and refactor SSRF

* fix project level bug

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-16 10:10:33 +08:00

91 lines
2.6 KiB
TypeScript

/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import { createCombinedAbortSignal } from './combinedAbortSignal.js';
describe('createCombinedAbortSignal', () => {
it('should return a non-aborted signal by default', () => {
const { signal, cleanup } = createCombinedAbortSignal();
expect(signal.aborted).toBe(false);
cleanup();
});
it('should abort after timeout', async () => {
const { signal, cleanup } = createCombinedAbortSignal(undefined, {
timeoutMs: 50,
});
expect(signal.aborted).toBe(false);
await new Promise((resolve) => setTimeout(resolve, 100));
expect(signal.aborted).toBe(true);
cleanup();
});
it('should abort when external signal is aborted', () => {
const externalController = new AbortController();
const { signal, cleanup } = createCombinedAbortSignal(
externalController.signal,
);
expect(signal.aborted).toBe(false);
externalController.abort();
expect(signal.aborted).toBe(true);
cleanup();
});
it('should abort immediately if external signal is already aborted', () => {
const externalController = new AbortController();
externalController.abort();
const { signal, cleanup } = createCombinedAbortSignal(
externalController.signal,
);
expect(signal.aborted).toBe(true);
cleanup();
});
it('should cleanup timeout timer', async () => {
const { signal, cleanup } = createCombinedAbortSignal(undefined, {
timeoutMs: 50,
});
cleanup();
// Wait longer than timeout - should not abort because timer was cleared
await new Promise((resolve) => setTimeout(resolve, 100));
expect(signal.aborted).toBe(false);
});
it('should work with both external signal and timeout', async () => {
const externalController = new AbortController();
const { signal, cleanup } = createCombinedAbortSignal(
externalController.signal,
{ timeoutMs: 200 },
);
// Abort external signal before timeout
externalController.abort();
expect(signal.aborted).toBe(true);
cleanup();
});
it('should timeout before external signal', async () => {
const externalController = new AbortController();
const { signal, cleanup } = createCombinedAbortSignal(
externalController.signal,
{ timeoutMs: 50 },
);
// Wait for timeout
await new Promise((resolve) => setTimeout(resolve, 100));
expect(signal.aborted).toBe(true);
// External signal is still not aborted
expect(externalController.signal.aborted).toBe(false);
cleanup();
});
});