Files
compiler-explorer/test/common-utils-tests.ts
Matt Godbolt c29ad46f3a [Not Live; disabled by default] Add Claude Explain feature for AI-powered assembly explanations (#7749)
Add Claude Explain feature for AI-powered code explanations

This PR introduces Claude Explain, a new feature that provides AI-powered explanations of compiler output directly within Compiler Explorer.

Key features:

Claude Explain functionality:
  - New explain view pane
  - Explains compiler output with full context of source code and compilation output
  - Configurable audience level and explanation type
  - Response caching to improve performance and reduce API calls
  - Usage statistics display showing requests used and token counts

User experience:
  - Consent flow on first use explaining data handling and privacy
  - AI disclaimer banner warning about potential LLM inaccuracies
  - Respects "no-ai" directive in source code for users who don't want AI processing

Privacy and security:
  - Data sent to Anthropic's Claude API as documented in privacy policy
  - No data used for model training
  - Clear consent required before first use
  - Support for opting out via "no-ai" directive

The feature is marked as beta and can be enabled via configuration.

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-05 09:31:48 -05:00

196 lines
7.8 KiB
TypeScript

// Copyright (c) 2023, Compiler Explorer Authors
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
import {describe, expect, it} from 'vitest';
import {addDigitSeparator, capitaliseFirst, escapeHTML, splitArguments} from '../shared/common-utils.js';
describe('HTML Escape Test Cases', () => {
it('should prevent basic injection', () => {
expect(escapeHTML("<script>alert('hi');</script>")).toEqual(
'&lt;script&gt;alert(&#x27;hi&#x27;);&lt;/script&gt;',
);
});
it('should prevent tag injection', () => {
expect(escapeHTML('\'"`>')).toEqual('&#x27;&quot;&#x60;&gt;');
});
});
describe('digit separator', () => {
it('handles short numbers', () => {
expect(addDigitSeparator('42', '_', 3)).toEqual('42');
});
it('handles long numbers', () => {
expect(addDigitSeparator('1234', '_', 3)).toEqual('1_234');
expect(addDigitSeparator('123456789', "'", 3)).toEqual("123'456'789");
expect(addDigitSeparator('1234567890', "'", 3)).toEqual("1'234'567'890");
});
it('handles hex numbers', () => {
expect(addDigitSeparator('AABBCCDD12345678', '_', 4)).toEqual('AABB_CCDD_1234_5678');
expect(addDigitSeparator('01AABBCCDD12345678', '_', 4)).toEqual('01_AABB_CCDD_1234_5678');
});
it('handles negative numbers', () => {
expect(addDigitSeparator('-42', '_', 3)).toEqual('-42');
expect(addDigitSeparator('-420', '_', 3)).toEqual('-420');
expect(addDigitSeparator('-4200', '_', 3)).toEqual('-4_200');
expect(addDigitSeparator('-123456789', '_', 3)).toEqual('-123_456_789');
});
});
describe('argument splitting', () => {
it('should handle empty strings', () => {
expect(splitArguments('')).toEqual([]);
});
it('should handle purely whitespace strings', () => {
expect(splitArguments(' ')).toEqual([]);
});
it('should handle normal things', () => {
expect(splitArguments('-hello --world etc --std=c++20')).toEqual(['-hello', '--world', 'etc', '--std=c++20']);
});
it('should handle hash chars', () => {
expect(splitArguments('-Wno#warnings -Wno-#pragma-messages')).toEqual([
'-Wno#warnings',
'-Wno-#pragma-messages',
]);
});
it('should handle doublequoted args', () => {
expect(splitArguments('--hello "-world etc"')).toEqual(['--hello', '-world etc']);
});
it('should handle singlequoted args', () => {
expect(splitArguments("--hello '-world etc'")).toEqual(['--hello', '-world etc']);
});
it('should handle mismatched doublequoted args', () => {
expect(splitArguments('--hello "-world etc')).toEqual(['--hello', '-world etc']);
});
it('should handle mismatched singlequoted args', () => {
expect(splitArguments("--hello '-world etc")).toEqual(['--hello', '-world etc']);
});
it('should handle hashes at the beginning of words', () => {
expect(splitArguments('hello #veryfancy etc')).toEqual(['hello', '#veryfancy', 'etc']);
});
it('should handle empty arguments', () => {
expect(splitArguments('nothing "" is lost \'\'')).toEqual(['nothing', '', 'is', 'lost', '']);
});
it('should handle weird characters', () => {
expect(splitArguments('hello | moose | \\n < yay & \n\t\nmoo')).toEqual([
'hello',
'|',
'moose',
'|',
'n',
'<',
'yay',
'&',
'moo',
]);
});
it('should handle escaped backslashes', () => {
expect(splitArguments('hello \\\\ moose')).toEqual(['hello', '\\', 'moose']);
});
it('should handle mismatched backslashes', () => {
expect(splitArguments('hello \\')).toEqual(['hello', '\\']);
expect(splitArguments('hello world\\')).toEqual(['hello', 'world\\']);
});
it('should handle mismatched backslashes in quotes', () => {
expect(splitArguments('hello "\\')).toEqual(['hello', '\\']);
});
it('should handle escaped quotes', () => {
expect(splitArguments('hi \\"mum\\"')).toEqual(['hi', '"mum"']);
expect(splitArguments('hi "\\"mum\\""')).toEqual(['hi', '"mum"']);
expect(splitArguments("hi \\'mum\\'")).toEqual(['hi', "'mum'"]);
});
it('should do something sane in the face of escaped quotes in quotes', () => {
// Not really valid input, bash sees this as a bunch of abutting single-quotes, unterminated.
// But this makes as much sense as anything and so I've added it to lock in the behaviour.
expect(splitArguments("hi '\\'mum\\''")).toEqual(['hi', "\\mum'"]);
});
it('should handle abutting quotes', () => {
expect(splitArguments('well "hello""there"')).toEqual(['well', 'hellothere']);
expect(splitArguments('well "hello"\'there\'')).toEqual(['well', 'hellothere']);
});
it('should ignore escaped hashes etc', () => {
expect(splitArguments('hello \\#veryfancy etc')).toEqual(['hello', '#veryfancy', 'etc']);
});
it('should handle multiple spaces', () => {
expect(splitArguments('hello world')).toEqual(['hello', 'world']);
});
it('should handle escaped spaces', () => {
expect(splitArguments('hello\\ there')).toEqual(['hello there']);
});
it('should handle mixed quotes and spaces', () => {
expect(splitArguments('"hello \'world\' there"')).toEqual(["hello 'world' there"]);
});
it('should handle quotes containing spaces', () => {
expect(splitArguments('hello " " world')).toEqual(['hello', ' ', 'world']);
});
it('should handle multiple escapes in quotes', () => {
expect(splitArguments('"hello \\"world\\" \\\\"')).toEqual(['hello "world" \\']);
});
});
describe('capitalise first', () => {
it('should capitalise normal strings', () => {
expect(capitaliseFirst('hello')).toEqual('Hello');
expect(capitaliseFirst('world')).toEqual('World');
});
it('should handle empty strings', () => {
expect(capitaliseFirst('')).toEqual('');
});
it('should handle single characters', () => {
expect(capitaliseFirst('a')).toEqual('A');
expect(capitaliseFirst('z')).toEqual('Z');
});
it('should handle already capitalised strings', () => {
expect(capitaliseFirst('Hello')).toEqual('Hello');
expect(capitaliseFirst('WORLD')).toEqual('WORLD');
});
it('should handle non-alphabetic first characters', () => {
expect(capitaliseFirst('123abc')).toEqual('123abc');
expect(capitaliseFirst('!hello')).toEqual('!hello');
});
});