Skip to content

[codex] add qasphere api command#60

Draft
satvik007 wants to merge 1 commit intomainfrom
codex/qasphere-api-public-command
Draft

[codex] add qasphere api command#60
satvik007 wants to merge 1 commit intomainfrom
codex/qasphere-api-public-command

Conversation

@satvik007
Copy link
Copy Markdown
Collaborator

Summary

  • add a new manifest-driven qasphere api command tree covering the documented public API resources
  • add shared request parsing, schema validation, query serialization, and generic public API transport helpers
  • add API command tests, manifest coverage tests, fixtures, and npm scripts for manifest and opt-in e2e coverage

Why

The CLI only supported result-upload flows. This change adds a first-class public API surface with schema-backed validation and consistent JSON I/O without disturbing the existing upload commands.

Impact

Users can now call documented QA Sphere public API endpoints through the CLI with nested resource commands, positional path args, named query flags, JSON body input, and multipart file upload support.

Validation

  • npm test
  • npm run build
  • npm run typecheck
  • ./build/bin/qasphere.js api projects list
  • ./build/bin/qasphere.js api testcases create MFP --body '{"type":"standalone","folderId":1,"title":"CLI invalid priority probe","priority":"critical"}'

@claude
Copy link
Copy Markdown

claude bot commented Mar 27, 2026

Claude finished @satvik007's task in 6m 25s —— View job


PR Review Complete

  • Gather PR context and diff
  • Launch specialized review subagents
  • Aggregate findings and post review
  • Clean up outdated bot comments

Reviewed all 17 changed files across 4 dimensions (code quality, security, performance, test coverage). Posted 7 inline comments covering:

  • Dead code: buildUploadFile, applyNullDefaults, unused type fields
  • Misleading control flow: return on throwing function, duplicate parseJsonResponse branches
  • DRY violations: identical normalizeCloneRunBody/normalizeCreatePlanBody, duplicated null-defaults helpers
  • Performance note: file upload reads entire file into memory
  • Test gaps: non-JSON error paths, 204 responses, validateBodyMode branches, QAS_URL validation untested

No security issues found. The @ts-nocheck on sharedSchemasSubset.ts was already flagged by another reviewer.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive API command system for the QA Sphere CLI, allowing users to interact with various public API endpoints. The implementation includes a fetcher utility with retry logic, a command executor for argument processing, and a manifest defining endpoint metadata and Zod validation schemas. Feedback focuses on enhancing type safety by removing "@ts-nocheck" and "z.any()" usages, fixing a validation logic error in the executor, refining JSON parsing to handle non-JSON content types safely, and eliminating redundant code or layers of indirection.

Comment on lines +1 to +2
/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-nocheck
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The file uses /* eslint-disable @typescript-eslint/ban-ts-comment */ // @ts-nocheck at the top. Disabling TypeScript checks and ESLint comments for an entire file, especially one defining critical data schemas, significantly reduces type safety and the benefits of using TypeScript. It can hide potential type mismatches or incorrect schema definitions that would otherwise be caught at compile time. Review the specific type issues that necessitated these suppressions and address them directly. Aim to remove these global disable comments.

}

if (spec.bodyMode === 'file') {
validateBodyMode('none', args)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When spec.bodyMode is 'file', the function incorrectly calls validateBodyMode('none', args). The validateBodyMode function is designed to check for conflicting body input flags based on the expected bodyMode. If the bodyMode is 'file', it should validate against 'file' mode to ensure only the --file flag is used and no JSON body flags are present. Calling it with 'none' will incorrectly disallow the --file flag.

Suggested change
validateBodyMode('none', args)
validateBodyMode('file', args)

Comment on lines +66 to +68
export const IDResponseSchema = z.object({
id: z.union([z.number().int(), z.string().min(1)]),
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The IDResponseSchema and IDsResponseSchema use z.union([z.number().int().array(), z.number().int().array().array()]) which is overly permissive. Using z.any() or broad unions like this bypasses Zod's validation and TypeScript's type checking, making the schemas less effective at ensuring data integrity and leading to potential runtime errors or unexpected behavior if the actual data does not conform to an implicit structure. Replace z.any() with more specific Zod schemas that accurately reflect the expected structure and types of the data.

text: z.string().min(1),
url: z.string(),
tcaseCount: z.number().int().optional(),
integrationLink: z.any().nullable().optional(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The schema for integrationLink uses z.any().nullable().optional(). Using z.any() bypasses Zod's validation and TypeScript's type checking, making the schemas less effective at ensuring data integrity and leading to potential runtime errors or unexpected behavior if the actual data does not conform to an implicit structure. Replace z.any() with a more specific Zod schema that accurately reflects the expected structure and types of the data.

})

export const GetRunsResponseSchema = z.object({
runs: z.array(z.any()).nullable(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The schema for runs uses z.array(z.any()).nullable(). Using z.any() bypasses Zod's validation and TypeScript's type checking, making the schemas less effective at ensuring data integrity and leading to potential runtime errors or unexpected behavior if the actual data does not conform to an implicit structure. Replace z.any() with a more specific Zod schema that accurately reflects the expected structure and types of the data.

Comment on lines +29 to +30
const items = Array.isArray(value) ? value : [value]
return items.map((item, index) => parseIntegerValue(`--${option.name}[${index}]`, item))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When option.array is true, items.map is used, and parseIntegerValue is called for each item. If value itself was undefined and option.array is true, items would become [undefined]. Calling parseIntegerValue with undefined will throw an ApiValidationError. It's better to filter out undefined or null values from the items array before mapping to parseIntegerValue, or ensure parseIntegerValue can handle them gracefully (e.g., by returning undefined or null).

Suggested change
const items = Array.isArray(value) ? value : [value]
return items.map((item, index) => parseIntegerValue(`--${option.name}[${index}]`, item))
const items = Array.isArray(value) ? value : [value]
return items.filter((item) => item !== undefined && item !== null).map((item, index) => parseIntegerValue("--" + option.name + "[" + index + "]", item))

Comment on lines +243 to +251
export const buildUploadFile = async (filePath: string) => {
const fileBuffer = await readFile(filePath)
return {
formField: 'file',
fileName: basename(filePath),
blob: new Blob([fileBuffer]),
stream: createReadStream(filePath),
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The buildUploadFile function is defined but not used anywhere in the provided code. In publicApi.ts, the file upload logic is implemented directly using readFile, FormData, and Blob. Unused code can lead to confusion and maintenance overhead. Consider integrating buildUploadFile into the file upload logic in publicApi.ts or removing it if it's not intended for immediate use.

@@ -0,0 +1 @@
export * from './sharedSchemasSubset'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The file src/commands/api/sharedSchemas.ts simply re-exports everything from src/commands/api/sharedSchemasSubset.ts. This creates an unnecessary layer of indirection. It would be simpler and more direct to rename sharedSchemasSubset.ts to sharedSchemas.ts and remove this re-exporting file.

Comment on lines +28 to +35
const body = (await response.json()) as Record<string, unknown>
const message = typeof body.message === 'string' ? body.message : response.statusText
throw new Error(message)
} catch (error) {
if (error instanceof Error && error.message !== 'Unexpected end of JSON input') {
throw error
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The buildErrorFromResponse function specifically re-throws JSON parsing errors only if the message is not 'Unexpected end of JSON input'. This approach might inadvertently suppress other valid JSON parsing errors, making it harder to debug issues where the API returns malformed JSON for reasons other than an incomplete payload. It's generally better to either re-throw all parsing errors with additional context or handle them more explicitly.

])

const idStringSchema = z.string().min(1)
const dateLikeSchema = z.union([z.string(), z.date()])
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The dateLikeSchema allows both z.string() and z.date(). While serializeQueryObject handles Date objects by calling toISOString(), if a string is provided, it's used as-is. This can lead to inconsistencies if the API expects a specific date string format (e.g., ISO 8601) and a non-conforming string is passed. It's generally safer to either enforce a specific string format for dates or always convert to Date objects internally before serialization to ensure consistency. Consider enforcing ISO 8601 string format for dates in the schema, e.g., z.string().datetime(), or ensure all date inputs are converted to Date objects before being passed to the serialization logic.

Copy link
Copy Markdown

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

Overall this is a well-structured addition — the manifest-driven command tree, Zod schema validation, and composable transport layer are cleanly separated. The test coverage is solid with MSW-based integration tests, unit tests for helpers, and manifest snapshot tests.

Key issues found:

  1. Dead codebuildUploadFile and applyNullDefaults in helpers.ts are exported but never used. The createReadStream import exists only for the dead function. ApiRequestEnvelope and several interface fields (queryNullDefaults, bodyNullDefaults, examples) in types.ts are also unused.

  2. Misleading control flow in publicApi.tsreturn buildErrorFromResponse(response) on a function that always throws is confusing. The parseJsonResponse function has two identical branches that can be collapsed. The error message string comparison ("Unexpected end of JSON input") is fragile across runtimes.

  3. DRY violationsnormalizeCloneRunBody and normalizeCreatePlanBody in manifest.ts are identical. applyNullDefaults in helpers.ts duplicates withNulls in manifest.ts.

  4. Test gaps — Non-JSON error responses, 204 No Content path, validateBodyMode branch coverage, serializeQueryObject edge cases (Dates, nulls in arrays), and QAS_URL validation are all untested.

No security issues of concern. The @ts-nocheck on sharedSchemasSubset.ts was already flagged by another reviewer — strongly agree that should be addressed.

body: formData,
})
if (!response.ok) {
return buildErrorFromResponse(response)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildErrorFromResponse always throws, so return here (and on line 92) is dead code that misleads readers into thinking a value is returned. Additionally, the catch block on line 31-34 matches error messages by the exact string "Unexpected end of JSON input", which is fragile across JS runtimes. Consider matching on SyntaxError type instead.

Suggested change
return buildErrorFromResponse(response)
await buildErrorFromResponse(response)

And similarly for line 91-92:

	await buildErrorFromResponse(response)

Comment on lines +42 to +54
const parseJsonResponse = async (response: Response) => {
if (response.status === 204) {
return null
}

const contentType = response.headers.get('content-type') ?? ''
if (!contentType.includes('application/json')) {
const text = await response.text()
return text ? JSON.parse(text) : null
}

const text = await response.text()
return text ? JSON.parse(text) : null
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both the if branch (non-JSON content type) and the fallthrough (JSON content type) execute identical code: response.text() then JSON.parse. The content-type check serves no purpose. This can be simplified to:

const parseJsonResponse = async (response: Response) => {
	if (response.status === 204) {
		return null
	}
	const text = await response.text()
	return text ? JSON.parse(text) : null
}

Also, for non-JSON content types, calling JSON.parse will throw an unhelpful SyntaxError if the server returns HTML (e.g. a proxy error page). Consider wrapping in a try/catch with a descriptive error message for that case.

Comment on lines +243 to +251
export const buildUploadFile = async (filePath: string) => {
const fileBuffer = await readFile(filePath)
return {
formField: 'file',
fileName: basename(filePath),
blob: new Blob([fileBuffer]),
stream: createReadStream(filePath),
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildUploadFile is exported but never imported anywhere in the codebase. File upload is handled directly in publicApi.ts with its own readFile + Blob logic. The createReadStream import on line 1 is also only used by this dead function.

Consider removing both buildUploadFile and the createReadStream import.

Comment on lines +130 to +144
export const applyNullDefaults = (
value: Record<string, unknown>,
keys: string[] | undefined
): Record<string, unknown> => {
if (!keys || keys.length === 0) {
return value
}
const next = { ...value }
for (const key of keys) {
if (!(key in next)) {
next[key] = null
}
}
return next
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

applyNullDefaults is exported but never called. The manifest file has its own local withNulls helper (manifest.ts:86) which does the same thing. This is a DRY violation — consider removing this dead export and, if consolidation is desired, having the manifest import a single shared implementation.

Comment on lines +125 to +139
const normalizeCloneRunBody = (value: unknown) => {
const input = asRecord(value)
if (!('description' in input)) {
input.description = ''
}
return input
}

const normalizeCreatePlanBody = (value: unknown) => {
const input = asRecord(value)
if (!('description' in input)) {
input.description = ''
}
return input
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

normalizeCloneRunBody and normalizeCreatePlanBody are identical — both spread asRecord(value) and default description to ''. Consider extracting a shared helper:

const withDefaultDescription = (value: unknown) => {
	const input = asRecord(value)
	if (!('description' in input)) {
		input.description = ''
	}
	return input
}

Comment on lines +25 to +53
export interface ApiRequestEnvelope {
query?: Record<string, unknown>
body?: unknown
}

export interface ApiEndpointSpec {
id: string
commandPath: ApiCommandPath
describe: string
method: ApiHttpMethod
pathTemplate: string
pathParams: ApiPathParamSpec[]
queryOptions?: ApiOptionSpec[]
supportsCustomFieldFilters?: boolean
bodyMode: ApiBodyMode
querySchema?: ZodTypeAny
bodySchema?: ZodTypeAny
responseSchema?: ZodTypeAny
queryNullDefaults?: string[]
bodyNullDefaults?: string[]
queryAdapter?: (value: Record<string, unknown>) => Record<string, unknown>
bodyAdapter?: (value: unknown) => unknown
requestSchemaLinks?: {
query?: string
body?: string
response?: string
}
examples?: string[]
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

queryNullDefaults, bodyNullDefaults, and examples are declared in the interface but never set on any spec in the manifest and never read at runtime. ApiRequestEnvelope (lines 25-28) is also exported but unused. Consider removing these dead fields/types to keep the interface focused.

Comment on lines +67 to +69
const fileBuffer = await readFile(request.filePath)
const formData = new FormData()
formData.append('file', new Blob([fileBuffer]), basename(request.filePath))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For file uploads, readFile loads the entire file into memory as a Buffer, then copies it into a Blob. For large files this could cause high memory pressure. Node 19.8+ has openAsBlob() which avoids buffering the entire file. Since the project supports Node 18+, this would need a fallback, but it may be worth noting as a future improvement or documenting a practical file size limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant