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
42 changes: 42 additions & 0 deletions src/__tests__/refs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, expect, it, vi } from 'vitest'
import {
extractId,
isIdRef,
looksLikeRawId,
requireIdRef,
resolveParentTaskId,
resolveProjectId,
Expand Down Expand Up @@ -62,6 +63,30 @@ describe('requireIdRef', () => {
})
})

describe('looksLikeRawId', () => {
it('detects alphanumeric mixed strings', () => {
expect(looksLikeRawId('6fmg66Fr27R59RPg')).toBe(true)
expect(looksLikeRawId('abc123')).toBe(true)
expect(looksLikeRawId('task1')).toBe(true)
})

it('detects purely numeric strings', () => {
expect(looksLikeRawId('123456789')).toBe(true)
expect(looksLikeRawId('42')).toBe(true)
})

it('rejects strings with spaces', () => {
expect(looksLikeRawId('Buy milk')).toBe(false)
expect(looksLikeRawId('task 1')).toBe(false)
})

it('rejects pure alpha strings (likely names)', () => {
expect(looksLikeRawId('Work')).toBe(false)
expect(looksLikeRawId('Shopping')).toBe(false)
expect(looksLikeRawId('mom')).toBe(false)
})
})

describe('resolveTaskRef', () => {
const tasks = [
{ id: 'task-1', content: 'Buy milk' },
Expand Down Expand Up @@ -112,6 +137,23 @@ describe('resolveTaskRef', () => {

await expect(resolveTaskRef(api, 'nonexistent')).rejects.toThrow('not found')
})

it('hints about id: prefix when ref looks like a raw ID', async () => {
const api = createMockApi({
getTasks: vi.fn().mockResolvedValue({ results: tasks }),
})

await expect(resolveTaskRef(api, '6fmg66Fr27R59RPg')).rejects.toThrow('id:6fmg66Fr27R59RPg')
})

it('does not hint about id: prefix for plain name searches', async () => {
const api = createMockApi({
getTasks: vi.fn().mockResolvedValue({ results: tasks }),
})

const error = await resolveTaskRef(api, 'nonexistent').catch((e: Error) => e)
expect(error.message).not.toContain('id:nonexistent')
})
})

describe('resolveProjectRef', () => {
Expand Down
36 changes: 32 additions & 4 deletions src/lib/refs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ export function extractId(ref: string): string {
return ref.slice(3)
}

export function looksLikeRawId(ref: string): boolean {
if (ref.includes(' ')) return false
return /^\d+$/.test(ref) || (/[a-zA-Z]/.test(ref) && /\d/.test(ref))
}

function idPrefixHint(ref: string): string[] {
if (!looksLikeRawId(ref)) return []
return [`If "${ref}" is an ID, prefix it: id:${ref}`]
}

export function requireIdRef(ref: string, entityName: string): string {
if (!isIdRef(ref)) {
throw new Error(
Expand Down Expand Up @@ -52,7 +62,11 @@ async function resolveRef<T extends { id: string }>(
}

throw new Error(
formatError(`${entityType.toUpperCase()}_NOT_FOUND`, `${entityType} "${ref}" not found.`),
formatError(
`${entityType.toUpperCase()}_NOT_FOUND`,
`${entityType} "${ref}" not found.`,
idPrefixHint(ref),
),
)
}

Expand Down Expand Up @@ -118,7 +132,13 @@ export async function resolveSectionId(
)
}

throw new Error(formatError('SECTION_NOT_FOUND', `Section "${ref}" not found in project.`))
throw new Error(
formatError(
'SECTION_NOT_FOUND',
`Section "${ref}" not found in project.`,
idPrefixHint(ref),
),
)
}

export async function resolveParentTaskId(
Expand Down Expand Up @@ -167,7 +187,13 @@ export async function resolveParentTaskId(
)
}

throw new Error(formatError('PARENT_NOT_FOUND', `Parent task "${ref}" not found in project.`))
throw new Error(
formatError(
'PARENT_NOT_FOUND',
`Parent task "${ref}" not found in project.`,
idPrefixHint(ref),
),
)
}

export async function resolveWorkspaceRef(ref: string): Promise<Workspace> {
Expand Down Expand Up @@ -198,5 +224,7 @@ export async function resolveWorkspaceRef(ref: string): Promise<Workspace> {
)
}

throw new Error(formatError('WORKSPACE_NOT_FOUND', `Workspace "${ref}" not found.`))
throw new Error(
formatError('WORKSPACE_NOT_FOUND', `Workspace "${ref}" not found.`, idPrefixHint(ref)),
)
}