Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ There are several ways to use {% data variables.product.prodname_copilot_short %
* You must choose a monthly or yearly billing cycle and provide a payment method.
* If you do not cancel before the end of the trial, it automatically converts to a paid plan.
* You can cancel any time during the 30 days. If you cancel, you will not be charged and will keep access until the trial ends.
* Free trials are limited to three per payment method. Additional trials will continue as paid subscriptions.

### Educational and open source benefits

Expand Down
2 changes: 1 addition & 1 deletion content/copilot/concepts/agents/about-copilot-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ The command-line interface (CLI) for {% data variables.product.prodname_copilot

* Linux
* macOS
* Windows from within [Windows Subsystem for Linux (WSL)](https://learn.microsoft.com/en-us/windows/wsl/about). Native Windows support in Powershell is available, but experimental.
* Windows from within Powershell and [Windows Subsystem for Linux (WSL)](https://learn.microsoft.com/en-us/windows/wsl/about)

For installation instructions, see [AUTOTITLE](/copilot/how-tos/set-up/install-copilot-cli).

Expand Down
17 changes: 15 additions & 2 deletions src/languages/lib/render-with-fallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,27 @@ import type { Context } from '@/types'

export class EmptyTitleError extends Error {}

interface LiquidToken {
export interface LiquidToken {
file?: string
getPosition?: () => [number, number]
}

interface LiquidError extends Error {
/**
* Custom error class for Liquid rendering errors with proper type safety.
* Use this instead of creating Error objects and mutating them with type assertions.
*
* @example
* const error = new LiquidError('Unknown tag', 'ParseError')
* error.token = { file: '/content/test.md', getPosition: () => [1, 5] }
*/
export class LiquidError extends Error {
token?: LiquidToken
originalError?: Error

constructor(message: string, name: 'ParseError' | 'RenderError' | 'TokenizationError') {
super(message)
this.name = name
}
}

interface RenderOptions {
Expand Down
16 changes: 12 additions & 4 deletions src/languages/scripts/count-translation-corruptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,19 @@ function run(languageCode: string, site: Site, englishReusables: Reusables) {
const illegalTags = new Map<string, number>()

function countError(error: TokenizationError, where: string) {
const originalError = (error as { originalError?: Error }).originalError
// TokenizationError from liquidjs may have originalError and token.content
// but these aren't in the public type definitions
const errorWithExtras = error as TokenizationError & {
originalError?: Error
token?: { content?: string }
}
const originalError = errorWithExtras.originalError
const errorString = originalError ? originalError.message : error.message
if (errorString.includes('illegal tag syntax')) {
const illegalTag = (error as unknown as { token: { content: string } }).token.content
illegalTags.set(illegalTag, (illegalTags.get(illegalTag) || 0) + 1)
if (errorString.includes('illegal tag syntax') && errorWithExtras.token?.content) {
illegalTags.set(
errorWithExtras.token.content,
(illegalTags.get(errorWithExtras.token.content) || 0) + 1,
)
}
errors.set(errorString, (errors.get(errorString) || 0) + 1)
wheres.set(where, (wheres.get(where) || 0) + 1)
Expand Down
96 changes: 35 additions & 61 deletions src/languages/tests/translation-error-comments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,11 @@ import {
EmptyTitleError,
renderContentWithFallback,
executeWithFallback,
LiquidError,
} from '../lib/render-with-fallback'
import { TitleFromAutotitleError } from '@/content-render/unified/rewrite-local-links'
import Page from '@/frame/lib/page'

// Type aliases for error objects with token information
type ErrorWithToken = Error & { token: { file: string; getPosition: () => number[] } }
type ErrorWithTokenNoFile = Error & { token: { getPosition: () => number[] } }
type ErrorWithTokenNoPosition = Error & { token: { file: string } }
type ErrorWithTokenAndOriginal = Error & {
token: { file: string; getPosition: () => number[] }
originalError: Error
}

describe('Translation Error Comments', () => {
// Mock renderContent for integration tests
let mockRenderContent: MockedFunction<
Expand All @@ -35,9 +27,8 @@ describe('Translation Error Comments', () => {
describe('createTranslationFallbackComment', () => {
describe('Liquid ParseError', () => {
test('includes all fields when token information is available', () => {
const error = new Error("Unknown tag 'badtag', line:1, col:3")
error.name = 'ParseError'
;(error as unknown as ErrorWithToken).token = {
const error = new LiquidError("Unknown tag 'badtag', line:1, col:3", 'ParseError')
error.token = {
file: '/content/test/article.md',
getPosition: () => [1, 3],
}
Expand All @@ -57,15 +48,15 @@ describe('Translation Error Comments', () => {

describe('Liquid RenderError', () => {
test('includes original error message when available', () => {
const error = new Error("Unknown variable 'variables.nonexistent.value'")
error.name = 'RenderError'
;(error as unknown as ErrorWithToken).token = {
const error = new LiquidError(
"Unknown variable 'variables.nonexistent.value'",
'RenderError',
)
error.token = {
file: '/content/test/intro.md',
getPosition: () => [3, 15],
}
;(error as unknown as ErrorWithTokenAndOriginal).originalError = new Error(
'Variable not found: variables.nonexistent.value',
)
error.originalError = new Error('Variable not found: variables.nonexistent.value')

const result = createTranslationFallbackComment(error, 'rawIntro')

Expand All @@ -78,9 +69,8 @@ describe('Translation Error Comments', () => {
})

test('falls back to main error message when no originalError', () => {
const error = new Error('Main error message')
error.name = 'RenderError'
;(error as unknown as ErrorWithToken).token = {
const error = new LiquidError('Main error message', 'RenderError')
error.token = {
file: '/content/test.md',
getPosition: () => [1, 1],
}
Expand All @@ -93,9 +83,8 @@ describe('Translation Error Comments', () => {

describe('Liquid TokenizationError', () => {
test('includes tokenization error details', () => {
const error = new Error('Unexpected token, line:1, col:10')
error.name = 'TokenizationError'
;(error as unknown as ErrorWithToken).token = {
const error = new LiquidError('Unexpected token, line:1, col:10', 'TokenizationError')
error.token = {
file: '/content/test/page.md',
getPosition: () => [1, 10],
}
Expand Down Expand Up @@ -147,9 +136,8 @@ describe('Translation Error Comments', () => {

describe('Error handling edge cases', () => {
test('handles error with no token information gracefully', () => {
const error = new Error('Generic liquid error without token info')
error.name = 'RenderError'
// No token property
const error = new LiquidError('Generic liquid error without token info', 'RenderError')
// No token property set

const result = createTranslationFallbackComment(error, 'rawIntro')

Expand All @@ -163,9 +151,8 @@ describe('Translation Error Comments', () => {
})

test('handles error with token but no file', () => {
const error = new Error('Error message')
error.name = 'ParseError'
;(error as unknown as ErrorWithTokenNoFile).token = {
const error = new LiquidError('Error message', 'ParseError')
error.token = {
// No file property
getPosition: () => [5, 10],
}
Expand All @@ -178,9 +165,8 @@ describe('Translation Error Comments', () => {
})

test('handles error with token but no getPosition method', () => {
const error = new Error('Error message')
error.name = 'ParseError'
;(error as unknown as ErrorWithTokenNoPosition).token = {
const error = new LiquidError('Error message', 'ParseError')
error.token = {
file: '/content/test.md',
// No getPosition method
}
Expand All @@ -194,8 +180,7 @@ describe('Translation Error Comments', () => {

test('truncates very long error messages', () => {
const longMessage = 'A'.repeat(300) // Very long error message
const error = new Error(longMessage)
error.name = 'ParseError'
const error = new LiquidError(longMessage, 'ParseError')

const result = createTranslationFallbackComment(error, 'rawTitle')

Expand All @@ -211,8 +196,7 @@ describe('Translation Error Comments', () => {
})

test('properly escapes quotes in error messages', () => {
const error = new Error('Error with "double quotes" and more')
error.name = 'RenderError'
const error = new LiquidError('Error with "double quotes" and more', 'RenderError')

const result = createTranslationFallbackComment(error, 'rawTitle')

Expand All @@ -233,9 +217,7 @@ describe('Translation Error Comments', () => {
})

test('handles error with no message', () => {
const error = new Error()
error.name = 'ParseError'
// Message will be empty string by default
const error = new LiquidError('', 'ParseError')

const result = createTranslationFallbackComment(error, 'title')

Expand All @@ -245,8 +227,7 @@ describe('Translation Error Comments', () => {
})

test('cleans up multiline messages', () => {
const error = new Error('Line 1\nLine 2\n Line 3 \n\nLine 5')
error.name = 'RenderError'
const error = new LiquidError('Line 1\nLine 2\n Line 3 \n\nLine 5', 'RenderError')

const result = createTranslationFallbackComment(error, 'content')

Expand All @@ -257,9 +238,8 @@ describe('Translation Error Comments', () => {

describe('Comment format validation', () => {
test('comment format is valid HTML', () => {
const error = new Error('Test error')
error.name = 'ParseError'
;(error as unknown as ErrorWithToken).token = {
const error = new LiquidError('Test error', 'ParseError')
error.token = {
file: '/content/test.md',
getPosition: () => [1, 1],
}
Expand All @@ -275,9 +255,8 @@ describe('Translation Error Comments', () => {
})

test('contains all required fields when available', () => {
const error = new Error('Detailed error message')
error.name = 'RenderError'
;(error as unknown as ErrorWithToken).token = {
const error = new LiquidError('Detailed error message', 'RenderError')
error.token = {
file: '/content/detailed-test.md',
getPosition: () => [42, 15],
}
Expand All @@ -294,9 +273,8 @@ describe('Translation Error Comments', () => {
})

test('maintains consistent field order', () => {
const error = new Error('Test message')
error.name = 'ParseError'
;(error as unknown as ErrorWithToken).token = {
const error = new LiquidError('Test message', 'ParseError')
error.token = {
file: '/content/test.md',
getPosition: () => [1, 1],
}
Expand Down Expand Up @@ -336,9 +314,8 @@ describe('Translation Error Comments', () => {
mockRenderContent.mockImplementation(
(template: string, innerContext: Record<string, unknown>) => {
if (innerContext.currentLanguage !== 'en' && template.includes('badtag')) {
const error = new Error("Unknown tag 'badtag'")
error.name = 'ParseError'
;(error as unknown as ErrorWithToken).token = {
const error = new LiquidError("Unknown tag 'badtag'", 'ParseError')
error.token = {
file: '/content/test.md',
getPosition: () => [1, 5],
}
Expand Down Expand Up @@ -375,8 +352,7 @@ describe('Translation Error Comments', () => {
mockRenderContent.mockImplementation(
(template: string, innerContext: Record<string, unknown>) => {
if (innerContext.currentLanguage !== 'en' && template.includes('badtag')) {
const error = new Error("Unknown tag 'badtag'")
error.name = 'ParseError'
const error = new LiquidError("Unknown tag 'badtag'", 'ParseError')
throw error
}
return 'English Title'
Expand All @@ -399,9 +375,8 @@ describe('Translation Error Comments', () => {
}

const failingCallable = async () => {
const error = new Error("Unknown variable 'variables.bad'")
error.name = 'RenderError'
;(error as unknown as ErrorWithToken).token = {
const error = new LiquidError("Unknown variable 'variables.bad'", 'RenderError')
error.token = {
file: '/content/article.md',
getPosition: () => [10, 20],
}
Expand All @@ -427,8 +402,7 @@ describe('Translation Error Comments', () => {
}

const failingCallable = async () => {
const error = new Error('Test error')
error.name = 'RenderError'
const error = new LiquidError('Test error', 'RenderError')
throw error
}

Expand Down
Loading