mirror of
https://github.com/schpet/linear-cli.git
synced 2026-09-14 14:26:50 +08:00
3c30f365b7
The skill learned how Linear mentions actually work; the CLI itself did not. An agent driving `linear` without it still writes `@peter` in a comment body and posts text that notifies nobody. Put the trap-avoiding rule inline on the ten commands that take a Markdown body, because a pointer alone only helps an agent that already suspects it has a gap. The full reference lives in a new `linear markdown` command, used both as its description and as what it prints: printing keeps the `+++` block unindented and copyable, and the description is what the skill-docs generator captures from `--help`. The `--json` listings say what the `url` field is for, with the team-first safeguard kept on the workspace-wide one. Help screens grow by one root row and about five lines per authoring command; parent command tables are unaffected, since cliffy renders only a description's first line there. Claude-Session: https://claude.ai/code/session_01TWeJWzhCAW61GLqv2kTpNB
163 lines
4.4 KiB
TypeScript
163 lines
4.4 KiB
TypeScript
import { assertEquals, assertStringIncludes } from "@std/assert"
|
|
import { getGraphQLClient } from "../src/utils/graphql.ts"
|
|
import { cli } from "../src/cli.ts"
|
|
import { configCommand } from "../src/commands/config.ts"
|
|
import { markdownCommand } from "../src/commands/markdown.ts"
|
|
|
|
// Regression guard for #245: `configure` is a natural name users (and the
|
|
// CLI's own help text) reach for, so it resolves to the canonical `config`
|
|
// command instead of erroring with "Unknown command".
|
|
Deno.test("cli - `configure` is an alias for the config command", () => {
|
|
assertEquals(cli.getCommand("configure"), configCommand)
|
|
})
|
|
|
|
// An exported but unregistered command type-checks and passes its own tests
|
|
// while being unreachable from the CLI, which is the whole point of the
|
|
// Markdown reference: agents have to be able to find it.
|
|
Deno.test("cli - `markdown` reference is reachable as a top-level command", () => {
|
|
assertEquals(cli.getCommand("markdown"), markdownCommand)
|
|
})
|
|
|
|
// Mock fetch function for testing
|
|
const originalFetch = globalThis.fetch
|
|
|
|
function mockFetch(response: Response) {
|
|
globalThis.fetch = () => Promise.resolve(response)
|
|
}
|
|
|
|
function restoreFetch() {
|
|
globalThis.fetch = originalFetch
|
|
}
|
|
|
|
// Mock environment variable for API key
|
|
const originalEnv = Deno.env.get
|
|
|
|
function mockEnv() {
|
|
Deno.env.get = (key: string) => {
|
|
if (key === "LINEAR_API_KEY") return "test-api-key"
|
|
return originalEnv(key)
|
|
}
|
|
}
|
|
|
|
function restoreEnv() {
|
|
Deno.env.get = originalEnv
|
|
}
|
|
|
|
Deno.test("getGraphQLClient handles authentication errors", async () => {
|
|
const jsonErrorResponse = {
|
|
errors: [{
|
|
message: "Authentication failed",
|
|
extensions: {
|
|
code: "INVALID_API_KEY",
|
|
},
|
|
}],
|
|
}
|
|
|
|
const mockResponse = new Response(
|
|
JSON.stringify(jsonErrorResponse),
|
|
{
|
|
status: 401,
|
|
statusText: "Unauthorized",
|
|
headers: {
|
|
"content-type": "application/json",
|
|
},
|
|
},
|
|
)
|
|
|
|
mockFetch(mockResponse)
|
|
mockEnv()
|
|
|
|
try {
|
|
const client = getGraphQLClient()
|
|
await client.request("query { viewer { id } }", {})
|
|
throw new Error("Expected GraphQL client to throw an error")
|
|
} catch (error) {
|
|
const errorMessage = (error as Error).message
|
|
|
|
// graphql-request 7.4+ parses the response body even for non-2xx,
|
|
// so the error message contains the actual API error
|
|
assertStringIncludes(errorMessage, "Authentication failed")
|
|
} finally {
|
|
restoreFetch()
|
|
restoreEnv()
|
|
}
|
|
})
|
|
|
|
Deno.test("getGraphQLClient handles HTTP errors", async () => {
|
|
const htmlErrorResponse = `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>500 Internal Server Error</title>
|
|
</head>
|
|
<body>
|
|
<h1>Internal Server Error</h1>
|
|
<p>The server encountered an unexpected condition that prevented it from fulfilling the request.</p>
|
|
<p>Error ID: abc123def456</p>
|
|
</body>
|
|
</html>
|
|
`.trim()
|
|
|
|
const mockResponse = new Response(
|
|
htmlErrorResponse,
|
|
{
|
|
status: 500,
|
|
statusText: "Internal Server Error",
|
|
headers: {
|
|
"content-type": "text/html",
|
|
},
|
|
},
|
|
)
|
|
|
|
mockFetch(mockResponse)
|
|
mockEnv()
|
|
|
|
try {
|
|
const client = getGraphQLClient()
|
|
await client.request("query { viewer { id } }", {})
|
|
throw new Error("Expected GraphQL client to throw an error")
|
|
} catch (error) {
|
|
const errorMessage = (error as Error).message
|
|
|
|
// graphql-request will throw a ClientError for HTTP errors
|
|
// The exact format may differ, but it should contain error information
|
|
assertStringIncludes(errorMessage.toLowerCase(), "500")
|
|
} finally {
|
|
restoreFetch()
|
|
restoreEnv()
|
|
}
|
|
})
|
|
|
|
Deno.test("getGraphQLClient handles malformed JSON responses", async () => {
|
|
const malformedJsonResponse = '{"error": "Invalid JSON", "incomplete": '
|
|
|
|
const mockResponse = new Response(
|
|
malformedJsonResponse,
|
|
{
|
|
status: 400,
|
|
statusText: "Bad Request",
|
|
headers: {
|
|
"content-type": "application/json",
|
|
},
|
|
},
|
|
)
|
|
|
|
mockFetch(mockResponse)
|
|
mockEnv()
|
|
|
|
try {
|
|
const client = getGraphQLClient()
|
|
await client.request("query { viewer { id } }", {})
|
|
throw new Error("Expected GraphQL client to throw an error")
|
|
} catch (error) {
|
|
const errorMessage = (error as Error).message
|
|
|
|
// graphql-request will handle JSON parsing errors
|
|
// The exact error message may vary, but should indicate an error code
|
|
assertStringIncludes(errorMessage.toLowerCase(), "400")
|
|
} finally {
|
|
restoreFetch()
|
|
restoreEnv()
|
|
}
|
|
})
|