@youdotcom-oss/api 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +36 -36
  2. package/bin/cli.js +233 -400
  3. package/package.json +1 -1
  4. package/src/cli.ts +92 -21
  5. package/src/contents/contents.schemas.ts +8 -7
  6. package/src/contents/tests/contents.request.spec.ts +109 -0
  7. package/src/contents/tests/contents.schema-validation.spec.ts +75 -0
  8. package/src/deep-search/deep-search.schemas.ts +48 -0
  9. package/src/deep-search/deep-search.utils.ts +79 -0
  10. package/src/deep-search/tests/deep-search.request.spec.ts +109 -0
  11. package/src/deep-search/tests/deep-search.schema-validation.spec.ts +71 -0
  12. package/src/deep-search/tests/deep-search.utils.docker.ts +139 -0
  13. package/src/main.ts +4 -3
  14. package/src/search/search.schemas.ts +65 -6
  15. package/src/search/search.utils.ts +6 -28
  16. package/src/search/tests/search.request.spec.ts +122 -0
  17. package/src/search/tests/search.schema-validation.spec.ts +152 -0
  18. package/src/search/tests/{search.utils.spec.ts → search.utils.docker.ts} +0 -10
  19. package/src/shared/api.constants.ts +1 -1
  20. package/src/shared/check-response-for-errors.ts +1 -1
  21. package/src/shared/command-runner.ts +95 -0
  22. package/src/shared/dry-run-utils.ts +141 -0
  23. package/src/shared/tests/command-runner.spec.ts +210 -0
  24. package/src/shared/use-get-user-agents.ts +1 -1
  25. package/src/commands/contents.ts +0 -52
  26. package/src/commands/express.ts +0 -52
  27. package/src/commands/search.ts +0 -52
  28. package/src/express/express.schemas.ts +0 -85
  29. package/src/express/express.utils.ts +0 -113
  30. package/src/express/tests/express.utils.spec.ts +0 -83
  31. /package/src/contents/tests/{contents.utils.spec.ts → contents.utils.docker.ts} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youdotcom-oss/api",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "You.com API client with bundled CLI for agents supporting Agent Skills",
5
5
  "license": "MIT",
6
6
  "engines": {
package/src/cli.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Commands:
6
6
  * search <query> - Search the web with You.com
7
- * express <input> - Get AI answers with web context
7
+ * deep-search <query> - Perform deep research with comprehensive answers
8
8
  * contents <url> [url...] - Extract content from URLs
9
9
  *
10
10
  * Options:
@@ -14,10 +14,17 @@
14
14
  * --help, -h - Show help
15
15
  */
16
16
  import { parseArgs } from 'node:util'
17
+ import type * as z from 'zod'
17
18
  import packageJson from '../package.json' with { type: 'json' }
18
- import { contentsCommand } from './commands/contents.ts'
19
- import { expressCommand } from './commands/express.ts'
20
- import { searchCommand } from './commands/search.ts'
19
+ import { ContentsQuerySchema } from './contents/contents.schemas.ts'
20
+ import { fetchContents } from './contents/contents.utils.ts'
21
+ import { DeepSearchQuerySchema } from './deep-search/deep-search.schemas.ts'
22
+ import { callDeepSearch } from './deep-search/deep-search.utils.ts'
23
+ import { SearchQuerySchema } from './search/search.schemas.ts'
24
+ import { fetchSearchResults } from './search/search.utils.ts'
25
+ import type { GetUserAgent } from './shared/api.types.ts'
26
+ import { type CommandConfig, runCommand } from './shared/command-runner.ts'
27
+ import { buildContentsRequest, buildDeepSearchRequest, buildSearchRequest } from './shared/dry-run-utils.ts'
21
28
  import { generateErrorReportLink } from './shared/generate-error-report-link.ts'
22
29
  import { useGetUserAgent } from './shared/use-get-user-agents.ts'
23
30
 
@@ -35,7 +42,7 @@ Usage: ydc <command> --json <json> [options]
35
42
 
36
43
  Commands:
37
44
  search Search the web with You.com
38
- express Get AI answers with web context
45
+ deep-search Perform deep research with comprehensive answers
39
46
  contents Extract content from URLs
40
47
 
41
48
  Global Options:
@@ -43,6 +50,7 @@ Global Options:
43
50
  --api-key <key> You.com API key (overrides YDC_API_KEY)
44
51
  --client <name> Client name for tracking and debugging
45
52
  --schema Output JSON schema for what can be passed to --json
53
+ --dry-run Show request details without making API call
46
54
  --help, -h Show this help
47
55
 
48
56
  Environment Variables:
@@ -55,9 +63,10 @@ Output Format:
55
63
 
56
64
  Examples:
57
65
  ydc search --json '{"query":"AI developments"}' --client Openclaw
58
- ydc express --json '{"input":"What happened today?","tools":[{"type":"web_search"}]}' --client MyAgent
66
+ ydc deep-search --json '{"query":"What are the latest breakthroughs in AI?","search_effort":"high"}' --client MyAgent
59
67
  ydc contents --json '{"urls":["https://example.com"],"formats":["markdown"]}'
60
68
  ydc search --schema # Get JSON schema for search --json input
69
+ ydc search --json '{"query":"AI"}' --dry-run # Inspect request without API call
61
70
  ydc search --json '{"query":"AI"}' | jq '.data.results.web[0].title'
62
71
 
63
72
  More info: https://github.com/youdotcom-oss/dx-toolkit/tree/main/packages/api
@@ -65,22 +74,84 @@ More info: https://github.com/youdotcom-oss/dx-toolkit/tree/main/packages/api
65
74
  process.exit(command ? 0 : 2)
66
75
  }
67
76
 
77
+ // Command configuration map
78
+ const commands = {
79
+ search: {
80
+ schema: SearchQuerySchema,
81
+ handler: ({
82
+ input,
83
+ YDC_API_KEY,
84
+ getUserAgent,
85
+ }: {
86
+ input: z.infer<typeof SearchQuerySchema>
87
+ YDC_API_KEY: string
88
+ getUserAgent: GetUserAgent
89
+ }) => fetchSearchResults({ searchQuery: input, YDC_API_KEY, getUserAgent }),
90
+ dryRunHandler: ({
91
+ input,
92
+ YDC_API_KEY,
93
+ getUserAgent,
94
+ }: {
95
+ input: z.infer<typeof SearchQuerySchema>
96
+ YDC_API_KEY: string
97
+ getUserAgent: GetUserAgent
98
+ }) => buildSearchRequest({ searchQuery: input, YDC_API_KEY, getUserAgent }),
99
+ },
100
+ 'deep-search': {
101
+ schema: DeepSearchQuerySchema,
102
+ handler: ({
103
+ input,
104
+ YDC_API_KEY,
105
+ getUserAgent,
106
+ }: {
107
+ input: z.infer<typeof DeepSearchQuerySchema>
108
+ YDC_API_KEY: string
109
+ getUserAgent: GetUserAgent
110
+ }) => callDeepSearch({ deepSearchQuery: input, YDC_API_KEY, getUserAgent }),
111
+ dryRunHandler: ({
112
+ input,
113
+ YDC_API_KEY,
114
+ getUserAgent,
115
+ }: {
116
+ input: z.infer<typeof DeepSearchQuerySchema>
117
+ YDC_API_KEY: string
118
+ getUserAgent: GetUserAgent
119
+ }) => buildDeepSearchRequest({ deepSearchQuery: input, YDC_API_KEY, getUserAgent }),
120
+ },
121
+ contents: {
122
+ schema: ContentsQuerySchema,
123
+ handler: ({
124
+ input,
125
+ YDC_API_KEY,
126
+ getUserAgent,
127
+ }: {
128
+ input: z.infer<typeof ContentsQuerySchema>
129
+ YDC_API_KEY: string
130
+ getUserAgent: GetUserAgent
131
+ }) => fetchContents({ contentsQuery: input, YDC_API_KEY, getUserAgent }),
132
+ dryRunHandler: ({
133
+ input,
134
+ YDC_API_KEY,
135
+ getUserAgent,
136
+ }: {
137
+ input: z.infer<typeof ContentsQuerySchema>
138
+ YDC_API_KEY: string
139
+ getUserAgent: GetUserAgent
140
+ }) => buildContentsRequest({ contentsQuery: input, YDC_API_KEY, getUserAgent }),
141
+ },
142
+ }
143
+
144
+ // Validate command
145
+ if (!(command in commands)) {
146
+ console.error(`Unknown command: ${command}`)
147
+ console.error(`Run 'ydc --help' for usage`)
148
+ process.exit(2)
149
+ }
150
+
151
+ // Execute command
68
152
  try {
69
- switch (command) {
70
- case 'search':
71
- await searchCommand(args)
72
- break
73
- case 'express':
74
- await expressCommand(args)
75
- break
76
- case 'contents':
77
- await contentsCommand(args)
78
- break
79
- default:
80
- console.error(`Unknown command: ${command}`)
81
- console.error(`Run 'ydc --help' for usage`)
82
- process.exit(2)
83
- }
153
+ // Type assertion is safe because we validated command exists above
154
+ await runCommand(args, commands[command as keyof typeof commands] as CommandConfig<unknown, unknown>)
84
155
  process.exit(0)
85
156
  } catch (error) {
86
157
  console.error(error)
@@ -21,20 +21,21 @@ export type ContentsQuery = z.infer<typeof ContentsQuerySchema>
21
21
 
22
22
  /**
23
23
  * Schema for a single content item in the API response
24
+ * Based on OpenAPI spec: https://you.com/specs/openapi_contents.yaml
24
25
  */
25
26
  const ContentsItemSchema = z.object({
26
27
  url: z.string().describe('URL'),
27
- title: z.string().optional().describe('Title'),
28
- html: z.string().optional().describe('HTML content'),
29
- markdown: z.string().optional().describe('Markdown content'),
28
+ title: z.string().optional().describe('Title (optional in actual API responses)'),
29
+ html: z.string().nullable().optional().describe('HTML content'),
30
+ markdown: z.string().nullable().optional().describe('Markdown content'),
30
31
  metadata: z
31
32
  .object({
32
- jsonld: z.array(z.record(z.string(), z.unknown())).optional().describe('JSON-LD structured data (Schema.org)'),
33
- opengraph: z.record(z.string(), z.string()).optional().describe('OpenGraph meta tags'),
34
- twitter: z.record(z.string(), z.string()).optional().describe('Twitter Card metadata'),
33
+ site_name: z.string().nullable().optional().describe('OpenGraph site name'),
34
+ favicon_url: z.string().describe('Favicon URL'),
35
35
  })
36
+ .nullable()
36
37
  .optional()
37
- .describe('Structured metadata when available'),
38
+ .describe('Page metadata (only when metadata format requested)'),
38
39
  })
39
40
 
40
41
  /**
@@ -0,0 +1,109 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { CONTENTS_API_URL } from '../../shared/api.constants.ts'
3
+ import { buildContentsRequest } from '../../shared/dry-run-utils.ts'
4
+
5
+ describe('buildContentsRequest', () => {
6
+ const getUserAgent = () => 'test-agent'
7
+ const YDC_API_KEY = 'test-key'
8
+
9
+ test('builds basic contents request with markdown format', () => {
10
+ const request = buildContentsRequest({
11
+ contentsQuery: { urls: ['https://example.com'] },
12
+ YDC_API_KEY,
13
+ getUserAgent,
14
+ })
15
+
16
+ expect(request.url).toBe(CONTENTS_API_URL)
17
+ expect(request.method).toBe('POST')
18
+ expect(request.headers['X-API-Key']).toBe('test-key')
19
+ expect(request.headers['Content-Type']).toBe('application/json')
20
+ expect(request.headers['User-Agent']).toBe('test-agent')
21
+
22
+ const body = JSON.parse(request.body!)
23
+ expect(body.urls).toEqual(['https://example.com'])
24
+ expect(body.formats).toEqual(['markdown'])
25
+ })
26
+
27
+ test('builds request with multiple URLs', () => {
28
+ const request = buildContentsRequest({
29
+ contentsQuery: {
30
+ urls: ['https://a.com', 'https://b.com', 'https://c.com'],
31
+ },
32
+ YDC_API_KEY,
33
+ getUserAgent,
34
+ })
35
+
36
+ const body = JSON.parse(request.body!)
37
+ expect(body.urls).toEqual(['https://a.com', 'https://b.com', 'https://c.com'])
38
+ })
39
+
40
+ test('builds request with multiple formats', () => {
41
+ const request = buildContentsRequest({
42
+ contentsQuery: {
43
+ urls: ['https://example.com'],
44
+ formats: ['html', 'markdown', 'metadata'],
45
+ },
46
+ YDC_API_KEY,
47
+ getUserAgent,
48
+ })
49
+
50
+ const body = JSON.parse(request.body!)
51
+ expect(body.formats).toEqual(['html', 'markdown', 'metadata'])
52
+ })
53
+
54
+ test('builds request with deprecated format parameter', () => {
55
+ const request = buildContentsRequest({
56
+ contentsQuery: {
57
+ urls: ['https://example.com'],
58
+ format: 'html',
59
+ },
60
+ YDC_API_KEY,
61
+ getUserAgent,
62
+ })
63
+
64
+ const body = JSON.parse(request.body!)
65
+ expect(body.formats).toEqual(['html'])
66
+ })
67
+
68
+ test('prefers formats array over deprecated format parameter', () => {
69
+ const request = buildContentsRequest({
70
+ contentsQuery: {
71
+ urls: ['https://example.com'],
72
+ formats: ['markdown', 'metadata'],
73
+ format: 'html',
74
+ },
75
+ YDC_API_KEY,
76
+ getUserAgent,
77
+ })
78
+
79
+ const body = JSON.parse(request.body!)
80
+ expect(body.formats).toEqual(['markdown', 'metadata'])
81
+ })
82
+
83
+ test('includes crawl_timeout when provided', () => {
84
+ const request = buildContentsRequest({
85
+ contentsQuery: {
86
+ urls: ['https://example.com'],
87
+ crawl_timeout: 30,
88
+ },
89
+ YDC_API_KEY,
90
+ getUserAgent,
91
+ })
92
+
93
+ const body = JSON.parse(request.body!)
94
+ expect(body.crawl_timeout).toBe(30)
95
+ })
96
+
97
+ test('omits crawl_timeout when not provided', () => {
98
+ const request = buildContentsRequest({
99
+ contentsQuery: {
100
+ urls: ['https://example.com'],
101
+ },
102
+ YDC_API_KEY,
103
+ getUserAgent,
104
+ })
105
+
106
+ const body = JSON.parse(request.body!)
107
+ expect(body.crawl_timeout).toBeUndefined()
108
+ })
109
+ })
@@ -0,0 +1,75 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { ContentsQuerySchema } from '../contents.schemas.ts'
3
+
4
+ describe('ContentsQuerySchema OpenAPI validation', () => {
5
+ test('accepts valid contents queries', () => {
6
+ const validQueries = [
7
+ { urls: ['https://example.com'] },
8
+ { urls: ['https://example.com'], formats: ['markdown'] },
9
+ { urls: ['https://example.com'], formats: ['html', 'markdown'] },
10
+ { urls: ['https://example.com'], formats: ['markdown', 'metadata'] },
11
+ { urls: ['https://example.com'], formats: ['html', 'markdown', 'metadata'] },
12
+ { urls: ['https://example.com'], format: 'html' }, // Deprecated but still supported
13
+ { urls: ['https://example.com'], crawl_timeout: 30 },
14
+ { urls: ['https://a.com', 'https://b.com', 'https://c.com'], formats: ['markdown'] },
15
+ ]
16
+
17
+ for (const validQuery of validQueries) {
18
+ expect(() => ContentsQuerySchema.parse(validQuery)).not.toThrow()
19
+ }
20
+ })
21
+
22
+ test('rejects invalid contents queries', () => {
23
+ const invalidQueries = [
24
+ {}, // Missing urls
25
+ { urls: [] }, // Empty urls array
26
+ { urls: ['not-a-url'] }, // Invalid URL
27
+ { urls: ['https://example.com'], formats: ['invalid'] }, // Invalid format
28
+ { urls: ['https://example.com'], crawl_timeout: 0 }, // Timeout too low
29
+ { urls: ['https://example.com'], crawl_timeout: 61 }, // Timeout too high
30
+ ]
31
+
32
+ for (const invalidQuery of invalidQueries) {
33
+ expect(() => ContentsQuerySchema.parse(invalidQuery)).toThrow()
34
+ }
35
+ })
36
+
37
+ test('accepts metadata format', () => {
38
+ const query = {
39
+ urls: ['https://example.com'],
40
+ formats: ['metadata'],
41
+ }
42
+
43
+ expect(() => ContentsQuerySchema.parse(query)).not.toThrow()
44
+ })
45
+
46
+ test('accepts all format combinations', () => {
47
+ const formatCombinations = [
48
+ ['html'],
49
+ ['markdown'],
50
+ ['metadata'],
51
+ ['html', 'markdown'],
52
+ ['html', 'metadata'],
53
+ ['markdown', 'metadata'],
54
+ ['html', 'markdown', 'metadata'],
55
+ ]
56
+
57
+ for (const formats of formatCombinations) {
58
+ expect(() => ContentsQuerySchema.parse({ urls: ['https://example.com'], formats })).not.toThrow()
59
+ }
60
+ })
61
+
62
+ test('crawl_timeout validation', () => {
63
+ // Valid timeouts (1-60)
64
+ const validTimeouts = [1, 30, 60]
65
+ for (const timeout of validTimeouts) {
66
+ expect(() => ContentsQuerySchema.parse({ urls: ['https://example.com'], crawl_timeout: timeout })).not.toThrow()
67
+ }
68
+
69
+ // Invalid timeouts
70
+ const invalidTimeouts = [0, -1, 61, 100]
71
+ for (const timeout of invalidTimeouts) {
72
+ expect(() => ContentsQuerySchema.parse({ urls: ['https://example.com'], crawl_timeout: timeout })).toThrow()
73
+ }
74
+ })
75
+ })
@@ -0,0 +1,48 @@
1
+ import * as z from 'zod'
2
+
3
+ /**
4
+ * Search effort levels for deep-search API
5
+ * Controls computation budget and response time
6
+ */
7
+ export const SearchEffortSchema = z.enum(['low', 'medium', 'high']).describe('Search effort level')
8
+
9
+ /**
10
+ * Input schema for deep-search API
11
+ * Based on OpenAPI spec: https://docs.you.com/api-reference/deep-search/v1-deep_search
12
+ *
13
+ * @public
14
+ */
15
+ export const DeepSearchQuerySchema = z.object({
16
+ query: z
17
+ .string()
18
+ .min(1, 'Query is required')
19
+ .describe('The research question or complex query requiring in-depth investigation and multi-step reasoning'),
20
+ search_effort: SearchEffortSchema.optional()
21
+ .default('medium')
22
+ .describe('Computation budget: low (<30s), medium (<60s, default), high (<300s)'),
23
+ })
24
+
25
+ export type DeepSearchQuery = z.infer<typeof DeepSearchQuerySchema>
26
+
27
+ /**
28
+ * Schema for a single source in the deep-search response
29
+ *
30
+ * @public
31
+ */
32
+ const DeepSearchSourceSchema = z.object({
33
+ url: z.string().describe('Source webpage URL'),
34
+ title: z.string().describe('Source webpage title'),
35
+ snippets: z.array(z.string()).describe('Relevant excerpts from the source page used in generating the answer'),
36
+ })
37
+
38
+ /**
39
+ * Response schema for deep-search API
40
+ *
41
+ * @public
42
+ */
43
+ export const DeepSearchResponseSchema = z.object({
44
+ answer: z.string().describe('Comprehensive response with inline citations, formatted in Markdown'),
45
+ results: z.array(DeepSearchSourceSchema).describe('List of web sources used to generate the answer'),
46
+ })
47
+
48
+ export type DeepSearchResponse = z.infer<typeof DeepSearchResponseSchema>
@@ -0,0 +1,79 @@
1
+ import type * as z from 'zod'
2
+ import { DEEP_SEARCH_API_URL } from '../shared/api.constants.ts'
3
+ import type { GetUserAgent } from '../shared/api.types.ts'
4
+ import { checkResponseForErrors } from '../shared/check-response-for-errors.ts'
5
+ import { type DeepSearchQuery, DeepSearchResponseSchema } from './deep-search.schemas.ts'
6
+
7
+ /**
8
+ * Perform deep research using You.com Deep Search API
9
+ *
10
+ * @param params - Deep search query parameters
11
+ * @returns Deep search response with comprehensive answer and sources
12
+ *
13
+ * @public
14
+ */
15
+ export const callDeepSearch = async ({
16
+ deepSearchQuery,
17
+ YDC_API_KEY = process.env.YDC_API_KEY,
18
+ getUserAgent,
19
+ }: {
20
+ deepSearchQuery: DeepSearchQuery
21
+ YDC_API_KEY?: string
22
+ getUserAgent: GetUserAgent
23
+ }) => {
24
+ if (!YDC_API_KEY) {
25
+ throw new Error('YDC_API_KEY is required for Deep Search API')
26
+ }
27
+
28
+ const response = await fetch(DEEP_SEARCH_API_URL, {
29
+ method: 'POST',
30
+ headers: new Headers({
31
+ 'X-API-Key': YDC_API_KEY,
32
+ 'Content-Type': 'application/json',
33
+ 'User-Agent': getUserAgent(),
34
+ }),
35
+ body: JSON.stringify(deepSearchQuery),
36
+ })
37
+
38
+ await checkResponseForErrors(response)
39
+ const data = await response.json()
40
+
41
+ return DeepSearchResponseSchema.parse(data)
42
+ }
43
+
44
+ /**
45
+ * Format deep-search response for display
46
+ * Returns markdown-formatted text with answer and sources
47
+ *
48
+ * @param response - Deep search API response
49
+ * @returns Formatted markdown string
50
+ *
51
+ * @public
52
+ */
53
+ export const formatDeepSearchResponse = (response: z.infer<typeof DeepSearchResponseSchema>): string => {
54
+ const parts: string[] = []
55
+
56
+ // Add the comprehensive answer
57
+ parts.push('# Answer\n')
58
+ parts.push(response.answer)
59
+ parts.push('\n')
60
+
61
+ // Add sources section
62
+ if (response.results && response.results.length > 0) {
63
+ parts.push('\n## Sources\n')
64
+
65
+ for (const [index, source] of response.results.entries()) {
66
+ parts.push(`\n### ${index + 1}. ${source.title}\n`)
67
+ parts.push(`**URL:** ${source.url}\n`)
68
+
69
+ if (source.snippets && source.snippets.length > 0) {
70
+ parts.push('\n**Key Excerpts:**\n')
71
+ for (const snippet of source.snippets) {
72
+ parts.push(`> ${snippet}\n`)
73
+ }
74
+ }
75
+ }
76
+ }
77
+
78
+ return parts.join('\n')
79
+ }
@@ -0,0 +1,109 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { DEEP_SEARCH_API_URL } from '../../shared/api.constants.ts'
3
+ import { buildDeepSearchRequest } from '../../shared/dry-run-utils.ts'
4
+
5
+ describe('buildDeepSearchRequest', () => {
6
+ const getUserAgent = () => 'test-agent'
7
+ const YDC_API_KEY = 'test-key'
8
+
9
+ test('builds basic deep-search request with query only', () => {
10
+ const request = buildDeepSearchRequest({
11
+ deepSearchQuery: { query: 'What is AI?' },
12
+ YDC_API_KEY,
13
+ getUserAgent,
14
+ })
15
+
16
+ expect(request.url).toBe(DEEP_SEARCH_API_URL)
17
+ expect(request.method).toBe('POST')
18
+ expect(request.headers['X-API-Key']).toBe('test-key')
19
+ expect(request.headers['Content-Type']).toBe('application/json')
20
+ expect(request.headers['User-Agent']).toBe('test-agent')
21
+
22
+ const body = JSON.parse(request.body!)
23
+ expect(body.query).toBe('What is AI?')
24
+ // search_effort not in body when not provided (default applied by schema validation, not dry-run)
25
+ expect(body.search_effort).toBeUndefined()
26
+ })
27
+
28
+ test('builds request with explicit medium search effort', () => {
29
+ const request = buildDeepSearchRequest({
30
+ deepSearchQuery: { query: 'What is AI?', search_effort: 'medium' },
31
+ YDC_API_KEY,
32
+ getUserAgent,
33
+ })
34
+
35
+ const body = JSON.parse(request.body!)
36
+ expect(body.query).toBe('What is AI?')
37
+ expect(body.search_effort).toBe('medium')
38
+ })
39
+
40
+ test('builds request with low search effort', () => {
41
+ const request = buildDeepSearchRequest({
42
+ deepSearchQuery: {
43
+ query: 'Quick explanation of JWT',
44
+ search_effort: 'low',
45
+ },
46
+ YDC_API_KEY,
47
+ getUserAgent,
48
+ })
49
+
50
+ const body = JSON.parse(request.body!)
51
+ expect(body.query).toBe('Quick explanation of JWT')
52
+ expect(body.search_effort).toBe('low')
53
+ })
54
+
55
+ test('builds request with high search effort', () => {
56
+ const request = buildDeepSearchRequest({
57
+ deepSearchQuery: {
58
+ query: 'Comprehensive analysis of climate change impacts',
59
+ search_effort: 'high',
60
+ },
61
+ YDC_API_KEY,
62
+ getUserAgent,
63
+ })
64
+
65
+ const body = JSON.parse(request.body!)
66
+ expect(body.query).toBe('Comprehensive analysis of climate change impacts')
67
+ expect(body.search_effort).toBe('high')
68
+ })
69
+
70
+ test('builds request with complex research question', () => {
71
+ const complexQuery = `What are the key differences between microservices and monolithic architecture?
72
+ Include pros and cons of each approach, best use cases, and migration strategies.`
73
+
74
+ const request = buildDeepSearchRequest({
75
+ deepSearchQuery: {
76
+ query: complexQuery,
77
+ search_effort: 'high',
78
+ },
79
+ YDC_API_KEY,
80
+ getUserAgent,
81
+ })
82
+
83
+ const body = JSON.parse(request.body!)
84
+ expect(body.query).toBe(complexQuery)
85
+ expect(body.search_effort).toBe('high')
86
+ })
87
+
88
+ test('uses correct API URL', () => {
89
+ const request = buildDeepSearchRequest({
90
+ deepSearchQuery: { query: 'test' },
91
+ YDC_API_KEY,
92
+ getUserAgent,
93
+ })
94
+
95
+ expect(request.url).toBe('https://api.you.com/v1/deep_search')
96
+ })
97
+
98
+ test('includes all required headers', () => {
99
+ const request = buildDeepSearchRequest({
100
+ deepSearchQuery: { query: 'test' },
101
+ YDC_API_KEY: 'my-api-key',
102
+ getUserAgent: () => 'CustomAgent/1.0',
103
+ })
104
+
105
+ expect(request.headers['X-API-Key']).toBe('my-api-key')
106
+ expect(request.headers['Content-Type']).toBe('application/json')
107
+ expect(request.headers['User-Agent']).toBe('CustomAgent/1.0')
108
+ })
109
+ })
@@ -0,0 +1,71 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { DeepSearchQuerySchema, SearchEffortSchema } from '../deep-search.schemas.ts'
3
+
4
+ describe('DeepSearchQuerySchema OpenAPI validation', () => {
5
+ test('accepts valid query parameters', () => {
6
+ const validQueries = [
7
+ { query: 'What is quantum computing?' },
8
+ { query: 'Explain machine learning', search_effort: 'low' },
9
+ { query: 'Latest AI developments', search_effort: 'medium' },
10
+ { query: 'Comprehensive research on climate change', search_effort: 'high' },
11
+ ]
12
+
13
+ for (const validQuery of validQueries) {
14
+ expect(() => DeepSearchQuerySchema.parse(validQuery)).not.toThrow()
15
+ }
16
+ })
17
+
18
+ test('rejects invalid query parameters', () => {
19
+ const invalidQueries = [
20
+ {}, // Missing query
21
+ { query: '' }, // Empty query
22
+ { query: 'test', search_effort: 'invalid' }, // Invalid search_effort
23
+ { query: 'test', search_effort: 'extreme' }, // Invalid effort level
24
+ ]
25
+
26
+ for (const invalidQuery of invalidQueries) {
27
+ expect(() => DeepSearchQuerySchema.parse(invalidQuery)).toThrow()
28
+ }
29
+ })
30
+
31
+ test('defaults search_effort to medium when not provided', () => {
32
+ const result = DeepSearchQuerySchema.parse({ query: 'test query' })
33
+ expect(result.search_effort).toBe('medium')
34
+ })
35
+
36
+ test('SearchEffortSchema accepts all valid effort levels', () => {
37
+ const validEfforts = ['low', 'medium', 'high']
38
+
39
+ for (const effort of validEfforts) {
40
+ expect(() => SearchEffortSchema.parse(effort)).not.toThrow()
41
+ }
42
+ })
43
+
44
+ test('SearchEffortSchema rejects invalid effort levels', () => {
45
+ const invalidEfforts = ['none', 'extreme', 'ultra', 'minimal', '']
46
+
47
+ for (const effort of invalidEfforts) {
48
+ expect(() => SearchEffortSchema.parse(effort)).toThrow()
49
+ }
50
+ })
51
+
52
+ test('accepts complex research questions', () => {
53
+ const complexQueries = [
54
+ {
55
+ query: 'What are the key differences between REST and GraphQL APIs, and when should each be used?',
56
+ search_effort: 'high',
57
+ },
58
+ {
59
+ query: 'Compare the advantages and disadvantages of microservices architecture versus monolithic architecture',
60
+ },
61
+ {
62
+ query: 'What happened in AI research during 2024? Provide a comprehensive summary with key breakthroughs.',
63
+ search_effort: 'high',
64
+ },
65
+ ]
66
+
67
+ for (const query of complexQueries) {
68
+ expect(() => DeepSearchQuerySchema.parse(query)).not.toThrow()
69
+ }
70
+ })
71
+ })