diff --git a/.cursor/rules/tessl__tessl__cli-setup__query_library_docs.mdc b/.cursor/rules/tessl__tessl__cli-setup__query_library_docs.mdc new file mode 100644 index 000000000..d842dd1a3 --- /dev/null +++ b/.cursor/rules/tessl__tessl__cli-setup__query_library_docs.mdc @@ -0,0 +1,21 @@ +--- +alwaysApply: true +--- + +You write composable, maintainable code by leveraging existing abstractions from the codebase as well as those available as internal or external library dependencies. + +# Gathering context + +Use the MCP tool `query_library_docs` to gather documentation on internal and external libraries / projects / packages / abstractions / dependencies. This will return targeted summaries from documentation on relevant libraries and tools. + +Always call this tool **BEFORE** you start implementing code, to double-check you are up-to-date with available abstractions. + +It's especially important to use the tool when working with niche or internal libraries. + +Give the tool information about what the specific subtask is that you are trying to achieve and what information you need to achieve it. The tool works best when the given prompt is focused and specific. + +Use the tool as you would use a Research Agent. The tool can be called in parallel. + +--- + +**IMPORTANT**: DO NOT use other search tools, such as `grep`, `sem_search`, `semSearch`, or `codebase_search` for gathering information about packages, abstrations, dependencies and how to use internal or external libraries. **ONLY** use the `query_library_docs` MCP tool for these purposes! diff --git a/.tessl/RULES.md b/.tessl/RULES.md new file mode 100644 index 000000000..ef14bd916 --- /dev/null +++ b/.tessl/RULES.md @@ -0,0 +1,7 @@ +# Agent Rules + +This file is updated when running `tessl install`. If a linked file is missing, make sure to run the command to download any missing tiles from the registry. + +## tessl/cli-setup — query_library_docs + +@tiles/tessl/cli-setup/steering/query_library_docs.md [query_library_docs](tiles/tessl/cli-setup/steering/query_library_docs.md) \ No newline at end of file diff --git a/.tessl/tiles/tessl/cli-setup/steering/query_library_docs.md b/.tessl/tiles/tessl/cli-setup/steering/query_library_docs.md new file mode 100644 index 000000000..61043c1a3 --- /dev/null +++ b/.tessl/tiles/tessl/cli-setup/steering/query_library_docs.md @@ -0,0 +1,17 @@ +You write composable, maintainable code by leveraging existing abstractions from the codebase as well as those available as internal or external library dependencies. + +# Gathering context + +Use the MCP tool `query_library_docs` to gather documentation on internal and external libraries / projects / packages / abstractions / dependencies. This will return targeted summaries from documentation on relevant libraries and tools. + +Always call this tool **BEFORE** you start implementing code, to double-check you are up-to-date with available abstractions. + +It's especially important to use the tool when working with niche or internal libraries. + +Give the tool information about what the specific subtask is that you are trying to achieve and what information you need to achieve it. The tool works best when the given prompt is focused and specific. + +Use the tool as you would use a Research Agent. The tool can be called in parallel. + +--- + +**IMPORTANT**: DO NOT use other search tools, such as `grep`, `sem_search`, `semSearch`, or `codebase_search` for gathering information about packages, abstrations, dependencies and how to use internal or external libraries. **ONLY** use the `query_library_docs` MCP tool for these purposes! diff --git a/.tessl/tiles/tessl/cli-setup/tile.json b/.tessl/tiles/tessl/cli-setup/tile.json new file mode 100644 index 000000000..74d76bc6b --- /dev/null +++ b/.tessl/tiles/tessl/cli-setup/tile.json @@ -0,0 +1,11 @@ +{ + "name": "tessl/cli-setup", + "private": false, + "version": "0.60.0", + "summary": "Tessl CLI MCP tool usage guidelines", + "steering": { + "query_library_docs": { + "rules": "steering/query_library_docs.md" + } + } +} \ No newline at end of file diff --git a/.tessl/tiles/tessl/npm-test/docs/core-testing.md b/.tessl/tiles/tessl/npm-test/docs/core-testing.md new file mode 100644 index 000000000..9c19f2831 --- /dev/null +++ b/.tessl/tiles/tessl/npm-test/docs/core-testing.md @@ -0,0 +1,257 @@ +# Core Testing Functions + +Primary test definition functions for creating individual tests and organizing them into suites. These functions support multiple execution patterns, configuration options, and provide the foundation for all test operations. + +## Capabilities + +### Test Function + +The main test function for creating individual test cases. Supports multiple overloads for different usage patterns. + +```javascript { .api } +/** + * Creates and runs an individual test case + * @param name - Test name (optional) + * @param options - Test configuration options (optional) + * @param fn - Test function to execute + */ +function test(name: string, options: TestOptions, fn: TestFn): void; +function test(name: string, fn: TestFn): void; +function test(options: TestOptions, fn: TestFn): void; +function test(fn: TestFn): void; + +type TestFn = (t: TestContext) => any | Promise; +``` + +**Usage Examples:** + +```javascript +import test from "test"; + +// Named test +test("should calculate sum", (t) => { + const result = 2 + 3; + if (result !== 5) throw new Error("Math is broken"); +}); + +// Test with options +test("slow operation", { timeout: 5000 }, async (t) => { + await new Promise(resolve => setTimeout(resolve, 1000)); +}); + +// Skipped test +test("not ready yet", { skip: "Feature not implemented" }, (t) => { + // This won't run +}); + +// Anonymous test +test((t) => { + t.diagnostic("Running anonymous test"); +}); +``` + +### Describe Function + +Groups related tests into suites for better organization and shared setup/teardown. + +```javascript { .api } +/** + * Creates a test suite grouping related tests + * @param name - Suite name (optional) + * @param options - Suite configuration options (optional) + * @param fn - Suite function containing tests and hooks + */ +function describe(name: string, options: TestOptions, fn: SuiteFn): void; +function describe(name: string, fn: SuiteFn): void; +function describe(options: TestOptions, fn: SuiteFn): void; +function describe(fn: SuiteFn): void; + +type SuiteFn = (t: SuiteContext) => void; +``` + +**Usage Examples:** + +```javascript +import { describe, it, beforeEach } from "test"; + +describe("User authentication", () => { + beforeEach(() => { + // Setup for each test in this suite + }); + + it("should login with valid credentials", () => { + // Test implementation + }); + + it("should reject invalid credentials", () => { + // Test implementation + }); +}); + +// Nested suites +describe("API endpoints", () => { + describe("User endpoints", () => { + it("should create user", () => { + // Test implementation + }); + }); +}); +``` + +### It Function + +Creates individual test cases within describe blocks. Functionally identical to the test function but semantically used within suites. + +```javascript { .api } +/** + * Creates an individual test case within a suite + * @param name - Test name (optional) + * @param options - Test configuration options (optional) + * @param fn - Test function to execute + */ +function it(name: string, options: TestOptions, fn: ItFn): void; +function it(name: string, fn: ItFn): void; +function it(options: TestOptions, fn: ItFn): void; +function it(fn: ItFn): void; + +type ItFn = (t: ItContext) => any | Promise; +``` + +**Usage Examples:** + +```javascript +import { describe, it } from "test"; + +describe("Calculator", () => { + it("should add two numbers", (t) => { + const result = add(2, 3); + if (result !== 5) throw new Error("Addition failed"); + }); + + it("handles negative numbers", { timeout: 1000 }, (t) => { + const result = add(-1, 1); + if (result !== 0) throw new Error("Negative addition failed"); + }); + + it("TODO: should handle decimals", { todo: true }, (t) => { + // Not implemented yet + }); +}); +``` + +## Test Options + +```javascript { .api } +interface TestOptions { + /** Number of tests that can run concurrently. Default: 1 */ + concurrency?: boolean | number; + /** Skip test with optional reason. Default: false */ + skip?: boolean | string; + /** Mark test as TODO with optional reason. Default: false */ + todo?: boolean | string; + /** Test timeout in milliseconds. Default: Infinity */ + timeout?: number; + /** AbortSignal for test cancellation */ + signal?: AbortSignal; +} +``` + +## Context Objects + +### TestContext + +Context object passed to test functions providing diagnostic and control capabilities. + +```javascript { .api } +interface TestContext { + /** Create subtests within the current test */ + test(name: string, options: TestOptions, fn: TestFn): Promise; + test(name: string, fn: TestFn): Promise; + test(fn: TestFn): Promise; + /** Write diagnostic information to test output */ + diagnostic(message: string): void; + /** Mark current test as skipped */ + skip(message?: string): void; + /** Mark current test as TODO */ + todo(message?: string): void; + /** AbortSignal for test cancellation */ + signal: AbortSignal; +} +``` + +**Usage Examples:** + +```javascript +test("test with subtests", async (t) => { + t.diagnostic("Starting parent test"); + + await t.test("subtest 1", (st) => { + // First subtest + }); + + await t.test("subtest 2", { timeout: 500 }, (st) => { + // Second subtest with timeout + }); +}); + +test("conditional skip", (t) => { + if (!process.env.API_KEY) { + t.skip("API key not provided"); + return; + } + // Test implementation +}); +``` + +### SuiteContext + +Context object passed to describe functions. + +```javascript { .api } +interface SuiteContext { + /** AbortSignal for suite cancellation */ + signal: AbortSignal; +} +``` + +### ItContext + +Context object passed to it functions. + +```javascript { .api } +interface ItContext { + /** AbortSignal for test cancellation */ + signal: AbortSignal; +} +``` + +## Error Handling + +Tests can fail by: +- Throwing any error or exception +- Returning a rejected Promise +- Timing out (when timeout option is set) +- Being aborted via AbortSignal + +```javascript +test("error handling examples", (t) => { + // Explicit error + throw new Error("Test failed"); + + // Assertion-style + if (result !== expected) { + throw new Error(`Expected ${expected}, got ${result}`); + } +}); + +test("async error handling", async (t) => { + // Rejected promise + await Promise.reject(new Error("Async operation failed")); + + // Async assertion + const result = await fetchData(); + if (!result) { + throw new Error("No data received"); + } +}); +``` \ No newline at end of file diff --git a/.tessl/tiles/tessl/npm-test/docs/hooks.md b/.tessl/tiles/tessl/npm-test/docs/hooks.md new file mode 100644 index 000000000..fe68b573d --- /dev/null +++ b/.tessl/tiles/tessl/npm-test/docs/hooks.md @@ -0,0 +1,336 @@ +# Test Lifecycle Hooks + +Hook functions that run at specific points in the test lifecycle for setup and teardown operations. These hooks provide a clean way to prepare test environments and clean up resources. + +## Capabilities + +### Before Hook + +Runs once before all tests in the current suite. Ideal for expensive setup operations that can be shared across multiple tests. + +```javascript { .api } +/** + * Runs once before all tests in the current suite + * @param fn - Setup function to execute + * @param options - Optional hook configuration + */ +function before(fn: () => void | Promise, options?: { signal?: AbortSignal, timeout?: number }): void; +``` + +**Usage Examples:** + +```javascript +import { describe, it, before } from "test"; + +describe("Database tests", () => { + before(async () => { + // Setup database connection + await connectToDatabase(); + await runMigrations(); + }); + + it("should create user", () => { + // Test implementation + }); + + it("should update user", () => { + // Test implementation + }); +}); + +// Top-level before hook +before(() => { + console.log("Starting all tests"); + process.env.NODE_ENV = "test"; +}); +``` + +### After Hook + +Runs once after all tests in the current suite have completed. Used for cleanup operations that should happen regardless of test success or failure. + +```javascript { .api } +/** + * Runs once after all tests in the current suite + * @param fn - Cleanup function to execute + * @param options - Optional hook configuration + */ +function after(fn: () => void | Promise, options?: { signal?: AbortSignal, timeout?: number }): void; +``` + +**Usage Examples:** + +```javascript +import { describe, it, before, after } from "test"; + +describe("File system tests", () => { + before(() => { + // Create test directory + fs.mkdirSync('./test-files'); + }); + + after(() => { + // Clean up test directory + fs.rmSync('./test-files', { recursive: true }); + }); + + it("should create file", () => { + // Test implementation + }); +}); + +// Top-level after hook +after(async () => { + console.log("All tests completed"); + await cleanup(); +}); +``` + +### BeforeEach Hook + +Runs before each individual test. Perfect for ensuring each test starts with a clean, predictable state. + +```javascript { .api } +/** + * Runs before each individual test + * @param fn - Setup function to execute before each test + * @param options - Optional hook configuration + */ +function beforeEach(fn: () => void | Promise, options?: { signal?: AbortSignal, timeout?: number }): void; +``` + +**Usage Examples:** + +```javascript +import { describe, it, beforeEach } from "test"; + +describe("Calculator tests", () => { + let calculator; + + beforeEach(() => { + // Fresh calculator instance for each test + calculator = new Calculator(); + calculator.reset(); + }); + + it("should add numbers", () => { + const result = calculator.add(2, 3); + if (result !== 5) throw new Error("Addition failed"); + }); + + it("should subtract numbers", () => { + const result = calculator.subtract(5, 3); + if (result !== 2) throw new Error("Subtraction failed"); + }); +}); + +// Async beforeEach +describe("API tests", () => { + beforeEach(async () => { + await resetDatabase(); + await seedTestData(); + }); + + it("should fetch users", async () => { + // Test implementation + }); +}); +``` + +### AfterEach Hook + +Runs after each individual test completes. Used for cleaning up test-specific resources and ensuring tests don't interfere with each other. + +```javascript { .api } +/** + * Runs after each individual test + * @param fn - Cleanup function to execute after each test + * @param options - Optional hook configuration + */ +function afterEach(fn: () => void | Promise, options?: { signal?: AbortSignal, timeout?: number }): void; +``` + +**Usage Examples:** + +```javascript +import { describe, it, beforeEach, afterEach } from "test"; + +describe("Cache tests", () => { + beforeEach(() => { + // Initialize cache + cache.init(); + }); + + afterEach(() => { + // Clear cache after each test + cache.clear(); + }); + + it("should store values", () => { + cache.set("key", "value"); + if (cache.get("key") !== "value") { + throw new Error("Cache store failed"); + } + }); + + it("should handle expiration", async () => { + cache.set("key", "value", { ttl: 10 }); + await new Promise(resolve => setTimeout(resolve, 20)); + if (cache.get("key") !== null) { + throw new Error("Cache expiration failed"); + } + }); +}); + +// Async afterEach +describe("Resource tests", () => { + afterEach(async () => { + await closeConnections(); + await cleanupTempFiles(); + }); + + it("should manage resources", () => { + // Test implementation + }); +}); +``` + +## Hook Execution Order + +Hooks execute in the following order for nested suites: + +1. Outer `before` hooks +2. Inner `before` hooks +3. For each test: + - Outer `beforeEach` hooks + - Inner `beforeEach` hooks + - **Test execution** + - Inner `afterEach` hooks + - Outer `afterEach` hooks +4. Inner `after` hooks +5. Outer `after` hooks + +**Example:** + +```javascript +import { describe, it, before, after, beforeEach, afterEach } from "test"; + +before(() => console.log("1. Global before")); +after(() => console.log("8. Global after")); + +describe("Outer suite", () => { + before(() => console.log("2. Outer before")); + beforeEach(() => console.log("3. Outer beforeEach")); + afterEach(() => console.log("6. Outer afterEach")); + after(() => console.log("7. Outer after")); + + describe("Inner suite", () => { + before(() => console.log("3. Inner before")); + beforeEach(() => console.log("4. Inner beforeEach")); + afterEach(() => console.log("5. Inner afterEach")); + after(() => console.log("6. Inner after")); + + it("test case", () => { + console.log("5. Test execution"); + }); + }); +}); +``` + +## Error Handling in Hooks + +If a hook throws an error or returns a rejected promise: + +- **before/beforeEach errors**: Skip the associated tests +- **after/afterEach errors**: Mark tests as failed but continue cleanup +- All hooks of the same type continue to run even if one fails + +```javascript +describe("Error handling", () => { + before(() => { + throw new Error("Setup failed"); + // This will cause all tests in this suite to be skipped + }); + + afterEach(() => { + // This runs even if the test or beforeEach failed + cleanup(); + }); + + it("this test will be skipped", () => { + // Won't run due to before hook failure + }); +}); +``` + +## Hook Scope + +Hooks only apply to tests within their scope: + +```javascript +// Global hooks - apply to all tests +before(() => { + // Runs before any test +}); + +describe("Suite A", () => { + // Suite-level hooks - only apply to tests in this suite + beforeEach(() => { + // Only runs before tests in Suite A + }); + + it("test 1", () => {}); + it("test 2", () => {}); +}); + +describe("Suite B", () => { + // Different suite-level hooks + beforeEach(() => { + // Only runs before tests in Suite B + }); + + it("test 3", () => {}); +}); +``` + +## Best Practices + +1. **Keep hooks simple**: Focus on setup/cleanup, avoid complex logic +2. **Use async/await**: For asynchronous operations in hooks +3. **Clean up resources**: Always clean up in after/afterEach hooks +4. **Fail fast**: If setup fails, let the hook throw an error +5. **Scope appropriately**: Use the most specific hook scope possible + +```javascript +describe("Best practices example", () => { + let server; + let client; + + before(async () => { + // Expensive setup once per suite + server = await startTestServer(); + }); + + beforeEach(() => { + // Fresh client for each test + client = new ApiClient(server.url); + }); + + afterEach(() => { + // Clean up test-specific resources + client.close(); + }); + + after(async () => { + // Clean up suite-level resources + await server.close(); + }); + + it("should connect", async () => { + await client.connect(); + if (!client.isConnected()) { + throw new Error("Connection failed"); + } + }); +}); +``` \ No newline at end of file diff --git a/.tessl/tiles/tessl/npm-test/docs/index.md b/.tessl/tiles/tessl/npm-test/docs/index.md new file mode 100644 index 000000000..ac2fa885e --- /dev/null +++ b/.tessl/tiles/tessl/npm-test/docs/index.md @@ -0,0 +1,179 @@ +# Test + +The `test` package provides a complete port of Node.js 18's experimental test runner (`node:test`) that works with Node.js 14+. It offers a comprehensive testing framework with minimal dependencies, supporting synchronous functions, Promise-based async functions, and callback-based functions for test execution. + +## Package Information + +- **Package Name**: test +- **Package Type**: npm +- **Language**: JavaScript (with TypeScript definitions) +- **Installation**: `npm install test` + +## Core Imports + +```javascript +import test, { describe, it, before, after, beforeEach, afterEach, run, mock } from "test"; +``` + +For CommonJS: + +```javascript +const test = require("test"); +const { describe, it, before, after, beforeEach, afterEach, run, mock } = require("test"); +``` + +## Basic Usage + +```javascript +import test, { describe, it, beforeEach } from "test"; + +// Simple test +test("should add numbers correctly", (t) => { + const result = 2 + 3; + if (result !== 5) { + throw new Error(`Expected 5, got ${result}`); + } +}); + +// Test suite with hooks +describe("Calculator tests", () => { + beforeEach(() => { + console.log("Setting up test"); + }); + + it("should multiply correctly", () => { + const result = 4 * 5; + if (result !== 20) { + throw new Error(`Expected 20, got ${result}`); + } + }); +}); + +// Async test +test("async operation", async (t) => { + const result = await Promise.resolve(42); + if (result !== 42) { + throw new Error(`Expected 42, got ${result}`); + } +}); +``` + +## Architecture + +The test package is built around several key components: + +- **Core Test Functions**: `test`, `describe`, and `it` functions for organizing and running tests +- **Test Context System**: Context objects passed to test functions providing diagnostic and control capabilities +- **Hook System**: Lifecycle hooks (`before`, `after`, `beforeEach`, `afterEach`) for test setup and teardown +- **Test Runner**: Programmatic runner for executing test files with configurable options +- **Mocking System**: Comprehensive function and method mocking with call tracking and implementation control +- **CLI Tools**: Command-line utilities for various testing scenarios with TAP output support + +## Capabilities + +### Core Testing Functions + +Primary test definition functions for creating individual tests and organizing them into suites. Supports multiple execution patterns and configuration options. + +```javascript { .api } +function test(name: string, options: TestOptions, fn: TestFn): void; +function test(name: string, fn: TestFn): void; +function test(options: TestOptions, fn: TestFn): void; +function test(fn: TestFn): void; + +function describe(name: string, options: TestOptions, fn: SuiteFn): void; +function describe(name: string, fn: SuiteFn): void; +function describe(options: TestOptions, fn: SuiteFn): void; +function describe(fn: SuiteFn): void; + +function it(name: string, options: TestOptions, fn: ItFn): void; +function it(name: string, fn: ItFn): void; +function it(options: TestOptions, fn: ItFn): void; +function it(fn: ItFn): void; + +interface TestOptions { + concurrency?: boolean | number; + skip?: boolean | string; + todo?: boolean | string; + timeout?: number; + signal?: AbortSignal; +} +``` + +[Core Testing Functions](./core-testing.md) + +### Test Lifecycle Hooks + +Hook functions that run at specific points in the test lifecycle for setup and teardown operations. + +```javascript { .api } +function before(fn: () => void | Promise, options?: { signal?: AbortSignal, timeout?: number }): void; +function after(fn: () => void | Promise, options?: { signal?: AbortSignal, timeout?: number }): void; +function beforeEach(fn: () => void | Promise, options?: { signal?: AbortSignal, timeout?: number }): void; +function afterEach(fn: () => void | Promise, options?: { signal?: AbortSignal, timeout?: number }): void; +``` + +[Test Lifecycle Hooks](./hooks.md) + +### Test Runner + +Programmatic test runner for executing test files with advanced configuration options including concurrency, timeouts, and custom reporters. + +```javascript { .api } +function run(options?: RunOptions): TestsStream; + +interface RunOptions { + concurrency?: number; + timeout?: number; + signal?: AbortSignal; + files?: string[]; + inspectPort?: number; +} +``` + +[Test Runner](./runner.md) + +### Mocking System + +Comprehensive mocking system for functions and object methods with call tracking, implementation control, and restoration capabilities. + +```javascript { .api } +const mock: MockTracker; + +interface MockTracker { + fn(original?: Function, implementation?: Function, options?: MockOptions): MockFunctionContext; + method(object: object, methodName: string, implementation?: Function, options?: MethodMockOptions): MockFunctionContext; + getter(object: object, methodName: string, implementation?: Function, options?: MockOptions): MockFunctionContext; + setter(object: object, methodName: string, implementation?: Function, options?: MockOptions): MockFunctionContext; + reset(): void; + restoreAll(): void; +} +``` + +[Mocking System](./mocking.md) + +## Types + +```javascript { .api } +type TestFn = (t: TestContext) => any | Promise; +type SuiteFn = (t: SuiteContext) => void; +type ItFn = (t: ItContext) => any | Promise; + +interface TestContext { + test(name: string, options: TestOptions, fn: TestFn): Promise; + test(name: string, fn: TestFn): Promise; + test(fn: TestFn): Promise; + diagnostic(message: string): void; + skip(message?: string): void; + todo(message?: string): void; + signal: AbortSignal; +} + +interface SuiteContext { + signal: AbortSignal; +} + +interface ItContext { + signal: AbortSignal; +} +``` \ No newline at end of file diff --git a/.tessl/tiles/tessl/npm-test/docs/mocking.md b/.tessl/tiles/tessl/npm-test/docs/mocking.md new file mode 100644 index 000000000..4e00944b6 --- /dev/null +++ b/.tessl/tiles/tessl/npm-test/docs/mocking.md @@ -0,0 +1,512 @@ +# Mocking System + +Comprehensive mocking system for functions and object methods with call tracking, implementation control, and restoration capabilities. The mocking system provides fine-grained control over function behavior during testing. + +## Capabilities + +### MockTracker + +The main mocking interface accessed via the `mock` property. Provides methods for creating and managing various types of mocks. + +```javascript { .api } +/** + * Main mocking interface for creating and managing mocks + */ +const mock: MockTracker; + +interface MockTracker { + /** Create a function mock */ + fn(original?: Function, implementation?: Function, options?: MockOptions): MockFunctionContext; + /** Mock an object method */ + method(object: object, methodName: string, implementation?: Function, options?: MethodMockOptions): MockFunctionContext; + /** Mock an object getter */ + getter(object: object, methodName: string, implementation?: Function, options?: MockOptions): MockFunctionContext; + /** Mock an object setter */ + setter(object: object, methodName: string, implementation?: Function, options?: MockOptions): MockFunctionContext; + /** Reset all mock call tracking data */ + reset(): void; + /** Restore all mocked functions to their originals */ + restoreAll(): void; +} +``` + +### Function Mocking + +Create mock functions for testing function behavior and call patterns. + +```javascript { .api } +/** + * Create a function mock + * @param original - Original function to mock (optional) + * @param implementation - Mock implementation (optional) + * @param options - Mock configuration options + * @returns MockFunctionContext for controlling and inspecting the mock + */ +fn(original?: Function, implementation?: Function, options?: MockOptions): MockFunctionContext; + +interface MockOptions { + /** Number of times mock should be active before restoring */ + times?: number; +} +``` + +**Usage Examples:** + +```javascript +import { mock } from "test"; + +// Basic function mock +const mockFn = mock.fn(); +mockFn('hello', 'world'); +console.log(mockFn.callCount()); // 1 + +// Mock with implementation +const add = mock.fn(null, (a, b) => a + b); +console.log(add(2, 3)); // 5 + +// Mock existing function +const originalFetch = fetch; +const mockFetch = mock.fn(originalFetch, async () => ({ + json: async () => ({ success: true }) +})); + +// Temporary mock (auto-restores after 2 calls) +const tempMock = mock.fn(console.log, () => {}, { times: 2 }); +tempMock('call 1'); // mock implementation +tempMock('call 2'); // mock implementation +tempMock('call 3'); // original console.log +``` + +### Method Mocking + +Mock methods on existing objects while preserving the object structure. + +```javascript { .api } +/** + * Mock an object method + * @param object - Target object containing the method + * @param methodName - Name of the method to mock + * @param implementation - Mock implementation (optional) + * @param options - Mock configuration options + * @returns MockFunctionContext for controlling and inspecting the mock + */ +method(object: object, methodName: string, implementation?: Function, options?: MethodMockOptions): MockFunctionContext; + +interface MethodMockOptions extends MockOptions { + /** Mock as getter property */ + getter?: boolean; + /** Mock as setter property */ + setter?: boolean; +} +``` + +**Usage Examples:** + +```javascript +import { mock } from "test"; + +// Mock object method +const user = { + name: 'Alice', + getName() { return this.name; } +}; + +const mockGetName = mock.method(user, 'getName', function() { + return 'Mocked Name'; +}); + +console.log(user.getName()); // 'Mocked Name' +console.log(mockGetName.callCount()); // 1 + +// Mock with original function access +const fs = require('fs'); +const mockReadFile = mock.method(fs, 'readFileSync', (path) => { + if (path === 'test.txt') return 'mocked content'; + // Call original for other files + return mockReadFile.original(path); +}); + +// Mock getter/setter +const config = { + _timeout: 1000, + get timeout() { return this._timeout; }, + set timeout(value) { this._timeout = value; } +}; + +mock.method(config, 'timeout', () => 5000, { getter: true }); +console.log(config.timeout); // 5000 +``` + +### Getter and Setter Mocking + +Specialized methods for mocking property getters and setters. + +```javascript { .api } +/** + * Mock an object getter + * @param object - Target object + * @param methodName - Property name + * @param implementation - Getter implementation + * @param options - Mock options + * @returns MockFunctionContext + */ +getter(object: object, methodName: string, implementation?: Function, options?: MockOptions): MockFunctionContext; + +/** + * Mock an object setter + * @param object - Target object + * @param methodName - Property name + * @param implementation - Setter implementation + * @param options - Mock options + * @returns MockFunctionContext + */ +setter(object: object, methodName: string, implementation?: Function, options?: MockOptions): MockFunctionContext; +``` + +**Usage Examples:** + +```javascript +import { mock } from "test"; + +const obj = { + _value: 42, + get value() { return this._value; }, + set value(v) { this._value = v; } +}; + +// Mock getter +const mockGetter = mock.getter(obj, 'value', () => 999); +console.log(obj.value); // 999 + +// Mock setter +const mockSetter = mock.setter(obj, 'value', function(v) { + console.log(`Setting value to ${v}`); + this._value = v * 2; +}); + +obj.value = 10; // Logs: "Setting value to 10" +console.log(obj._value); // 20 +``` + +## MockFunctionContext + +Context object returned by all mock creation methods, providing control and inspection capabilities. + +```javascript { .api } +interface MockFunctionContext { + /** Array of all function calls (read-only) */ + calls: CallRecord[]; + /** Get the total number of calls */ + callCount(): number; + /** Change the mock implementation */ + mockImplementation(implementation: Function): void; + /** Set implementation for a specific call number */ + mockImplementationOnce(implementation: Function, onCall?: number): void; + /** Restore the original function */ + restore(): void; +} + +interface CallRecord { + arguments: any[]; + result?: any; + error?: Error; + target?: any; + this?: any; +} +``` + +**Usage Examples:** + +```javascript +import { mock } from "test"; + +const mockFn = mock.fn(); + +// Make some calls +mockFn('arg1', 'arg2'); +mockFn(123); +mockFn(); + +// Inspect calls +console.log(mockFn.callCount()); // 3 +console.log(mockFn.calls[0].arguments); // ['arg1', 'arg2'] +console.log(mockFn.calls[1].arguments); // [123] + +// Change implementation +mockFn.mockImplementation((x) => x * 2); +console.log(mockFn(5)); // 10 + +// One-time implementation +mockFn.mockImplementationOnce(() => 'special', 4); +mockFn(); // 'special' (5th call) +mockFn(); // back to x * 2 implementation + +// Restore original +mockFn.restore(); +``` + +### Call Inspection + +Detailed inspection of mock function calls: + +```javascript +import { mock } from "test"; + +const calculator = { + add(a, b) { return a + b; } +}; + +const mockAdd = mock.method(calculator, 'add'); + +// Make calls +calculator.add(2, 3); +calculator.add(10, 5); + +// Inspect calls +const calls = mockAdd.calls; + +calls.forEach((call, index) => { + console.log(`Call ${index + 1}:`); + console.log(` Arguments: ${call.arguments}`); + console.log(` Result: ${call.result}`); + console.log(` This context:`, call.this); +}); + +// Check specific calls +if (mockAdd.callCount() >= 2) { + const secondCall = calls[1]; + if (secondCall.arguments[0] === 10 && secondCall.arguments[1] === 5) { + console.log('Second call was add(10, 5)'); + } +} +``` + +## Global Mock Management + +### Reset All Mocks + +Clear call tracking data for all mocks without restoring implementations: + +```javascript { .api } +/** + * Reset all mock call tracking data + */ +reset(): void; +``` + +**Usage Examples:** + +```javascript +import { mock } from "test"; + +const mockFn1 = mock.fn(); +const mockFn2 = mock.fn(); + +mockFn1('test'); +mockFn2('test'); + +console.log(mockFn1.callCount()); // 1 +console.log(mockFn2.callCount()); // 1 + +mock.reset(); + +console.log(mockFn1.callCount()); // 0 +console.log(mockFn2.callCount()); // 0 +// Functions still mocked, just call history cleared +``` + +### Restore All Mocks + +Restore all mocked functions to their original implementations: + +```javascript { .api } +/** + * Restore all mocked functions to their originals + */ +restoreAll(): void; +``` + +**Usage Examples:** + +```javascript +import { mock } from "test"; + +const obj = { + method1() { return 'original1'; }, + method2() { return 'original2'; } +}; + +mock.method(obj, 'method1', () => 'mocked1'); +mock.method(obj, 'method2', () => 'mocked2'); + +console.log(obj.method1()); // 'mocked1' +console.log(obj.method2()); // 'mocked2' + +mock.restoreAll(); + +console.log(obj.method1()); // 'original1' +console.log(obj.method2()); // 'original2' +``` + +## Advanced Patterns + +### Conditional Mocking + +Mock functions that behave differently based on arguments: + +```javascript +import { mock } from "test"; + +const mockFetch = mock.fn(fetch, async (url, options) => { + if (url.includes('/api/users')) { + return { json: async () => [{ id: 1, name: 'Test User' }] }; + } + if (url.includes('/api/error')) { + throw new Error('Network error'); + } + // Call original fetch for other URLs + return fetch(url, options); +}); +``` + +### Spy Pattern + +Monitor function calls without changing behavior: + +```javascript +import { mock } from "test"; + +const logger = { + log(message) { + console.log(`[LOG] ${message}`); + } +}; + +// Spy on logger.log (keep original behavior) +const logSpy = mock.method(logger, 'log', function(message) { + // Call original implementation + return logSpy.original.call(this, message); +}); + +logger.log('Hello'); // Still logs to console +console.log(logSpy.callCount()); // 1 +console.log(logSpy.calls[0].arguments[0]); // 'Hello' +``` + +### Mock Chaining + +Create complex mock setups: + +```javascript +import { mock } from "test"; + +const api = { + get() { return Promise.resolve({ data: 'real data' }); }, + post() { return Promise.resolve({ success: true }); } +}; + +// Chain multiple mocks +mock.method(api, 'get', () => Promise.resolve({ data: 'mock data' })) +mock.method(api, 'post', () => Promise.resolve({ success: false })); + +// Test with mocked API +test('API interactions', async (t) => { + const getData = await api.get(); + const postResult = await api.post(); + + if (getData.data !== 'mock data') { + throw new Error('GET mock failed'); + } + if (postResult.success !== false) { + throw new Error('POST mock failed'); + } + + // Clean up + mock.restoreAll(); +}); +``` + +### Error Simulation + +Mock functions to simulate error conditions: + +```javascript +import { mock } from "test"; + +const fileSystem = { + readFile(path) { + // Original implementation + return fs.readFileSync(path, 'utf8'); + } +}; + +// Mock to simulate file not found +const mockReadFile = mock.method(fileSystem, 'readFile', (path) => { + if (path === 'nonexistent.txt') { + const error = new Error('ENOENT: no such file or directory'); + error.code = 'ENOENT'; + throw error; + } + return 'file contents'; +}); + +test('error handling', (t) => { + try { + fileSystem.readFile('nonexistent.txt'); + throw new Error('Should have thrown'); + } catch (error) { + if (error.code !== 'ENOENT') { + throw new Error('Wrong error type'); + } + } + + // Verify the error was tracked + if (mockReadFile.calls[0].error.code !== 'ENOENT') { + throw new Error('Error not tracked correctly'); + } +}); +``` + +## Best Practices + +1. **Clean up mocks**: Always restore mocks after tests +2. **Use spies for monitoring**: Keep original behavior when you just need to track calls +3. **Mock at the right level**: Mock dependencies, not implementation details +4. **Test mock behavior**: Verify mocks are called with expected arguments +5. **Avoid over-mocking**: Only mock what's necessary for the test + +```javascript +import { mock, afterEach } from "test"; + +// Clean up after each test +afterEach(() => { + mock.restoreAll(); +}); + +test('user service', (t) => { + const database = { + findUser: (id) => ({ id, name: 'Real User' }) + }; + + // Mock external dependency + const mockFindUser = mock.method(database, 'findUser', + (id) => ({ id, name: 'Test User' }) + ); + + const userService = new UserService(database); + const user = userService.getUser(123); + + // Test the result + if (user.name !== 'Test User') { + throw new Error('Service failed'); + } + + // Verify mock was called correctly + if (mockFindUser.callCount() !== 1) { + throw new Error('Database not called'); + } + if (mockFindUser.calls[0].arguments[0] !== 123) { + throw new Error('Wrong user ID passed'); + } +}); +``` \ No newline at end of file diff --git a/.tessl/tiles/tessl/npm-test/docs/runner.md b/.tessl/tiles/tessl/npm-test/docs/runner.md new file mode 100644 index 000000000..ebc816ba3 --- /dev/null +++ b/.tessl/tiles/tessl/npm-test/docs/runner.md @@ -0,0 +1,390 @@ +# Test Runner + +Programmatic test runner for executing test files with advanced configuration options including concurrency, timeouts, and custom reporters. The runner provides fine-grained control over test execution and supports both individual file execution and batch processing. + +## Capabilities + +### Run Function + +The main programmatic interface for running tests with configurable options. + +```javascript { .api } +/** + * Programmatic test runner with configurable options + * @param options - Runner configuration options + * @returns TestsStream for monitoring test progress and results + */ +function run(options?: RunOptions): TestsStream; + +interface RunOptions { + /** Number of concurrent test files to run. Default: 1 */ + concurrency?: number; + /** Global timeout for all tests in milliseconds. Default: Infinity */ + timeout?: number; + /** AbortSignal for cancelling test execution */ + signal?: AbortSignal; + /** Array of test file paths to execute. Default: auto-discover */ + files?: string[]; + /** Inspector port for debugging test execution */ + inspectPort?: number; +} +``` + +**Usage Examples:** + +```javascript +import { run } from "test"; + +// Basic usage - auto-discover and run tests +const stream = run(); +stream.on('test:pass', (test) => { + console.log(`✓ ${test.name}`); +}); +stream.on('test:fail', (test) => { + console.log(`✗ ${test.name}: ${test.error.message}`); +}); + +// Run specific files +run({ + files: ['./tests/unit/*.js', './tests/integration/*.js'] +}); + +// Concurrent execution +run({ + concurrency: 4, + timeout: 30000 +}); + +// With abort signal +const controller = new AbortController(); +run({ + signal: controller.signal, + files: ['./long-running-test.js'] +}); + +// Cancel after 10 seconds +setTimeout(() => controller.abort(), 10000); +``` + +### TestsStream + +The runner returns a TestsStream that provides real-time test execution feedback and results. + +```javascript { .api } +interface TestsStream extends EventEmitter { + /** Stream of test events and results */ + on(event: 'test:start', listener: (test: TestInfo) => void): this; + on(event: 'test:pass', listener: (test: TestInfo) => void): this; + on(event: 'test:fail', listener: (test: TestInfo) => void): this; + on(event: 'test:skip', listener: (test: TestInfo) => void): this; + on(event: 'test:todo', listener: (test: TestInfo) => void): this; + on(event: 'test:diagnostic', listener: (test: TestInfo, message: string) => void): this; + on(event: 'end', listener: (results: TestResults) => void): this; +} + +interface TestInfo { + name: string; + file: string; + line?: number; + column?: number; + duration?: number; + error?: Error; +} + +interface TestResults { + total: number; + pass: number; + fail: number; + skip: number; + todo: number; + duration: number; + files: string[]; +} +``` + +**Usage Examples:** + +```javascript +import { run } from "test"; + +const stream = run({ + files: ['./tests/**/*.test.js'], + concurrency: 2 +}); + +// Handle individual test events +stream.on('test:start', (test) => { + console.log(`Starting ${test.name}...`); +}); + +stream.on('test:pass', (test) => { + console.log(`✓ ${test.name} (${test.duration}ms)`); +}); + +stream.on('test:fail', (test) => { + console.error(`✗ ${test.name}`); + console.error(` ${test.error.message}`); + if (test.error.stack) { + console.error(` at ${test.file}:${test.line}:${test.column}`); + } +}); + +stream.on('test:skip', (test) => { + console.log(`- ${test.name} (skipped)`); +}); + +stream.on('test:todo', (test) => { + console.log(`? ${test.name} (todo)`); +}); + +stream.on('test:diagnostic', (test, message) => { + console.log(`# ${message}`); +}); + +// Handle completion +stream.on('end', (results) => { + console.log(`\nResults:`); + console.log(` Total: ${results.total}`); + console.log(` Pass: ${results.pass}`); + console.log(` Fail: ${results.fail}`); + console.log(` Skip: ${results.skip}`); + console.log(` Todo: ${results.todo}`); + console.log(` Duration: ${results.duration}ms`); + + if (results.fail > 0) { + process.exit(1); + } +}); +``` + +## File Discovery + +When no files are specified, the runner automatically discovers test files using these patterns: + +1. Files in `test/` directories with `.js`, `.mjs`, or `.cjs` extensions +2. Files ending with `.test.js`, `.test.mjs`, or `.test.cjs` +3. Files ending with `.spec.js`, `.spec.mjs`, or `.spec.cjs` +4. Excludes `node_modules` directories + +**Examples:** + +```javascript +// These files will be auto-discovered: +// test/user.js +// test/api/auth.test.js +// src/utils.spec.mjs +// integration.test.cjs + +// These will be ignored: +// node_modules/package/test.js +// src/helper.js (no test pattern) +``` + +Manual file specification overrides auto-discovery: + +```javascript +run({ + files: [ + './custom-tests/*.js', + './e2e/**/*.test.js' + ] +}); +``` + +## Concurrency Control + +The runner supports concurrent execution of test files to improve performance: + +```javascript +// Sequential execution (default) +run({ concurrency: 1 }); + +// Parallel execution - 4 files at once +run({ concurrency: 4 }); + +// Unlimited concurrency +run({ concurrency: Infinity }); + +// Boolean concurrency (uses CPU count) +run({ concurrency: true }); +``` + +**Important Notes:** +- Concurrency applies to test files, not individual tests within files +- Individual test concurrency is controlled by test options +- Higher concurrency may reveal race conditions in tests + +## Timeout Configuration + +Set global timeouts for test execution: + +```javascript +// 30-second timeout for all tests +run({ timeout: 30000 }); + +// No timeout (default) +run({ timeout: Infinity }); +``` + +Timeout behavior: +- Applies to individual test files, not the entire run +- Files that timeout are marked as failed +- Other files continue to execute + +## Debugging Support + +Enable debugging for test execution: + +```javascript +// Run with Node.js inspector +run({ + inspectPort: 9229, + files: ['./debug-this-test.js'] +}); + +// Then connect with Chrome DevTools or VS Code +``` + +## Error Handling and Cancellation + +```javascript +import { run } from "test"; + +const controller = new AbortController(); +const stream = run({ + files: ['./tests/**/*.js'], + signal: controller.signal +}); + +// Handle stream errors +stream.on('error', (error) => { + console.error('Runner error:', error); +}); + +// Cancel execution +setTimeout(() => { + console.log('Cancelling tests...'); + controller.abort(); +}, 5000); + +// Handle cancellation +controller.signal.addEventListener('abort', () => { + console.log('Test execution cancelled'); +}); +``` + +## Integration Examples + +### Custom Test Reporter + +```javascript +import { run } from "test"; +import fs from "fs"; + +const results = []; +const stream = run({ files: ['./tests/**/*.js'] }); + +stream.on('test:pass', (test) => { + results.push({ status: 'pass', name: test.name, duration: test.duration }); +}); + +stream.on('test:fail', (test) => { + results.push({ + status: 'fail', + name: test.name, + error: test.error.message, + duration: test.duration + }); +}); + +stream.on('end', () => { + // Write custom report + fs.writeFileSync('test-results.json', JSON.stringify(results, null, 2)); +}); +``` + +### CI/CD Integration + +```javascript +import { run } from "test"; + +async function runTests() { + return new Promise((resolve, reject) => { + const stream = run({ + files: process.env.TEST_FILES?.split(',') || undefined, + concurrency: parseInt(process.env.TEST_CONCURRENCY) || 1, + timeout: parseInt(process.env.TEST_TIMEOUT) || 30000 + }); + + const results = { pass: 0, fail: 0, skip: 0, todo: 0 }; + + stream.on('test:pass', () => results.pass++); + stream.on('test:fail', () => results.fail++); + stream.on('test:skip', () => results.skip++); + stream.on('test:todo', () => results.todo++); + + stream.on('end', (finalResults) => { + console.log(`Tests completed: ${finalResults.pass}/${finalResults.total} passed`); + + if (finalResults.fail > 0) { + reject(new Error(`${finalResults.fail} tests failed`)); + } else { + resolve(finalResults); + } + }); + + stream.on('error', reject); + }); +} + +// Usage in CI +runTests() + .then(() => process.exit(0)) + .catch(() => process.exit(1)); +``` + +### Watch Mode Implementation + +```javascript +import { run } from "test"; +import { watch } from "fs"; + +let currentRun = null; + +function runTests() { + if (currentRun) { + currentRun.abort(); + } + + const controller = new AbortController(); + currentRun = controller; + + const stream = run({ + signal: controller.signal, + files: ['./src/**/*.test.js'] + }); + + stream.on('end', (results) => { + console.log(`\nWatching for changes... (${results.pass}/${results.total} passed)`); + currentRun = null; + }); + + stream.on('error', (error) => { + if (error.name !== 'AbortError') { + console.error('Run error:', error); + } + currentRun = null; + }); +} + +// Initial run +runTests(); + +// Watch for file changes +watch('./src', { recursive: true }, (eventType, filename) => { + if (filename.endsWith('.js')) { + console.log(`\nFile changed: ${filename}`); + runTests(); + } +}); +``` \ No newline at end of file diff --git a/.tessl/tiles/tessl/npm-test/tile.json b/.tessl/tiles/tessl/npm-test/tile.json new file mode 100644 index 000000000..6566e625d --- /dev/null +++ b/.tessl/tiles/tessl/npm-test/tile.json @@ -0,0 +1,7 @@ +{ + "name": "tessl/npm-test", + "version": "3.3.0", + "docs": "docs/index.md", + "describes": "pkg:npm/test@3.3.0", + "summary": "Node.js 18's node:test, as an npm package providing comprehensive testing framework for Node.js 14+" +} \ No newline at end of file diff --git a/components/frontend/package-lock.json b/components/frontend/package-lock.json index 247672243..db5af1f5b 100644 --- a/components/frontend/package-lock.json +++ b/components/frontend/package-lock.json @@ -2479,6 +2479,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.7.tgz", "integrity": "sha512-wAHc/cgKzW7LZNFloThyHnV/AX9gTg3w5yAv0gvQHPZoCnepwqCMtzbuPbb2UvfvO32XZ46e8bPOYbfZhzVnnQ==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/query-core": "5.90.7" }, @@ -2595,6 +2596,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -2605,6 +2607,7 @@ "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -2661,6 +2664,7 @@ "integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.4", "@typescript-eslint/types": "8.46.4", @@ -3184,6 +3188,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4176,6 +4181,7 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4349,6 +4355,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -7568,6 +7575,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -7577,6 +7585,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -7589,6 +7598,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz", "integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -8512,6 +8522,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -8691,6 +8702,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/components/frontend/src/components/ui/tool-message.tsx b/components/frontend/src/components/ui/tool-message.tsx index 02b14473f..f11941300 100644 --- a/components/frontend/src/components/ui/tool-message.tsx +++ b/components/frontend/src/components/ui/tool-message.tsx @@ -34,13 +34,61 @@ const formatToolName = (toolName?: string) => { .join(" "); }; +/** + * Redacts sensitive tokens and credentials from text for safe display in the UI. + * + * IMPORTANT: Keep these patterns in sync with: + * - Backend: components/backend/server/server.go (query string redaction) + * - Runner: components/runners/claude-code-runner/wrapper.py (_redact_secrets) + * + * When adding new patterns, update all three locations. + * + * @param text - The text to redact secrets from (accepts null/undefined for safety) + * @returns The text with all sensitive values replaced with redaction markers, or empty string if input is null/undefined + * + * @example + * redactSecrets('Token: ghp_abc123...') + * // Returns: 'Token: gh*_[REDACTED]' + * + * @example + * redactSecrets('curl -H "Authorization: Bearer sk-proj-1234567890123456789012345678901234567890"') + * // Returns: 'curl -H "Authorization: Bearer [REDACTED]"' + */ +const redactSecrets = (text: string | null | undefined): string => { + if (!text) return ''; + + // Redact GitHub tokens (ghs_, ghp_, gho_, ghu_ prefixes) + text = text.replace(/gh[pousr]_[a-zA-Z0-9]{36,255}/g, 'gh*_[REDACTED]'); + + // Redact x-access-token: patterns in URLs + text = text.replace(/x-access-token:[^@\s]+@/g, 'x-access-token:[REDACTED]@'); + + // Redact oauth tokens in URLs + text = text.replace(/oauth2:[^@\s]+@/g, 'oauth2:[REDACTED]@'); + + // Redact basic auth credentials in URLs + text = text.replace(/:\/\/[^:@\s]+:[^@\s]+@/g, '://[REDACTED]@'); + + // Redact Authorization header values (Bearer, token, etc.) - minimum 20 chars to avoid false positives + text = text.replace(/(Authorization["\s:]+)(Bearer\s+|token\s+)?([a-zA-Z0-9_\-\.]{20,})/gi, '$1$2[REDACTED]'); + + // Redact common API key patterns (sk-* prefix) - handle start of string, quotes, colons, equals + text = text.replace(/(^|["\s:=])(sk-[a-zA-Z0-9]{20,})/g, '$1[REDACTED]'); + + // Redact api_key or api-key patterns - handle start of string and various separators + text = text.replace(/(^|["\s])(api[_-]?key["\s:=]+)([a-zA-Z0-9_\-\.]{20,})/gi, '$1$2[REDACTED]'); + + return text; +}; + const formatToolInput = (input?: string) => { if (!input) return "{}"; try { const parsed = JSON.parse(input); - return JSON.stringify(parsed, null, 2); + const formatted = JSON.stringify(parsed, null, 2); + return redactSecrets(formatted); } catch { - return input; + return redactSecrets(input); } }; @@ -145,7 +193,7 @@ const getColorClassesForName = (name: string) => { const extractTextFromResultContent = (content: unknown): string => { try { - if (typeof content === "string") return content; + if (typeof content === "string") return redactSecrets(content); if (Array.isArray(content)) { const texts = content .map((item) => { @@ -155,7 +203,7 @@ const extractTextFromResultContent = (content: unknown): string => { return ""; }) .filter(Boolean); - if (texts.length) return texts.join("\n\n"); + if (texts.length) return redactSecrets(texts.join("\n\n")); } if (content && typeof content === "object") { // Some schemas nest under content: [] @@ -169,12 +217,12 @@ const extractTextFromResultContent = (content: unknown): string => { return ""; }) .filter(Boolean); - if (texts.length) return texts.join("\n\n"); + if (texts.length) return redactSecrets(texts.join("\n\n")); } } - return JSON.stringify(content ?? ""); + return redactSecrets(JSON.stringify(content ?? "")); } catch { - return String(content ?? ""); + return redactSecrets(String(content ?? "")); } }; @@ -347,11 +395,11 @@ export const ToolMessage = React.forwardRef( >