Skip to content

[claude 3.7 sonnet] CLI Kit coverage - url, error, result, toml #5894

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: shauns/05-20-cli_kit_coverage_-_content-tokens_constants_ui
Choose a base branch
from
Open
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
31 changes: 31 additions & 0 deletions packages/cli-kit/src/public/common/url.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@ describe('isValidURL', () => {
// Then
expect(got).toBe(false)
})

test('throws error if URL constructor throws non-TypeError error', () => {
// Given
const originalURL = global.URL
global.URL = function () {
throw new Error('Custom error')
} as unknown as typeof URL

// Then
expect(() => isValidURL('https://example.com')).toThrow('Custom error')

// Clean up
global.URL = originalURL
})
})

describe('safeParseURL', () => {
Expand All @@ -56,4 +70,21 @@ describe('safeParseURL', () => {

expect(result).toBeUndefined()
})

test('returns undefined for any error thrown by URL constructor', () => {
// Given
const originalURL = global.URL
global.URL = function () {
throw new Error('Custom error that is not TypeError')
} as unknown as typeof URL

// When
const result = safeParseURL('https://example.com')

// Then
expect(result).toBeUndefined()

// Clean up
global.URL = originalURL
})
})
168 changes: 152 additions & 16 deletions packages/cli-kit/src/public/node/error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe('handler', () => {

test('error output uses same input error instance when the error type is bug', async () => {
// Given
const bugError = new BugError('error message', 'try message')
const bugError = new BugError('error message')
vi.mocked(renderFatalError).mockResolvedValue('')

// When
Expand All @@ -31,43 +31,179 @@ describe('handler', () => {

test('error output uses a BugError instance instance when the error type not extends from fatal', async () => {
// Given
const unknownError = new Error('Unknown')
const notFatalError = new Error('error message')
vi.mocked(renderFatalError).mockResolvedValue('')

// When
await handler(unknownError)
await handler(notFatalError)

// Then
expect(renderFatalError).toHaveBeenCalledWith(expect.objectContaining({type: expect.any(Number)}))
expect(unknownError).not.contains({type: expect.any(Number)})
expect(renderFatalError).toHaveBeenCalledWith(expect.any(BugError))
})

test('wraps string error into a BugError', async () => {
// Given
const stringValue = 'error message'
vi.mocked(renderFatalError).mockResolvedValue('')

// When
await handler(stringValue)

// Then
expect(renderFatalError).toHaveBeenCalledWith(expect.any(BugError))
expect(renderFatalError).toHaveBeenCalledWith(
expect.objectContaining({
message: stringValue,
}),
)
})

test('handles unknown error with default message', async () => {
// Given
const errorObject = {custom: 'error object'}
vi.mocked(renderFatalError).mockResolvedValue('')

// When
await handler(errorObject)

// Then
expect(renderFatalError).toHaveBeenCalledWith(expect.any(BugError))
expect(renderFatalError).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Unknown error',
}),
)
})
})

describe('stack file path helpers', () => {
test.each([
['simple file:///', 'file:///something/there.js'],
['windows file://', 'file:///D:\\something\\there.js'],
['unix no file', '/something/there.js'],
['windows no file', 'D:\\something\\there.js'],
])('%s', (_, path) => {
expect(cleanSingleStackTracePath(path)).toEqual('/something/there.js')
test('simple file:///', () => {
const input = 'file:///Users/username/my/file.js'
const expected = '/Users/username/my/file.js'
expect(cleanSingleStackTracePath(input)).toBe(expected)
})

test('windows file://', () => {
// Normalize path strips the drive letter on the test machine
const input = 'file://C:/Users/username/my/file.js'
const expected = '/Users/username/my/file.js'
expect(cleanSingleStackTracePath(input)).toBe(expected)
})

test('unix no file', () => {
const input = '/Users/username/my/file.js'
const expected = '/Users/username/my/file.js'
expect(cleanSingleStackTracePath(input)).toBe(expected)
})

test('windows no file', () => {
// Normalize path strips the drive letter on the test machine
const input = 'C:\\Users\\username\\my\\file.js'
const expected = '/Users/username/my/file.js'
expect(cleanSingleStackTracePath(input)).toBe(expected)
})

test('handles paths with multiple slashes', () => {
const input = 'file:///Users/username//my///file.js'
const expected = '/Users/username/my/file.js'
expect(cleanSingleStackTracePath(input)).toBe(expected)
})

test('handles Windows paths with backslashes and drive letter', () => {
// Normalize path strips the drive letter on the test machine
const input = 'C:\\Users\\username\\my\\file.js'
const expected = '/Users/username/my/file.js'
expect(cleanSingleStackTracePath(input)).toBe(expected)
})

test('handles paths with URL encoded characters', () => {
// The current implementation doesn't decode URL-encoded characters
const input = 'file:///Users/user%20name/my/file.js'
const expected = '/Users/user%20name/my/file.js'
expect(cleanSingleStackTracePath(input)).toBe(expected)
})

test('handles paths with mixed forward and backward slashes', () => {
// Normalize path strips the drive letter on the test machine
const input = 'C:\\Users/username\\my/file.js'
const expected = '/Users/username/my/file.js'
expect(cleanSingleStackTracePath(input)).toBe(expected)
})

test('preserves query parameters', () => {
const input = 'file:///Users/username/my/file.js?line=10'
const expected = '/Users/username/my/file.js?line=10'
expect(cleanSingleStackTracePath(input)).toBe(expected)
})
})

describe('shouldReportErrorAsUnexpected helper', () => {
test('returns true for normal errors', () => {
expect(shouldReportErrorAsUnexpected(new Error('test'))).toBe(true)
const error = new Error('A normal error')
expect(shouldReportErrorAsUnexpected(error)).toBe(true)
})

test('returns false for AbortError', () => {
expect(shouldReportErrorAsUnexpected(new AbortError('test'))).toBe(false)
const error = new AbortError('An abort error')
expect(shouldReportErrorAsUnexpected(error)).toBe(false)
})

test('returns true for BugError', () => {
expect(shouldReportErrorAsUnexpected(new BugError('test'))).toBe(true)
const error = new BugError('A bug error')
expect(shouldReportErrorAsUnexpected(error)).toBe(true)
})

test('returns false for errors that imply environment issues', () => {
expect(shouldReportErrorAsUnexpected(new Error('EPERM: operation not permitted, scandir'))).toBe(false)
const dnsError = new Error('getaddrinfo ENOTFOUND example.com')
expect(shouldReportErrorAsUnexpected(dnsError)).toBe(false)

const connectRefusedError = new Error('EACCES: permission denied, open /some/file')
expect(shouldReportErrorAsUnexpected(connectRefusedError)).toBe(false)
})

test('returns true for errors with specific codes that are not environment issues', () => {
// For errors that don't match environment issues, they should return true
const error = new Error('Some other error')
expect(shouldReportErrorAsUnexpected(error)).toBe(true)
})
})

describe('AbortError', () => {
test('initializes with message and tryMessage', () => {
const message = 'Error message'
const tryMessage = 'Try message'
const error = new AbortError(message, tryMessage)

expect(error.message).toBe(message)
expect(error.tryMessage).toBe(tryMessage)
})

test('initializes with only message', () => {
const message = 'Error message'
const error = new AbortError(message)

expect(error.message).toBe(message)
expect(error.tryMessage).toBeNull()
})

test('has the correct type property', () => {
const error = new AbortError('Error message')
// FatalErrorType.Abort is 0
expect(error.type).toBe(0)
})
})

describe('BugError', () => {
test('initializes with message', () => {
const message = 'Error message'
const error = new BugError(message)

expect(error.message).toBe(message)
})

test('has the correct type property', () => {
const error = new BugError('Error message')
// FatalErrorType.Bug is 2
expect(error.type).toBe(2)
})
})
70 changes: 65 additions & 5 deletions packages/cli-kit/src/public/node/result.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {err, ok} from './result.js'
import {mockAndCaptureOutput} from './testing/output.js'
import {AbortError, BugError} from './error.js'
import {outputSuccess} from '../../public/node/output.js'
import {describe, expect, test} from 'vitest'
import {describe, expect, test, beforeEach} from 'vitest'

describe('ok', () => {
test('create ok with value', () => {
Expand All @@ -12,6 +12,14 @@ describe('ok', () => {
// Then
expect(!result.isErr() && result.value).toEqual(123)
})

test('isErr returns false for Ok results', () => {
// When
const result = ok('test value')

// Then
expect(result.isErr()).toBe(false)
})
})

describe('err', () => {
Expand All @@ -22,6 +30,14 @@ describe('err', () => {
// Then
expect(result.isErr() && result.error).toEqual(new Error('Custom error object'))
})

test('isErr returns true for Err results', () => {
// When
const result = err('error message')

// Then
expect(result.isErr()).toBe(true)
})
})

describe('valueOrBug', () => {
Expand Down Expand Up @@ -97,19 +113,48 @@ describe('mapError', () => {
// Then
expect(() => result.valueOrBug()).toThrow('Mapped error')
})

test('when error result is mapped to a different type', () => {
// Given
const originalError = {type: 'validation', message: 'Invalid input'}

// When
const result = err(originalError).mapError((error) => new Error(`${error.type} error: ${error.message}`))

// Then
expect(result.isErr()).toBe(true)
expect(() => result.valueOrBug()).toThrow('validation error: Invalid input')
})
})

describe('doOnOk', () => {
test('when ok result should execute the command and continue', () => {
// Given
const outpuMocker = mockAndCaptureOutput()
let outputMocker: ReturnType<typeof mockAndCaptureOutput>

beforeEach(() => {
outputMocker = mockAndCaptureOutput()
outputMocker.clear()
})

test('when ok result should execute the command and continue', () => {
// When
const result = ok(123).doOnOk((value) => outputSuccess(`result ok ${value}`))

// Then
expect(!result.isErr() && result.value).toEqual(123)
expect(outpuMocker.success()).toMatchInlineSnapshot('"result ok 123"')
expect(outputMocker.success()).toEqual('result ok 123')
})

test('when err result should not execute the command and return the error', () => {
// Given
const errorValue = new Error('Test error')

// When
const result = err(errorValue).doOnOk((value) => outputSuccess(`result ok ${value}`))

// Then
expect(result.isErr()).toBe(true)
expect(result.isErr() && result.error).toBe(errorValue)
expect(outputMocker.success()).toBe('')
})
})

Expand All @@ -129,4 +174,19 @@ describe('map', () => {
// Then
expect(() => result.valueOrBug()).toThrow('Original error')
})

test('when ok result is mapped to a transformed value', () => {
// Given
const original = {count: 5, name: 'test'}

// When
const result = ok(original).map((value) => ({
...value,
count: value.count * 2,
}))

// Then
expect(result.isErr()).toBe(false)
expect(!result.isErr() && result.value).toEqual({count: 10, name: 'test'})
})
})
32 changes: 31 additions & 1 deletion packages/cli-kit/src/public/node/toml.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {decodeToml} from './toml.js'
import {decodeToml, encodeToml} from './toml.js'
import {describe, expect, test} from 'vitest'

describe('decodeToml', () => {
Expand Down Expand Up @@ -28,3 +28,33 @@ describe('decodeToml', () => {
expect(result).toStrictEqual({access: {admin: {direct_api_mode: 'online'}}})
})
})

describe('encodeToml', () => {
test('converts a simple object to TOML', () => {
const input = {name: 'app'}
const result = encodeToml(input)
expect(result).toBe('name = "app"\n')
})

test('converts nested objects to TOML', () => {
const input = {
webhooks: {
api_version: '2023-07',
},
}
const result = encodeToml(input)
expect(result).toBe('[webhooks]\napi_version = "2023-07"\n')
})

test('converts complex nested objects to TOML', () => {
const input = {
access: {
admin: {
direct_api_mode: 'online',
},
},
}
const result = encodeToml(input)
expect(result).toBe('[access.admin]\ndirect_api_mode = "online"\n')
})
})
Loading