From a9261f8853650044c7e946ae28123b86c1ce69aa Mon Sep 17 00:00:00 2001 From: Joseph Date: Wed, 24 Jun 2026 17:28:54 -0400 Subject: [PATCH 1/2] Guard document updates with comments --- README.md | 5 +- skills/linear-cli/references/document.md | 26 ++-- src/commands/document/document-update.ts | 66 +++++++-- src/commands/document/document-view.ts | 44 ++++++ .../document-update.test.ts.snap | 31 +++- .../__snapshots__/document-view.test.ts.snap | 29 +++- .../commands/document/document-update.test.ts | 136 ++++++++++++++++++ test/commands/document/document-view.test.ts | 54 +++++++ 8 files changed, 359 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 043ee6d2..41de9936 100644 --- a/README.md +++ b/README.md @@ -200,7 +200,7 @@ linear document list --json # output as JSON linear document view # view document rendered in terminal linear document view --raw # output raw markdown (for piping) linear document view --web # open in browser -linear document view --json # output as JSON +linear document view --json # output as JSON, including document comments # create a document linear document create --title "My Doc" --content "# Hello" # inline content @@ -213,6 +213,7 @@ cat spec.md | linear document create --title "Spec" # from std linear document update --title "New Title" # update title linear document update --content-file ./updated.md # update content linear document update --edit # open in $EDITOR +linear document update --content-file ./updated.md --force # bypass comment-anchor guard # delete a document linear document delete # soft delete (move to trash) @@ -220,6 +221,8 @@ linear document delete --permanent # permanent delete linear document delete --bulk # bulk delete ``` +content updates are refused by default when a document has Linear document comments, because replacing markdown can detach or hide inline comment anchors. review comments first, then rerun with `--force` if you intentionally want to replace the content anyway. + ### other commands ```bash diff --git a/skills/linear-cli/references/document.md b/skills/linear-cli/references/document.md index 6d6c7e08..3facef40 100644 --- a/skills/linear-cli/references/document.md +++ b/skills/linear-cli/references/document.md @@ -61,11 +61,12 @@ Description: Options: - -h, --help - Show this help. - --workspace - Target workspace (uses credentials) - --raw - Output raw markdown without rendering - -w, --web - Open document in browser - --json - Output full document as JSON + -h, --help - Show this help. + --workspace - Target workspace (uses credentials) + --raw - Output raw markdown without rendering + -w, --web - Open document in browser + --json - Output full document as JSON + --no-download - Keep remote URLs instead of downloading files ``` ### create @@ -105,13 +106,14 @@ Description: Options: - -h, --help - Show this help. - --workspace - Target workspace (uses credentials) - -t, --title - New title for the document - -c, --content <content> - New markdown content (inline) - -f, --content-file <path> - Read new content from file - --icon <icon> - New icon (emoji) - -e, --edit - Open current content in $EDITOR for editing + -h, --help - Show this help. + --workspace <slug> - Target workspace (uses credentials) + -t, --title <title> - New title for the document + -c, --content <content> - New markdown content (inline) + -f, --content-file <path> - Read new content from file + --icon <icon> - New icon (emoji) + -e, --edit - Open current content in $EDITOR for editing + --force - Update content even when document comments may lose inline anchors ``` ### delete diff --git a/src/commands/document/document-update.ts b/src/commands/document/document-update.ts index 4dcceabd..037dd51f 100644 --- a/src/commands/document/document-update.ts +++ b/src/commands/document/document-update.ts @@ -10,6 +10,24 @@ import { ValidationError, } from "../../utils/errors.ts" +const DocumentCommentGuard = gql(` + query DocumentCommentGuard($id: String!) { + document(id: $id) { + id + title + content + comments(first: 1) { + nodes { + id + quotedText + resolvedAt + archivedAt + } + } + } + } +`) + /** * Open editor with initial content and return the edited content */ @@ -108,9 +126,13 @@ export const updateCommand = new Command() ) .option("--icon <icon:string>", "New icon (emoji)") .option("-e, --edit", "Open current content in $EDITOR for editing") + .option( + "--force", + "Update content even when document comments may lose inline anchors", + ) .action( async ( - { title, content, contentFile, icon, edit }, + { title, content, contentFile, icon, edit, force }, documentId, ) => { try { @@ -152,17 +174,7 @@ export const updateCommand = new Command() } } else if (edit) { // Edit mode: fetch current content and open in editor - const getDocumentQuery = gql(` - query GetDocumentForEdit($id: String!) { - document(id: $id) { - id - title - content - } - } - `) - - const documentData = await client.request(getDocumentQuery, { + const documentData = await client.request(DocumentCommentGuard, { id: documentId, }) @@ -209,6 +221,36 @@ export const updateCommand = new Command() }) } + if (input.content !== undefined && !force) { + const documentData = await client.request(DocumentCommentGuard, { + id: documentId, + }) + + if (!documentData?.document) { + throw new NotFoundError("Document", documentId) + } + + const comments = documentData.document.comments.nodes + if (comments.length > 0) { + const comment = comments[0] + const commentKind = comment.quotedText + ? "inline comment" + : "document comment" + const quotedText = comment.quotedText + ? ` quoting "${comment.quotedText}"` + : "" + + throw new ValidationError( + `Refusing to update document content because this document has ${commentKind}s.`, + { + suggestion: + `Updating Markdown content can detach or hide Linear document comments. ` + + `First review comment ${comment.id}${quotedText}, then rerun with --force if you accept that risk.`, + }, + ) + } + } + // Execute the update const updateMutation = gql(` mutation UpdateDocument($id: String!, $input: DocumentUpdateInput!) { diff --git a/src/commands/document/document-view.ts b/src/commands/document/document-view.ts index bfc43868..02967d73 100644 --- a/src/commands/document/document-view.ts +++ b/src/commands/document/document-view.ts @@ -39,6 +39,50 @@ const GetDocument = gql(` identifier title } + comments(first: 50, orderBy: createdAt) { + nodes { + id + body + quotedText + documentContentId + createdAt + updatedAt + archivedAt + resolvedAt + url + user { + name + email + } + parent { + id + } + children(first: 50, orderBy: createdAt) { + nodes { + id + body + quotedText + documentContentId + createdAt + updatedAt + archivedAt + resolvedAt + url + user { + name + email + } + parent { + id + } + } + } + } + pageInfo { + hasNextPage + endCursor + } + } } } `) diff --git a/test/commands/document/__snapshots__/document-update.test.ts.snap b/test/commands/document/__snapshots__/document-update.test.ts.snap index f22a5096..7f04d177 100644 --- a/test/commands/document/__snapshots__/document-update.test.ts.snap +++ b/test/commands/document/__snapshots__/document-update.test.ts.snap @@ -11,12 +11,13 @@ Description: Options: - -h, --help - Show this help. - -t, --title <title> - New title for the document - -c, --content <content> - New markdown content (inline) - -f, --content-file <path> - Read new content from file - --icon <icon> - New icon (emoji) - -e, --edit - Open current content in \$EDITOR for editing + -h, --help - Show this help. + -t, --title <title> - New title for the document + -c, --content <content> - New markdown content (inline) + -f, --content-file <path> - Read new content from file + --icon <icon> - New icon (emoji) + -e, --edit - Open current content in \$EDITOR for editing + --force - Update content even when document comments may lose inline anchors " stderr: @@ -41,6 +42,24 @@ stderr: "" `; +snapshot[`Document Update Command - Blocks Content Update With Comments 1`] = ` +stdout: +"" +stderr: +'✗ Failed to update document: Refusing to update document content because this document has inline comments. + Updating Markdown content can detach or hide Linear document comments. First review comment comment-1 quoting "Current Content", then rerun with --force if you accept that risk. +' +`; + +snapshot[`Document Update Command - Force Content Update With Comments 1`] = ` +stdout: +"✓ Updated document: Delegation System Spec +https://linear.app/test/document/delegation-system-spec-d4b93e3b2695 +" +stderr: +"" +`; + snapshot[`Document Update Command - Update Multiple Fields 1`] = ` stdout: "✓ Updated document: Updated Title diff --git a/test/commands/document/__snapshots__/document-view.test.ts.snap b/test/commands/document/__snapshots__/document-view.test.ts.snap index 123ca86e..f37124fa 100644 --- a/test/commands/document/__snapshots__/document-view.test.ts.snap +++ b/test/commands/document/__snapshots__/document-view.test.ts.snap @@ -70,7 +70,34 @@ stdout: "name": "TinyCloud SDK", "slugId": "tinycloud-sdk" }, - "issue": null + "issue": null, + "comments": { + "nodes": [ + { + "id": "comment-1", + "body": "Can we clarify this?", + "quotedText": "delegation system architecture", + "documentContentId": "document-content-1", + "createdAt": "2026-01-18T11:00:00Z", + "updatedAt": "2026-01-18T11:00:00Z", + "archivedAt": null, + "resolvedAt": null, + "url": "https://linear.app/test/comment/comment-1", + "user": { + "name": "Jane Reviewer", + "email": "jane@example.com" + }, + "parent": null, + "children": { + "nodes": [] + } + } + ], + "pageInfo": { + "hasNextPage": false, + "endCursor": "cursor-1" + } + } } ' stderr: diff --git a/test/commands/document/document-update.test.ts b/test/commands/document/document-update.test.ts index 969f2074..ec23ea98 100644 --- a/test/commands/document/document-update.test.ts +++ b/test/commands/document/document-update.test.ts @@ -72,6 +72,24 @@ await snapshotTest({ denoArgs: commonDenoArgs, async fn() { const server = new MockLinearServer([ + { + queryName: "DocumentCommentGuard", + variables: { + id: "d4b93e3b2695", + }, + response: { + data: { + document: { + id: "doc-1", + title: "Delegation System Spec", + content: "# Current Content", + comments: { + nodes: [], + }, + }, + }, + }, + }, { queryName: "UpdateDocument", variables: { @@ -112,6 +130,106 @@ await snapshotTest({ }, }) +// Test content updates refuse to run when document comments exist +await snapshotTest({ + name: "Document Update Command - Blocks Content Update With Comments", + meta: import.meta, + colors: false, + canFail: true, + args: ["d4b93e3b2695", "--content", "# Updated Content"], + denoArgs: commonDenoArgs, + async fn() { + const server = new MockLinearServer([ + { + queryName: "DocumentCommentGuard", + variables: { + id: "d4b93e3b2695", + }, + response: { + data: { + document: { + id: "doc-1", + title: "Delegation System Spec", + content: "# Current Content", + comments: { + nodes: [ + { + id: "comment-1", + quotedText: "Current Content", + resolvedAt: null, + archivedAt: null, + }, + ], + }, + }, + }, + }, + }, + ]) + + try { + await server.start() + Deno.env.set("LINEAR_GRAPHQL_ENDPOINT", server.getEndpoint()) + Deno.env.set("LINEAR_API_KEY", "Bearer test-token") + + await updateCommand.parse() + } finally { + await server.stop() + Deno.env.delete("LINEAR_GRAPHQL_ENDPOINT") + Deno.env.delete("LINEAR_API_KEY") + } + }, +}) + +// Test --force bypasses the comment guard for intentional content replacement +await snapshotTest({ + name: "Document Update Command - Force Content Update With Comments", + meta: import.meta, + colors: false, + args: ["d4b93e3b2695", "--content", "# Updated Content", "--force"], + denoArgs: commonDenoArgs, + async fn() { + const server = new MockLinearServer([ + { + queryName: "UpdateDocument", + variables: { + id: "d4b93e3b2695", + input: { + content: "# Updated Content", + }, + }, + response: { + data: { + documentUpdate: { + success: true, + document: { + id: "doc-1", + slugId: "d4b93e3b2695", + title: "Delegation System Spec", + url: + "https://linear.app/test/document/delegation-system-spec-d4b93e3b2695", + updatedAt: "2026-01-19T10:00:00Z", + }, + }, + }, + }, + }, + ]) + + try { + await server.start() + Deno.env.set("LINEAR_GRAPHQL_ENDPOINT", server.getEndpoint()) + Deno.env.set("LINEAR_API_KEY", "Bearer test-token") + + await updateCommand.parse() + } finally { + await server.stop() + Deno.env.delete("LINEAR_GRAPHQL_ENDPOINT") + Deno.env.delete("LINEAR_API_KEY") + } + }, +}) + // Test updating multiple fields await snapshotTest({ name: "Document Update Command - Update Multiple Fields", @@ -129,6 +247,24 @@ await snapshotTest({ denoArgs: commonDenoArgs, async fn() { const server = new MockLinearServer([ + { + queryName: "DocumentCommentGuard", + variables: { + id: "d4b93e3b2695", + }, + response: { + data: { + document: { + id: "doc-1", + title: "Delegation System Spec", + content: "# Current Content", + comments: { + nodes: [], + }, + }, + }, + }, + }, { queryName: "UpdateDocument", variables: { diff --git a/test/commands/document/document-view.test.ts b/test/commands/document/document-view.test.ts index a2df16f3..b0f72881 100644 --- a/test/commands/document/document-view.test.ts +++ b/test/commands/document/document-view.test.ts @@ -42,6 +42,33 @@ await snapshotTest({ creator: { name: "John Doe", email: "john@example.com" }, project: { name: "TinyCloud SDK", slugId: "tinycloud-sdk" }, issue: null, + comments: { + nodes: [ + { + id: "comment-1", + body: "Can we clarify this?", + quotedText: "delegation system architecture", + documentContentId: "document-content-1", + createdAt: "2026-01-18T11:00:00Z", + updatedAt: "2026-01-18T11:00:00Z", + archivedAt: null, + resolvedAt: null, + url: "https://linear.app/test/comment/comment-1", + user: { + name: "Jane Reviewer", + email: "jane@example.com", + }, + parent: null, + children: { + nodes: [], + }, + }, + ], + pageInfo: { + hasNextPage: false, + endCursor: "cursor-1", + }, + }, }, }, }, @@ -136,6 +163,33 @@ await snapshotTest({ creator: { name: "John Doe", email: "john@example.com" }, project: { name: "TinyCloud SDK", slugId: "tinycloud-sdk" }, issue: null, + comments: { + nodes: [ + { + id: "comment-1", + body: "Can we clarify this?", + quotedText: "delegation system architecture", + documentContentId: "document-content-1", + createdAt: "2026-01-18T11:00:00Z", + updatedAt: "2026-01-18T11:00:00Z", + archivedAt: null, + resolvedAt: null, + url: "https://linear.app/test/comment/comment-1", + user: { + name: "Jane Reviewer", + email: "jane@example.com", + }, + parent: null, + children: { + nodes: [], + }, + }, + ], + pageInfo: { + hasNextPage: false, + endCursor: "cursor-1", + }, + }, }, }, }, From 5da67fa9aca4950284da9dc7e58f3af2f5b1ebef Mon Sep 17 00:00:00 2001 From: Joseph <joseph@preemptivesec.com> Date: Wed, 24 Jun 2026 17:54:41 -0400 Subject: [PATCH 2/2] Refine document comment guard --- README.md | 2 +- src/commands/document/document-update.ts | 97 +++++++++---- src/commands/document/document-view.ts | 86 +++++++++--- .../document-update.test.ts.snap | 13 +- .../__snapshots__/document-view.test.ts.snap | 23 +++- .../commands/document/document-update.test.ts | 129 +++++++++++++++++- test/commands/document/document-view.test.ts | 55 +++++++- 7 files changed, 339 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 41de9936..33e265f3 100644 --- a/README.md +++ b/README.md @@ -221,7 +221,7 @@ linear document delete <slug> --permanent # permanent delete linear document delete --bulk <slug1> <slug2> # bulk delete ``` -content updates are refused by default when a document has Linear document comments, because replacing markdown can detach or hide inline comment anchors. review comments first, then rerun with `--force` if you intentionally want to replace the content anyway. +content updates are refused by default when a document has active inline Linear comments, because replacing markdown can detach or hide those anchors. top-level document comments do not block updates. review the inline comment first, then rerun with `--force` if you intentionally want to replace the content anyway. ### other commands diff --git a/src/commands/document/document-update.ts b/src/commands/document/document-update.ts index 037dd51f..7773884a 100644 --- a/src/commands/document/document-update.ts +++ b/src/commands/document/document-update.ts @@ -10,24 +10,83 @@ import { ValidationError, } from "../../utils/errors.ts" -const DocumentCommentGuard = gql(` - query DocumentCommentGuard($id: String!) { +const GetDocumentForEdit = gql(` + query GetDocumentForEdit($id: String!) { document(id: $id) { id title content - comments(first: 1) { + } + } +`) + +const DocumentInlineCommentGuard = gql(` + query DocumentInlineCommentGuard($id: String!, $after: String) { + document(id: $id) { + id + comments(first: 50, after: $after, orderBy: createdAt) { nodes { id quotedText - resolvedAt - archivedAt + } + pageInfo { + hasNextPage + endCursor } } } } `) +interface DocumentInlineComment { + id: string + quotedText?: string | null +} + +interface DocumentInlineCommentGuardResponse { + document?: { + comments: { + nodes: DocumentInlineComment[] + pageInfo: { + hasNextPage: boolean + endCursor?: string | null + } + } + } | null +} + +async function getFirstInlineDocumentComment( + client: ReturnType<typeof getGraphQLClient>, + documentId: string, +) { + let after: string | null | undefined = null + + while (true) { + const documentData: DocumentInlineCommentGuardResponse = await client + .request(DocumentInlineCommentGuard, { + id: documentId, + after, + }) + + if (!documentData?.document) { + throw new NotFoundError("Document", documentId) + } + + const comments = documentData.document.comments.nodes + const inlineComment = comments.find((comment) => comment.quotedText) + if (inlineComment) { + return inlineComment + } + + const pageInfo = documentData.document.comments.pageInfo + if (!pageInfo.hasNextPage) { + return undefined + } + + after = pageInfo.endCursor + } +} + /** * Open editor with initial content and return the edited content */ @@ -174,7 +233,7 @@ export const updateCommand = new Command() } } else if (edit) { // Edit mode: fetch current content and open in editor - const documentData = await client.request(DocumentCommentGuard, { + const documentData = await client.request(GetDocumentForEdit, { id: documentId, }) @@ -222,30 +281,18 @@ export const updateCommand = new Command() } if (input.content !== undefined && !force) { - const documentData = await client.request(DocumentCommentGuard, { - id: documentId, - }) - - if (!documentData?.document) { - throw new NotFoundError("Document", documentId) - } - - const comments = documentData.document.comments.nodes - if (comments.length > 0) { - const comment = comments[0] - const commentKind = comment.quotedText - ? "inline comment" - : "document comment" - const quotedText = comment.quotedText - ? ` quoting "${comment.quotedText}"` - : "" + const comment = await getFirstInlineDocumentComment( + client, + documentId, + ) + if (comment) { throw new ValidationError( - `Refusing to update document content because this document has ${commentKind}s.`, + "Refusing to update document content because this document has inline comments.", { suggestion: `Updating Markdown content can detach or hide Linear document comments. ` + - `First review comment ${comment.id}${quotedText}, then rerun with --force if you accept that risk.`, + `First review comment ${comment.id} quoting "${comment.quotedText}", then rerun with --force if you accept that risk.`, }, ) } diff --git a/src/commands/document/document-view.ts b/src/commands/document/document-view.ts index 02967d73..6779e24d 100644 --- a/src/commands/document/document-view.ts +++ b/src/commands/document/document-view.ts @@ -39,7 +39,33 @@ const GetDocument = gql(` identifier title } - comments(first: 50, orderBy: createdAt) { + } + } +`) + +const GetDocumentWithComments = gql(` + query GetDocumentWithComments($id: String!, $commentsAfter: String) { + document(id: $id) { + id + title + slugId + content + url + createdAt + updatedAt + creator { + name + email + } + project { + name + slugId + } + issue { + identifier + title + } + comments(first: 50, after: $commentsAfter, orderBy: createdAt) { nodes { id body @@ -57,26 +83,6 @@ const GetDocument = gql(` parent { id } - children(first: 50, orderBy: createdAt) { - nodes { - id - body - quotedText - documentContentId - createdAt - updatedAt - archivedAt - resolvedAt - url - user { - name - email - } - parent { - id - } - } - } } pageInfo { hasNextPage @@ -87,6 +93,40 @@ const GetDocument = gql(` } `) +async function getDocumentWithAllComments( + client: ReturnType<typeof getGraphQLClient>, + id: string, +) { + const firstResult = await client.request(GetDocumentWithComments, { + id, + commentsAfter: null, + }) + + if (!firstResult.document) { + return undefined + } + + const document = firstResult.document + let commentsAfter = document.comments.pageInfo.endCursor + + while (document.comments.pageInfo.hasNextPage) { + const nextResult = await client.request(GetDocumentWithComments, { + id, + commentsAfter, + }) + + if (!nextResult.document) { + return undefined + } + + document.comments.nodes.push(...nextResult.document.comments.nodes) + document.comments.pageInfo = nextResult.document.comments.pageInfo + commentsAfter = nextResult.document.comments.pageInfo.endCursor + } + + return document +} + export const viewCommand = new Command() .name("view") .description("View a document's content") @@ -104,7 +144,9 @@ export const viewCommand = new Command() try { const client = getGraphQLClient() - const result = await client.request(GetDocument, { id }) + const result = json + ? { document: await getDocumentWithAllComments(client, id) } + : await client.request(GetDocument, { id }) spinner?.stop() const document = result.document diff --git a/test/commands/document/__snapshots__/document-update.test.ts.snap b/test/commands/document/__snapshots__/document-update.test.ts.snap index 7f04d177..b4549882 100644 --- a/test/commands/document/__snapshots__/document-update.test.ts.snap +++ b/test/commands/document/__snapshots__/document-update.test.ts.snap @@ -42,12 +42,21 @@ stderr: "" `; -snapshot[`Document Update Command - Blocks Content Update With Comments 1`] = ` +snapshot[`Document Update Command - Allows Content Update With Top Level Comments 1`] = ` +stdout: +"✓ Updated document: Delegation System Spec +https://linear.app/test/document/delegation-system-spec-d4b93e3b2695 +" +stderr: +"" +`; + +snapshot[`Document Update Command - Blocks Content Update With Inline Comments 1`] = ` stdout: "" stderr: '✗ Failed to update document: Refusing to update document content because this document has inline comments. - Updating Markdown content can detach or hide Linear document comments. First review comment comment-1 quoting "Current Content", then rerun with --force if you accept that risk. + Updating Markdown content can detach or hide Linear document comments. First review comment comment-2 quoting "Current Content", then rerun with --force if you accept that risk. ' `; diff --git a/test/commands/document/__snapshots__/document-view.test.ts.snap b/test/commands/document/__snapshots__/document-view.test.ts.snap index f37124fa..0fbff8b8 100644 --- a/test/commands/document/__snapshots__/document-view.test.ts.snap +++ b/test/commands/document/__snapshots__/document-view.test.ts.snap @@ -87,15 +87,30 @@ stdout: "name": "Jane Reviewer", "email": "jane@example.com" }, - "parent": null, - "children": { - "nodes": [] + "parent": null + }, + { + "id": "comment-2", + "body": "Follow-up note", + "quotedText": null, + "documentContentId": "document-content-1", + "createdAt": "2026-01-18T12:00:00Z", + "updatedAt": "2026-01-18T12:00:00Z", + "archivedAt": null, + "resolvedAt": null, + "url": "https://linear.app/test/comment/comment-2", + "user": { + "name": "John Doe", + "email": "john@example.com" + }, + "parent": { + "id": "comment-1" } } ], "pageInfo": { "hasNextPage": false, - "endCursor": "cursor-1" + "endCursor": null } } } diff --git a/test/commands/document/document-update.test.ts b/test/commands/document/document-update.test.ts index ec23ea98..1806f963 100644 --- a/test/commands/document/document-update.test.ts +++ b/test/commands/document/document-update.test.ts @@ -73,9 +73,10 @@ await snapshotTest({ async fn() { const server = new MockLinearServer([ { - queryName: "DocumentCommentGuard", + queryName: "DocumentInlineCommentGuard", variables: { id: "d4b93e3b2695", + after: null, }, response: { data: { @@ -85,6 +86,10 @@ await snapshotTest({ content: "# Current Content", comments: { nodes: [], + pageInfo: { + hasNextPage: false, + endCursor: null, + }, }, }, }, @@ -130,9 +135,85 @@ await snapshotTest({ }, }) -// Test content updates refuse to run when document comments exist +// Test content updates allow top-level document comments without inline anchors await snapshotTest({ - name: "Document Update Command - Blocks Content Update With Comments", + name: + "Document Update Command - Allows Content Update With Top Level Comments", + meta: import.meta, + colors: false, + args: ["d4b93e3b2695", "--content", "# Updated Content"], + denoArgs: commonDenoArgs, + async fn() { + const server = new MockLinearServer([ + { + queryName: "DocumentInlineCommentGuard", + variables: { + id: "d4b93e3b2695", + after: null, + }, + response: { + data: { + document: { + id: "doc-1", + comments: { + nodes: [ + { + id: "comment-1", + quotedText: null, + }, + ], + pageInfo: { + hasNextPage: false, + endCursor: null, + }, + }, + }, + }, + }, + }, + { + queryName: "UpdateDocument", + variables: { + id: "d4b93e3b2695", + input: { + content: "# Updated Content", + }, + }, + response: { + data: { + documentUpdate: { + success: true, + document: { + id: "doc-1", + slugId: "d4b93e3b2695", + title: "Delegation System Spec", + url: + "https://linear.app/test/document/delegation-system-spec-d4b93e3b2695", + updatedAt: "2026-01-19T10:00:00Z", + }, + }, + }, + }, + }, + ]) + + try { + await server.start() + Deno.env.set("LINEAR_GRAPHQL_ENDPOINT", server.getEndpoint()) + Deno.env.set("LINEAR_API_KEY", "Bearer test-token") + + await updateCommand.parse() + } finally { + await server.stop() + Deno.env.delete("LINEAR_GRAPHQL_ENDPOINT") + Deno.env.delete("LINEAR_API_KEY") + } + }, +}) + +// Test content updates refuse to run when inline document comments exist +await snapshotTest({ + name: "Document Update Command - Blocks Content Update With Inline Comments", meta: import.meta, colors: false, canFail: true, @@ -141,9 +222,10 @@ await snapshotTest({ async fn() { const server = new MockLinearServer([ { - queryName: "DocumentCommentGuard", + queryName: "DocumentInlineCommentGuard", variables: { id: "d4b93e3b2695", + after: null, }, response: { data: { @@ -155,11 +237,39 @@ await snapshotTest({ nodes: [ { id: "comment-1", + quotedText: null, + }, + ], + pageInfo: { + hasNextPage: true, + endCursor: "cursor-1", + }, + }, + }, + }, + }, + }, + { + queryName: "DocumentInlineCommentGuard", + variables: { + id: "d4b93e3b2695", + after: "cursor-1", + }, + response: { + data: { + document: { + id: "doc-1", + comments: { + nodes: [ + { + id: "comment-2", quotedText: "Current Content", - resolvedAt: null, - archivedAt: null, }, ], + pageInfo: { + hasNextPage: false, + endCursor: null, + }, }, }, }, @@ -248,9 +358,10 @@ await snapshotTest({ async fn() { const server = new MockLinearServer([ { - queryName: "DocumentCommentGuard", + queryName: "DocumentInlineCommentGuard", variables: { id: "d4b93e3b2695", + after: null, }, response: { data: { @@ -260,6 +371,10 @@ await snapshotTest({ content: "# Current Content", comments: { nodes: [], + pageInfo: { + hasNextPage: false, + endCursor: null, + }, }, }, }, diff --git a/test/commands/document/document-view.test.ts b/test/commands/document/document-view.test.ts index b0f72881..d7022ad0 100644 --- a/test/commands/document/document-view.test.ts +++ b/test/commands/document/document-view.test.ts @@ -146,8 +146,8 @@ await snapshotTest({ async fn() { const server = new MockLinearServer([ { - queryName: "GetDocument", - variables: { id: "d4b93e3b2695" }, + queryName: "GetDocumentWithComments", + variables: { id: "d4b93e3b2695", commentsAfter: null }, response: { data: { document: { @@ -180,14 +180,59 @@ await snapshotTest({ email: "jane@example.com", }, parent: null, - children: { - nodes: [], + }, + ], + pageInfo: { + hasNextPage: true, + endCursor: "cursor-1", + }, + }, + }, + }, + }, + }, + { + queryName: "GetDocumentWithComments", + variables: { id: "d4b93e3b2695", commentsAfter: "cursor-1" }, + response: { + data: { + document: { + id: "doc-1", + title: "Delegation System Spec", + slugId: "d4b93e3b2695", + content: + "# Delegation System\n\nThis document describes the delegation system architecture.", + url: + "https://linear.app/test/document/delegation-system-spec-d4b93e3b2695", + createdAt: "2026-01-15T08:00:00Z", + updatedAt: "2026-01-18T10:30:00Z", + creator: { name: "John Doe", email: "john@example.com" }, + project: { name: "TinyCloud SDK", slugId: "tinycloud-sdk" }, + issue: null, + comments: { + nodes: [ + { + id: "comment-2", + body: "Follow-up note", + quotedText: null, + documentContentId: "document-content-1", + createdAt: "2026-01-18T12:00:00Z", + updatedAt: "2026-01-18T12:00:00Z", + archivedAt: null, + resolvedAt: null, + url: "https://linear.app/test/comment/comment-2", + user: { + name: "John Doe", + email: "john@example.com", + }, + parent: { + id: "comment-1", }, }, ], pageInfo: { hasNextPage: false, - endCursor: "cursor-1", + endCursor: null, }, }, },