Files
89bf02f5f7 feat(fetch): x402 Payment Required handling + first unified-tool tests (#5)
* feat(fetch): x402 Payment Required handling + first unified-tool tests

Adds HTTP 402 (x402) parsing to the unified fetch tool so AI agents can
surface payment quotes from x402-gated endpoints and replay with X-PAYMENT.

- request / fetch / head detect status === 402 and return a structured
  payment_required envelope (parsed accepts[], x402-version, www-authenticate)
- new optional 'payment' input: pre-encoded base64 string sent verbatim, or
  a JSON object that is base64-encoded into X-PAYMENT before retry
- adds the first Jest test file for the HIP-0300 unified surface
  (test/tools/unified/fetch.test.ts) covering 200, 402, payment object, and
  payment string paths against an in-process http.createServer fixture

Also fixes test/setup.ts so the existing test suite actually runs:
- imports 'jest' from @jest/globals (jest is not a global under ESM)
- ESM-safe __dirname via fileURLToPath(import.meta.url)
Net effect: 169 previously-unrunnable tests can now start; 7/7 new fetch
tests pass; remaining failures are pre-existing test bugs unrelated to this
PR (CommonJS require() in ESM mode, missing mocks, etc.).

No new dependencies, no breaking changes to fetch's existing semantics.

* test: anchor setup.ts on process.cwd() to fix Node 18/20 ESM CI

`test/setup.ts` used `fileURLToPath(import.meta.url)` to derive its own
directory, but `ts-jest`'s handling of `import.meta` inside
`setupFilesAfterEnv` is fragile across Node versions — every test suite
failed on Node 18.x and 20.x with TS1343 ("\'import.meta\' meta-property
is only allowed when \'--module\' is es2020/...\"), even though the
root tsconfig and ts-jest's transform tsconfig both set
`module: ESNext`.

Tests always run from the project root, so `path.join(process.cwd(),
'test', ...)` is a stable anchor that doesn't depend on how the setup
file is loaded.

Local: 15/16 suites pass after the fix (2 unrelated shell.test.ts timing
flakes remain).

* fix(ci): suppress TS2307 on optional jsautogui dynamic import

The Node 20 CI job was failing two test suites — test/config/
configuration.test.ts and test/integration/mcp-server.test.ts — with:

    src/autogui/adapters/jsautogui.ts:37:37 - error TS2307: Cannot find
    module 'jsautogui' or its corresponding type declarations.

while the actual jest run itself reported 326/326 tests passing in the
suites it could load. Root cause: jsautogui is in optionalDependencies
and ships native bindings that npm may skip on some platforms. When
the package (and its bundled .d.ts) isn't present, ts-jest's transform
pass fails the whole suite at type-check time even though the runtime
code path is already guarded by try/catch + AutoGUINotAvailableError.

Adds a localised `// @ts-ignore` on the single dynamic-import line,
with a comment explaining why. The runtime behaviour is unchanged —
if jsautogui isn't installed, the catch arm still throws
AutoGUINotAvailableError as before. Confirmed locally that the
previously-failing test/config/configuration.test.ts now passes both
with jsautogui present and with node_modules/jsautogui removed
(simulating the Ubuntu CI install).

No code logic changed; package.json and package-lock.json untouched.

---------

Co-authored-by: Abhishek Krishna <[email protected]>
2026-05-19 11:33:09 -07:00

100 lines
3.0 KiB
TypeScript

/**
* Jest test setup file
*/
import * as fs from 'fs/promises';
import * as path from 'path';
import { beforeAll, afterAll, beforeEach, afterEach, jest } from '@jest/globals';
// Anchor on the project root rather than this file's directory so we don't
// need `import.meta.url` here — ts-jest's handling of `import.meta` in
// `setupFilesAfterEnv` differs across Node versions and breaks Node 18/20 CI.
// Tests always run from the project root, so `process.cwd()` is stable.
const TEST_ROOT = path.join(process.cwd(), 'test');
// Global test timeout
jest.setTimeout(10000);
// Test fixtures directory
export const TEST_FIXTURES_DIR = path.join(TEST_ROOT, 'fixtures');
// Each suite gets a unique temp directory to prevent parallel suite interference.
// setupFilesAfterEnv creates a fresh module instance per suite, so this value is
// unique per suite even though it's module-scoped.
const suiteId = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
export const TEST_TEMP_DIR = path.join(TEST_ROOT, 'temp', suiteId);
// Create test directories
beforeAll(async () => {
try {
await fs.mkdir(TEST_FIXTURES_DIR, { recursive: true });
await fs.mkdir(TEST_TEMP_DIR, { recursive: true });
} catch (error) {
// Directories might already exist
}
});
// Clean up this suite's unique temp subdirectory
afterAll(async () => {
try {
await fs.rm(TEST_TEMP_DIR, { recursive: true, force: true });
} catch (error) {
// Directory might not exist
}
});
// Mock console methods to reduce test noise
const originalConsoleError = console.error;
const originalConsoleWarn = console.warn;
beforeEach(() => {
// Mock console.error and console.warn unless we're specifically testing them
console.error = jest.fn();
console.warn = jest.fn();
});
afterEach(() => {
// Restore console methods
console.error = originalConsoleError;
console.warn = originalConsoleWarn;
// Clear all mocks
jest.clearAllMocks();
});
// Helper to create test files
export async function createTestFile(relativePath: string, content: string): Promise<string> {
const fullPath = path.join(TEST_TEMP_DIR, relativePath);
const dir = path.dirname(fullPath);
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(fullPath, content, 'utf8');
return fullPath;
}
// Helper to read test file
export async function readTestFile(relativePath: string): Promise<string> {
const fullPath = path.join(TEST_TEMP_DIR, relativePath);
return fs.readFile(fullPath, 'utf8');
}
// Helper to check if test file exists
export async function testFileExists(relativePath: string): Promise<boolean> {
try {
const fullPath = path.join(TEST_TEMP_DIR, relativePath);
await fs.access(fullPath);
return true;
} catch {
return false;
}
}
// Helper to cleanup test files
export async function cleanupTestFile(relativePath: string): Promise<void> {
try {
const fullPath = path.join(TEST_TEMP_DIR, relativePath);
await fs.rm(fullPath, { force: true });
} catch {
// File might not exist
}
}