Files
Peter Schilling 3c30f365b7 Teach Linear Markdown to agents without the skill
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
2026-09-01 16:04:26 -07:00

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()
}
})