@knpkv/jira-cli 1.1.2 → 1.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.
- package/CHANGELOG.md +37 -0
- package/README.md +23 -7
- package/dist/AttachmentService.d.ts +22 -0
- package/dist/AttachmentService.d.ts.map +1 -0
- package/dist/AttachmentService.js +67 -0
- package/dist/AttachmentService.js.map +1 -0
- package/dist/IssueService.d.ts +2 -20
- package/dist/IssueService.d.ts.map +1 -1
- package/dist/IssueService.js +47 -36
- package/dist/IssueService.js.map +1 -1
- package/dist/JiraAuth.d.ts +1 -1
- package/dist/JiraAuth.d.ts.map +1 -1
- package/dist/JiraAuth.js.map +1 -1
- package/dist/VersionService.d.ts.map +1 -1
- package/dist/VersionService.js +39 -45
- package/dist/VersionService.js.map +1 -1
- package/dist/bin.js +2 -2
- package/dist/bin.js.map +1 -1
- package/dist/commands/auth.d.ts +1 -1
- package/dist/commands/index.d.ts +1 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +1 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/issue.d.ts.map +1 -1
- package/dist/commands/issue.js +2 -1
- package/dist/commands/issue.js.map +1 -1
- package/dist/commands/issueAttachment.d.ts +6 -0
- package/dist/commands/issueAttachment.d.ts.map +1 -0
- package/dist/commands/issueAttachment.js +88 -0
- package/dist/commands/issueAttachment.js.map +1 -0
- package/dist/commands/layers.d.ts +4 -3
- package/dist/commands/layers.d.ts.map +1 -1
- package/dist/commands/layers.js +12 -4
- package/dist/commands/layers.js.map +1 -1
- package/dist/commands/search.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/attachmentInsertion.d.ts +10 -0
- package/dist/internal/attachmentInsertion.d.ts.map +1 -0
- package/dist/internal/attachmentInsertion.js +22 -0
- package/dist/internal/attachmentInsertion.js.map +1 -0
- package/dist/internal/frontmatter.d.ts.map +1 -1
- package/dist/internal/frontmatter.js +15 -3
- package/dist/internal/frontmatter.js.map +1 -1
- package/dist/internal/sync/baseline.js +2 -1
- package/dist/internal/sync/baseline.js.map +1 -1
- package/dist/internal/sync/changes.js.map +1 -1
- package/dist/internal/sync/config.d.ts.map +1 -1
- package/dist/internal/sync/config.js +1 -1
- package/dist/internal/sync/config.js.map +1 -1
- package/dist/internal/sync/document.d.ts.map +1 -1
- package/dist/internal/sync/document.js +88 -29
- package/dist/internal/sync/document.js.map +1 -1
- package/dist/internal/sync/fieldValues.d.ts +0 -5
- package/dist/internal/sync/fieldValues.d.ts.map +1 -1
- package/dist/internal/sync/fieldValues.js +9 -4
- package/dist/internal/sync/fieldValues.js.map +1 -1
- package/dist/internal/sync/manifest.js.map +1 -1
- package/dist/internal/sync/schemas.d.ts +5 -5
- package/dist/internal/sync/schemas.d.ts.map +1 -1
- package/dist/internal/sync/schemas.js +2 -1
- package/dist/internal/sync/schemas.js.map +1 -1
- package/dist/internal/sync/types.d.ts +5 -2
- package/dist/internal/sync/types.d.ts.map +1 -1
- package/dist/internal/sync/types.js.map +1 -1
- package/package.json +13 -9
- package/src/AttachmentService.ts +106 -0
- package/src/IssueService.ts +81 -61
- package/src/JiraAuth.ts +7 -11
- package/src/VersionService.ts +142 -159
- package/src/bin.ts +2 -2
- package/src/commands/index.ts +1 -0
- package/src/commands/issue.ts +3 -2
- package/src/commands/issueAttachment.ts +146 -0
- package/src/commands/layers.ts +20 -3
- package/src/commands/search.ts +1 -1
- package/src/index.ts +6 -0
- package/src/internal/attachmentInsertion.ts +44 -0
- package/src/internal/frontmatter.ts +17 -3
- package/src/internal/sync/baseline.ts +3 -3
- package/src/internal/sync/changes.ts +1 -1
- package/src/internal/sync/config.ts +1 -2
- package/src/internal/sync/document.ts +98 -29
- package/src/internal/sync/fieldValues.ts +4 -6
- package/src/internal/sync/manifest.ts +1 -1
- package/src/internal/sync/schemas.ts +3 -2
- package/src/internal/sync/types.ts +16 -4
- package/test/IssueService.test.ts +94 -0
- package/test/VersionService.test.ts +41 -20
- package/test/attachmentInsertion.test.ts +25 -0
- package/test/commandTree.test.ts +38 -0
- package/test/frontmatter.test.ts +20 -1
- package/test/integration.test.ts +94 -3
- package/test/layers.test.ts +12 -0
- package/test/syncDocument.test.ts +30 -1
- package/vitest.config.integration.ts +4 -0
- package/vitest.config.ts +4 -0
package/test/commandTree.test.ts
CHANGED
|
@@ -4,6 +4,7 @@ import * as Layer from "effect/Layer"
|
|
|
4
4
|
import * as Ref from "effect/Ref"
|
|
5
5
|
import * as Terminal from "effect/Terminal"
|
|
6
6
|
import { Command } from "effect/unstable/cli"
|
|
7
|
+
import { AttachmentService } from "../src/AttachmentService.js"
|
|
7
8
|
import { issueCommand } from "../src/commands/issue.js"
|
|
8
9
|
import { versionCommand } from "../src/commands/version.js"
|
|
9
10
|
import { type Issue, IssueService } from "../src/IssueService.js"
|
|
@@ -13,6 +14,7 @@ import { type RelatedWork, type Version, VersionService } from "../src/VersionSe
|
|
|
13
14
|
interface CommandCalls {
|
|
14
15
|
readonly issueGet: number
|
|
15
16
|
readonly issueSearch: number
|
|
17
|
+
readonly issueAttachmentUpload: number
|
|
16
18
|
readonly versionList: number
|
|
17
19
|
readonly versionGet: number
|
|
18
20
|
readonly versionUpdate: number
|
|
@@ -23,6 +25,7 @@ interface CommandCalls {
|
|
|
23
25
|
|
|
24
26
|
const emptyCalls: CommandCalls = {
|
|
25
27
|
issueGet: 0,
|
|
28
|
+
issueAttachmentUpload: 0,
|
|
26
29
|
issueSearch: 0,
|
|
27
30
|
relatedWorkAdd: 0,
|
|
28
31
|
relatedWorkList: 0,
|
|
@@ -52,6 +55,15 @@ const sampleIssue: Issue = {
|
|
|
52
55
|
url: "https://example.atlassian.net/browse/PROJ-123"
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
const sampleAttachment = {
|
|
59
|
+
id: "30001",
|
|
60
|
+
filename: "evidence.png",
|
|
61
|
+
url: "https://example.atlassian.net/rest/api/3/attachment/content/30001",
|
|
62
|
+
mediaType: "image/png",
|
|
63
|
+
mimeType: "image/png",
|
|
64
|
+
size: 123
|
|
65
|
+
}
|
|
66
|
+
|
|
55
67
|
const sampleVersion: Version = {
|
|
56
68
|
approvers: [],
|
|
57
69
|
archived: false,
|
|
@@ -88,6 +100,16 @@ const CaptureTerminalLayer = (stdout: Ref.Ref<string>) =>
|
|
|
88
100
|
|
|
89
101
|
const CommandServicesLayer = (calls: Ref.Ref<CommandCalls>) =>
|
|
90
102
|
Layer.mergeAll(
|
|
103
|
+
Layer.succeed(
|
|
104
|
+
AttachmentService,
|
|
105
|
+
AttachmentService.of({
|
|
106
|
+
uploadToIssue: () =>
|
|
107
|
+
Ref.update(calls, (state) => ({
|
|
108
|
+
...state,
|
|
109
|
+
issueAttachmentUpload: state.issueAttachmentUpload + 1
|
|
110
|
+
})).pipe(Effect.as(sampleAttachment))
|
|
111
|
+
})
|
|
112
|
+
),
|
|
91
113
|
Layer.succeed(
|
|
92
114
|
IssueService,
|
|
93
115
|
IssueService.of({
|
|
@@ -158,24 +180,40 @@ describe("Jira command tree", () => {
|
|
|
158
180
|
const calls = yield* Ref.make(emptyCalls)
|
|
159
181
|
const getOutput = yield* Ref.make("")
|
|
160
182
|
const searchOutput = yield* Ref.make("")
|
|
183
|
+
const uploadOutput = yield* Ref.make("")
|
|
161
184
|
const legacyGetOutput = yield* Ref.make("")
|
|
162
185
|
const legacySearchOutput = yield* Ref.make("")
|
|
163
186
|
|
|
164
187
|
const getExit = yield* runJiraCommand(["issue", "get", "PROJ-123"], calls, getOutput)
|
|
165
188
|
const searchExit = yield* runJiraCommand(["issue", "search", "project = PROJ"], calls, searchOutput)
|
|
189
|
+
const uploadExit = yield* runJiraCommand(
|
|
190
|
+
[
|
|
191
|
+
"issue",
|
|
192
|
+
"attachment",
|
|
193
|
+
"upload",
|
|
194
|
+
"PROJ-123",
|
|
195
|
+
"./evidence.png",
|
|
196
|
+
"--no-insert"
|
|
197
|
+
],
|
|
198
|
+
calls,
|
|
199
|
+
uploadOutput
|
|
200
|
+
)
|
|
166
201
|
const legacyGetExit = yield* runJiraCommand(["get", "PROJ-123"], calls, legacyGetOutput)
|
|
167
202
|
const legacySearchExit = yield* runJiraCommand(["search", "project = PROJ"], calls, legacySearchOutput)
|
|
168
203
|
|
|
169
204
|
expect(getExit._tag).toBe("Success")
|
|
170
205
|
expect(searchExit._tag).toBe("Success")
|
|
206
|
+
expect(uploadExit._tag).toBe("Success")
|
|
171
207
|
expect(legacyGetExit._tag).toBe("Failure")
|
|
172
208
|
expect(legacySearchExit._tag).toBe("Failure")
|
|
173
209
|
|
|
174
210
|
expect(yield* Ref.get(calls)).toMatchObject({
|
|
175
211
|
issueGet: 1,
|
|
212
|
+
issueAttachmentUpload: 1,
|
|
176
213
|
issueSearch: 1,
|
|
177
214
|
writeMulti: 2
|
|
178
215
|
})
|
|
216
|
+
expect(sampleAttachment.mimeType).toBe(sampleAttachment.mediaType)
|
|
179
217
|
}))
|
|
180
218
|
|
|
181
219
|
it.effect("exposes canonical version commands and rejects legacy names", () =>
|
package/test/frontmatter.test.ts
CHANGED
|
@@ -10,7 +10,10 @@ import type { Issue } from "../src/IssueService.js"
|
|
|
10
10
|
*/
|
|
11
11
|
const parseFrontMatter = (output: string): Record<string, unknown> => {
|
|
12
12
|
const match = output.match(/^---\n([\s\S]*?)\n---/)
|
|
13
|
-
|
|
13
|
+
const parsed = match ? yaml.load(match[1]) : {}
|
|
14
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
|
|
15
|
+
? Object.fromEntries(Object.entries(parsed))
|
|
16
|
+
: {}
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
const makeIssue = (overrides: Partial<Issue> = {}): Issue => ({
|
|
@@ -57,6 +60,22 @@ describe("frontmatter", () => {
|
|
|
57
60
|
expect(data.priority).toBeNull()
|
|
58
61
|
expect(data.assignee).toBeNull()
|
|
59
62
|
})
|
|
63
|
+
|
|
64
|
+
it("renders image and SVG attachments as inline previews with identity metadata", () => {
|
|
65
|
+
const output = serializeIssue(makeIssue({
|
|
66
|
+
attachments: [{
|
|
67
|
+
id: "10001",
|
|
68
|
+
filename: "diagram.svg",
|
|
69
|
+
url: "https://example.atlassian.net/rest/api/3/attachment/content/10001",
|
|
70
|
+
mediaType: "application/octet-stream",
|
|
71
|
+
size: 42
|
|
72
|
+
}]
|
|
73
|
+
}))
|
|
74
|
+
expect(output).toContain(
|
|
75
|
+
"<!-- jiraAttachment: {\"jiraAttachmentId\":\"10001\",\"mediaType\":\"application/octet-stream\",\"size\":42} -->"
|
|
76
|
+
)
|
|
77
|
+
expect(output).toContain("")
|
|
78
|
+
})
|
|
60
79
|
})
|
|
61
80
|
|
|
62
81
|
describe("extractFrontMatter", () => {
|
package/test/integration.test.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* - JIRA_BASE_URL defaults to https://knpkv.atlassian.net
|
|
11
11
|
* - JIRA_ISSUE_KEY defaults to KAN-1
|
|
12
12
|
*/
|
|
13
|
+
import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient"
|
|
13
14
|
import * as NodeServices from "@effect/platform-node/NodeServices"
|
|
14
15
|
import { describe, expect, it } from "@effect/vitest"
|
|
15
16
|
import { JiraApiClient, JiraApiConfig } from "@knpkv/jira-api-client"
|
|
@@ -18,11 +19,14 @@ import * as FileSystem from "effect/FileSystem"
|
|
|
18
19
|
import * as Path from "effect/Path"
|
|
19
20
|
import * as Redacted from "effect/Redacted"
|
|
20
21
|
import * as yaml from "js-yaml"
|
|
22
|
+
import { AttachmentService, layer as AttachmentServiceLayer } from "../src/AttachmentService.js"
|
|
23
|
+
import { insertJiraAttachmentReference } from "../src/internal/attachmentInsertion.js"
|
|
21
24
|
import { IssueService, layer as IssueServiceLayer, SiteUrl } from "../src/IssueService.js"
|
|
22
25
|
import { layer as MarkdownWriterLayer, MarkdownWriter } from "../src/MarkdownWriter.js"
|
|
23
26
|
|
|
24
27
|
const DEFAULT_BASE_URL = "https://knpkv.atlassian.net"
|
|
25
28
|
const DEFAULT_ISSUE_KEY = "KAN-1"
|
|
29
|
+
const INTEGRATION_ATTACHMENT_FILENAME = "inline-attachment.svg"
|
|
26
30
|
|
|
27
31
|
interface IntegrationConfig {
|
|
28
32
|
readonly baseUrl: string
|
|
@@ -54,6 +58,20 @@ const SHOULD_RUN_INTEGRATION = Effect.runSync(
|
|
|
54
58
|
)
|
|
55
59
|
)
|
|
56
60
|
|
|
61
|
+
const SHOULD_RUN_ATTACHMENT_INTEGRATION = Effect.runSync(
|
|
62
|
+
Effect.all([
|
|
63
|
+
Config.option(Config.string("JIRA_INTEGRATION")),
|
|
64
|
+
Config.option(Config.string("JIRA_ATTACHMENT_INTEGRATION"))
|
|
65
|
+
]).pipe(
|
|
66
|
+
Effect.map(([integration, attachment]) =>
|
|
67
|
+
Option.isSome(integration) &&
|
|
68
|
+
envFlagEnabled(integration.value) &&
|
|
69
|
+
Option.isSome(attachment) &&
|
|
70
|
+
envFlagEnabled(attachment.value)
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
|
|
57
75
|
const readIntegrationConfig = Effect.gen(function*() {
|
|
58
76
|
const baseUrl = yield* Config.option(Config.string("JIRA_BASE_URL"))
|
|
59
77
|
const issueKey = yield* Config.option(Config.string("JIRA_ISSUE_KEY"))
|
|
@@ -72,17 +90,19 @@ const makeIntegrationLayer = (config: IntegrationConfig) => {
|
|
|
72
90
|
const configLayer = Layer.succeed(JiraApiConfig, {
|
|
73
91
|
baseUrl: config.baseUrl,
|
|
74
92
|
auth: {
|
|
75
|
-
type: "basic"
|
|
93
|
+
type: "basic",
|
|
76
94
|
email: config.email,
|
|
77
95
|
apiToken: config.apiKey
|
|
78
|
-
}
|
|
96
|
+
} satisfies { readonly type: "basic"; readonly email: string; readonly apiToken: typeof config.apiKey }
|
|
79
97
|
})
|
|
80
98
|
|
|
81
99
|
return MarkdownWriterLayer.pipe(
|
|
100
|
+
Layer.provideMerge(AttachmentServiceLayer),
|
|
82
101
|
Layer.provideMerge(IssueServiceLayer),
|
|
83
102
|
Layer.provideMerge(Layer.succeed(SiteUrl, config.baseUrl)),
|
|
84
103
|
Layer.provideMerge(JiraApiClient.layer),
|
|
85
104
|
Layer.provideMerge(configLayer),
|
|
105
|
+
Layer.provideMerge(NodeHttpClient.layerUndici),
|
|
86
106
|
Layer.provideMerge(NodeServices.layer)
|
|
87
107
|
)
|
|
88
108
|
}
|
|
@@ -102,16 +122,49 @@ const readText = (filePath: string) =>
|
|
|
102
122
|
Effect.flatMap((fs) => fs.readFileString(filePath))
|
|
103
123
|
)
|
|
104
124
|
|
|
125
|
+
const writeText = (filePath: string, content: string) =>
|
|
126
|
+
FileSystem.FileSystem.pipe(
|
|
127
|
+
Effect.flatMap((fs) => fs.writeFileString(filePath, content))
|
|
128
|
+
)
|
|
129
|
+
|
|
105
130
|
const pathExists = (filePath: string) =>
|
|
106
131
|
FileSystem.FileSystem.pipe(
|
|
107
132
|
Effect.flatMap((fs) => fs.exists(filePath))
|
|
108
133
|
)
|
|
109
134
|
|
|
135
|
+
const integrationAttachmentFixturePath = Path.Path.pipe(
|
|
136
|
+
Effect.flatMap((path) =>
|
|
137
|
+
path.fromFileUrl(new URL("../../atlassian-common/test/fixtures/attachments/inline-attachment.svg", import.meta.url))
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
const copyIntegrationAttachmentAsset = (testDir: string) =>
|
|
142
|
+
Effect.gen(function*() {
|
|
143
|
+
const source = yield* integrationAttachmentFixturePath
|
|
144
|
+
const target = yield* joinPath(testDir, INTEGRATION_ATTACHMENT_FILENAME)
|
|
145
|
+
yield* writeText(target, yield* readText(source))
|
|
146
|
+
return target
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const expectCanonicalIssueIncludesAttachmentMedia = (
|
|
150
|
+
config: IntegrationConfig,
|
|
151
|
+
issue: { readonly attachments: ReadonlyArray<{ readonly filename: string }> },
|
|
152
|
+
markdown: string
|
|
153
|
+
) => {
|
|
154
|
+
if (config.issueKey !== DEFAULT_ISSUE_KEY) return
|
|
155
|
+
|
|
156
|
+
expect(issue.attachments.some((attachment) => attachment.filename === INTEGRATION_ATTACHMENT_FILENAME)).toBe(true)
|
|
157
|
+
expect(markdown).toContain("## Attachments")
|
|
158
|
+
expect(markdown).toContain(`
|
|
159
|
+
expect(markdown).toContain("jiraAttachmentId")
|
|
160
|
+
expect(markdown).toContain("image/svg+xml")
|
|
161
|
+
}
|
|
162
|
+
|
|
110
163
|
const parseFrontMatter = (markdown: string): Record<string, unknown> => {
|
|
111
164
|
const match = /^---\n([\s\S]*?)\n---/.exec(markdown)
|
|
112
165
|
const parsed = match?.[1] ? yaml.load(match[1]) : {}
|
|
113
166
|
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
|
|
114
|
-
? parsed
|
|
167
|
+
? Object.fromEntries(Object.entries(parsed))
|
|
115
168
|
: {}
|
|
116
169
|
}
|
|
117
170
|
|
|
@@ -140,6 +193,7 @@ const getIssueAsMarkdown = (config: IntegrationConfig, testDir: string) =>
|
|
|
140
193
|
expect(frontMatter.summary).toBe(issue.summary)
|
|
141
194
|
expect(typeof frontMatter.status).toBe("string")
|
|
142
195
|
expect(markdown).toContain(`# ${config.issueKey}:`)
|
|
196
|
+
expectCanonicalIssueIncludesAttachmentMedia(config, issue, markdown)
|
|
143
197
|
|
|
144
198
|
return { frontMatter, issue }
|
|
145
199
|
})
|
|
@@ -184,4 +238,41 @@ describe("Jira integration", () => {
|
|
|
184
238
|
}),
|
|
185
239
|
120000
|
|
186
240
|
)
|
|
241
|
+
|
|
242
|
+
it.effect.skipIf(!SHOULD_RUN_ATTACHMENT_INTEGRATION)(
|
|
243
|
+
"uploads an SVG attachment and inserts a Markdown attachment reference",
|
|
244
|
+
() =>
|
|
245
|
+
Effect.gen(function*() {
|
|
246
|
+
const config = yield* readIntegrationConfig
|
|
247
|
+
return yield* Effect.gen(function*() {
|
|
248
|
+
const testDir = yield* makeTempRoot
|
|
249
|
+
const assetPath = yield* copyIntegrationAttachmentAsset(testDir)
|
|
250
|
+
const documentPath = yield* joinPath(testDir, `${config.issueKey}.md`)
|
|
251
|
+
yield* writeText(documentPath, `# ${config.issueKey}\n\n\n`)
|
|
252
|
+
|
|
253
|
+
const attachmentService = yield* AttachmentService
|
|
254
|
+
const issueService = yield* IssueService
|
|
255
|
+
const attachment = yield* attachmentService.uploadToIssue(config.issueKey, { filePath: assetPath })
|
|
256
|
+
expect(attachment.id).toBeTruthy()
|
|
257
|
+
expect(attachment.filename).toBe(INTEGRATION_ATTACHMENT_FILENAME)
|
|
258
|
+
expect(attachment.mediaType === null || attachment.mediaType.includes("svg")).toBe(true)
|
|
259
|
+
|
|
260
|
+
const issue = yield* issueService.getByKey(config.issueKey)
|
|
261
|
+
expect(issue.attachments.some((candidate) => candidate.id === attachment.id)).toBe(true)
|
|
262
|
+
|
|
263
|
+
const insertion = insertJiraAttachmentReference(yield* readText(documentPath), assetPath, attachment)
|
|
264
|
+
expect(insertion.replacements).toBe(1)
|
|
265
|
+
yield* writeText(documentPath, insertion.content)
|
|
266
|
+
|
|
267
|
+
const updated = yield* readText(documentPath)
|
|
268
|
+
expect(updated).toContain(`"jiraAttachmentId":"${attachment.id}"`)
|
|
269
|
+
expect(updated).toContain(`
|
|
270
|
+
expect(updated).not.toContain(``)
|
|
271
|
+
}).pipe(
|
|
272
|
+
Effect.provide(makeIntegrationLayer(config)),
|
|
273
|
+
Effect.scoped
|
|
274
|
+
)
|
|
275
|
+
}),
|
|
276
|
+
120000
|
|
277
|
+
)
|
|
187
278
|
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, expect, it } from "@effect/vitest"
|
|
2
|
+
import { getLayerType } from "../src/commands/layers.js"
|
|
3
|
+
|
|
4
|
+
describe("getLayerType", () => {
|
|
5
|
+
it("routes attachment upload dry-runs to the config-free minimal layer", () => {
|
|
6
|
+
expect(getLayerType(["issue", "attachment", "upload", "PROJ-1", "./evidence.png", "--dry-run"])).toBe(
|
|
7
|
+
"minimal"
|
|
8
|
+
)
|
|
9
|
+
expect(getLayerType(["issue", "attachment", "upload", "PROJ-1", "./evidence.png", "-n"])).toBe("minimal")
|
|
10
|
+
expect(getLayerType(["issue", "attachment", "upload", "PROJ-1", "./evidence.png"])).toBe("full")
|
|
11
|
+
})
|
|
12
|
+
})
|
|
@@ -29,7 +29,13 @@ const document: IssueDocument = {
|
|
|
29
29
|
created: "2026-06-27",
|
|
30
30
|
body: "Existing Jira comment."
|
|
31
31
|
}],
|
|
32
|
-
attachments: [{
|
|
32
|
+
attachments: [{
|
|
33
|
+
id: "30001",
|
|
34
|
+
filename: "evidence.png",
|
|
35
|
+
url: "https://example.atlassian.net/evidence.png",
|
|
36
|
+
mediaType: "image/png",
|
|
37
|
+
size: 1234
|
|
38
|
+
}],
|
|
33
39
|
localNotes: "Private local note."
|
|
34
40
|
}
|
|
35
41
|
|
|
@@ -66,4 +72,27 @@ describe("Issue Document parsing", () => {
|
|
|
66
72
|
const serialized = serializeIssueDocument(document).replace("<!-- draftId: draft-1 -->", "<!-- draft: draft-1 -->")
|
|
67
73
|
expect(() => parseIssueDocument("PROJ-123.md", serialized)).toThrow(SyncValidationError)
|
|
68
74
|
})
|
|
75
|
+
|
|
76
|
+
it("renders image attachments inline with hidden identity metadata", () => {
|
|
77
|
+
const serialized = serializeIssueDocument(document)
|
|
78
|
+
expect(serialized).toContain(
|
|
79
|
+
"<!-- jiraAttachment: {\"jiraAttachmentId\":\"30001\",\"mediaType\":\"image/png\",\"size\":1234} -->"
|
|
80
|
+
)
|
|
81
|
+
expect(serialized).toContain("")
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it("keeps parsing legacy attachment link lines", () => {
|
|
85
|
+
const serialized = serializeIssueDocument(document).replace(
|
|
86
|
+
/<!-- jiraAttachment:[\s\S]*?\n!\[evidence\.png]\(https:\/\/example\.atlassian\.net\/evidence\.png\)/,
|
|
87
|
+
"- [evidence.png](https://example.atlassian.net/evidence.png)"
|
|
88
|
+
)
|
|
89
|
+
const parsed = parseIssueDocument("PROJ-123.md", serialized)
|
|
90
|
+
expect(parsed.attachments).toEqual([{
|
|
91
|
+
id: "",
|
|
92
|
+
filename: "evidence.png",
|
|
93
|
+
url: "https://example.atlassian.net/evidence.png",
|
|
94
|
+
mediaType: null,
|
|
95
|
+
size: null
|
|
96
|
+
}])
|
|
97
|
+
})
|
|
69
98
|
})
|
|
@@ -3,6 +3,10 @@ import { defineConfig } from "vitest/config"
|
|
|
3
3
|
export default defineConfig({
|
|
4
4
|
resolve: {
|
|
5
5
|
alias: {
|
|
6
|
+
"@knpkv/atlassian-common/attachments": new URL("../atlassian-common/src/attachments.ts", import.meta.url).pathname,
|
|
7
|
+
"@knpkv/atlassian-common/auth": new URL("../atlassian-common/src/auth/index.ts", import.meta.url).pathname,
|
|
8
|
+
"@knpkv/atlassian-common/config": new URL("../atlassian-common/src/config/index.ts", import.meta.url).pathname,
|
|
9
|
+
"@knpkv/agent-skills": new URL("../agent-skills/src/index.ts", import.meta.url).pathname,
|
|
6
10
|
"@knpkv/jira-api-client": new URL("../jira-api-client/src/index.ts", import.meta.url).pathname
|
|
7
11
|
}
|
|
8
12
|
},
|
package/vitest.config.ts
CHANGED
|
@@ -3,6 +3,10 @@ import { defineConfig } from "vitest/config"
|
|
|
3
3
|
export default defineConfig({
|
|
4
4
|
resolve: {
|
|
5
5
|
alias: {
|
|
6
|
+
"@knpkv/atlassian-common/attachments": new URL("../atlassian-common/src/attachments.ts", import.meta.url).pathname,
|
|
7
|
+
"@knpkv/atlassian-common/auth": new URL("../atlassian-common/src/auth/index.ts", import.meta.url).pathname,
|
|
8
|
+
"@knpkv/atlassian-common/config": new URL("../atlassian-common/src/config/index.ts", import.meta.url).pathname,
|
|
9
|
+
"@knpkv/agent-skills": new URL("../agent-skills/src/index.ts", import.meta.url).pathname,
|
|
6
10
|
"@knpkv/jira-api-client": new URL("../jira-api-client/src/index.ts", import.meta.url).pathname
|
|
7
11
|
}
|
|
8
12
|
},
|