@knpkv/confluence-to-markdown 0.6.0 → 0.7.0

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.
Files changed (357) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/LICENSE +21 -0
  3. package/README.md +22 -13
  4. package/dist/AdfPlaceholders.d.ts +42 -0
  5. package/dist/AdfPlaceholders.d.ts.map +1 -0
  6. package/dist/AdfPlaceholders.js +547 -0
  7. package/dist/AdfPlaceholders.js.map +1 -0
  8. package/dist/AdfSchemaValidator.d.ts +37 -0
  9. package/dist/AdfSchemaValidator.d.ts.map +1 -0
  10. package/dist/AdfSchemaValidator.js +37 -0
  11. package/dist/AdfSchemaValidator.js.map +1 -0
  12. package/dist/AdfWalker.d.ts +39 -0
  13. package/dist/AdfWalker.d.ts.map +1 -0
  14. package/dist/AdfWalker.js +527 -0
  15. package/dist/AdfWalker.js.map +1 -0
  16. package/dist/AtlaskitTransformers.d.ts +35 -0
  17. package/dist/AtlaskitTransformers.d.ts.map +1 -0
  18. package/dist/AtlaskitTransformers.js +48 -0
  19. package/dist/AtlaskitTransformers.js.map +1 -0
  20. package/dist/Brand.d.ts +6 -6
  21. package/dist/Brand.d.ts.map +1 -1
  22. package/dist/Brand.js +8 -6
  23. package/dist/Brand.js.map +1 -1
  24. package/dist/ConfluenceAuth.d.ts +4 -4
  25. package/dist/ConfluenceAuth.d.ts.map +1 -1
  26. package/dist/ConfluenceAuth.js +37 -39
  27. package/dist/ConfluenceAuth.js.map +1 -1
  28. package/dist/ConfluenceClient.d.ts +7 -17
  29. package/dist/ConfluenceClient.d.ts.map +1 -1
  30. package/dist/ConfluenceClient.js +81 -38
  31. package/dist/ConfluenceClient.js.map +1 -1
  32. package/dist/ConfluenceConfig.d.ts +3 -3
  33. package/dist/ConfluenceConfig.d.ts.map +1 -1
  34. package/dist/ConfluenceConfig.js +13 -11
  35. package/dist/ConfluenceConfig.js.map +1 -1
  36. package/dist/ConfluenceError.d.ts +68 -16
  37. package/dist/ConfluenceError.d.ts.map +1 -1
  38. package/dist/ConfluenceError.js +30 -1
  39. package/dist/ConfluenceError.js.map +1 -1
  40. package/dist/GitError.d.ts +5 -5
  41. package/dist/GitService.d.ts +11 -3
  42. package/dist/GitService.d.ts.map +1 -1
  43. package/dist/GitService.js +22 -27
  44. package/dist/GitService.js.map +1 -1
  45. package/dist/LocalFileSystem.d.ts +3 -3
  46. package/dist/LocalFileSystem.d.ts.map +1 -1
  47. package/dist/LocalFileSystem.js +6 -6
  48. package/dist/LocalFileSystem.js.map +1 -1
  49. package/dist/MarkdownConverter.d.ts +16 -65
  50. package/dist/MarkdownConverter.d.ts.map +1 -1
  51. package/dist/MarkdownConverter.js +64 -85
  52. package/dist/MarkdownConverter.js.map +1 -1
  53. package/dist/Schemas.d.ts +128 -141
  54. package/dist/Schemas.d.ts.map +1 -1
  55. package/dist/Schemas.js +21 -23
  56. package/dist/Schemas.js.map +1 -1
  57. package/dist/SyncEngine.d.ts +8 -5
  58. package/dist/SyncEngine.d.ts.map +1 -1
  59. package/dist/SyncEngine.js +189 -113
  60. package/dist/SyncEngine.js.map +1 -1
  61. package/dist/bin.js +23 -35
  62. package/dist/bin.js.map +1 -1
  63. package/dist/commands/auth.d.ts +2 -14
  64. package/dist/commands/auth.d.ts.map +1 -1
  65. package/dist/commands/auth.js +11 -16
  66. package/dist/commands/auth.js.map +1 -1
  67. package/dist/commands/clone.d.ts +4 -6
  68. package/dist/commands/clone.d.ts.map +1 -1
  69. package/dist/commands/clone.js +34 -32
  70. package/dist/commands/clone.js.map +1 -1
  71. package/dist/commands/delete.d.ts +2 -10
  72. package/dist/commands/delete.d.ts.map +1 -1
  73. package/dist/commands/delete.js +5 -4
  74. package/dist/commands/delete.js.map +1 -1
  75. package/dist/commands/errorHandler.d.ts +2 -1
  76. package/dist/commands/errorHandler.d.ts.map +1 -1
  77. package/dist/commands/errorHandler.js +22 -15
  78. package/dist/commands/errorHandler.js.map +1 -1
  79. package/dist/commands/fetch.d.ts +27 -0
  80. package/dist/commands/fetch.d.ts.map +1 -0
  81. package/dist/commands/fetch.js +48 -0
  82. package/dist/commands/fetch.js.map +1 -0
  83. package/dist/commands/git.d.ts +7 -10
  84. package/dist/commands/git.d.ts.map +1 -1
  85. package/dist/commands/git.js +6 -6
  86. package/dist/commands/git.js.map +1 -1
  87. package/dist/commands/index.d.ts +1 -0
  88. package/dist/commands/index.d.ts.map +1 -1
  89. package/dist/commands/index.js +1 -0
  90. package/dist/commands/index.js.map +1 -1
  91. package/dist/commands/layers.d.ts +10 -9
  92. package/dist/commands/layers.d.ts.map +1 -1
  93. package/dist/commands/layers.js +41 -30
  94. package/dist/commands/layers.js.map +1 -1
  95. package/dist/commands/new.d.ts +2 -6
  96. package/dist/commands/new.d.ts.map +1 -1
  97. package/dist/commands/new.js +5 -4
  98. package/dist/commands/new.js.map +1 -1
  99. package/dist/commands/pageInput.d.ts +19 -0
  100. package/dist/commands/pageInput.d.ts.map +1 -0
  101. package/dist/commands/pageInput.js +68 -0
  102. package/dist/commands/pageInput.js.map +1 -0
  103. package/dist/commands/root.d.ts +8 -0
  104. package/dist/commands/root.d.ts.map +1 -0
  105. package/dist/commands/root.js +29 -0
  106. package/dist/commands/root.js.map +1 -0
  107. package/dist/commands/sync.d.ts +6 -9
  108. package/dist/commands/sync.d.ts.map +1 -1
  109. package/dist/commands/sync.js +5 -6
  110. package/dist/commands/sync.js.map +1 -1
  111. package/dist/index.d.ts +3 -8
  112. package/dist/index.d.ts.map +1 -1
  113. package/dist/index.js +2 -13
  114. package/dist/index.js.map +1 -1
  115. package/dist/internal/NodeLayers.d.ts.map +1 -1
  116. package/dist/internal/NodeLayers.js +1 -2
  117. package/dist/internal/NodeLayers.js.map +1 -1
  118. package/dist/internal/adfMetadata.d.ts +30 -0
  119. package/dist/internal/adfMetadata.d.ts.map +1 -0
  120. package/dist/internal/adfMetadata.js +126 -0
  121. package/dist/internal/adfMetadata.js.map +1 -0
  122. package/dist/internal/cleanMarkdown.d.ts +5 -0
  123. package/dist/internal/cleanMarkdown.d.ts.map +1 -0
  124. package/dist/internal/cleanMarkdown.js +13 -0
  125. package/dist/internal/cleanMarkdown.js.map +1 -0
  126. package/dist/internal/frontmatter.d.ts.map +1 -1
  127. package/dist/internal/frontmatter.js +41 -8
  128. package/dist/internal/frontmatter.js.map +1 -1
  129. package/dist/internal/gitCommands.d.ts +9 -3
  130. package/dist/internal/gitCommands.d.ts.map +1 -1
  131. package/dist/internal/gitCommands.js +18 -9
  132. package/dist/internal/gitCommands.js.map +1 -1
  133. package/dist/internal/hashUtils.d.ts +1 -1
  134. package/dist/internal/hashUtils.d.ts.map +1 -1
  135. package/dist/internal/hashUtils.js +1 -1
  136. package/dist/internal/hashUtils.js.map +1 -1
  137. package/dist/internal/oauthServer.d.ts +10 -5
  138. package/dist/internal/oauthServer.d.ts.map +1 -1
  139. package/dist/internal/oauthServer.js +19 -40
  140. package/dist/internal/oauthServer.js.map +1 -1
  141. package/dist/internal/pathUtils.d.ts +1 -1
  142. package/dist/internal/pathUtils.d.ts.map +1 -1
  143. package/dist/internal/pathUtils.js +1 -1
  144. package/dist/internal/pathUtils.js.map +1 -1
  145. package/dist/internal/process.d.ts +15 -0
  146. package/dist/internal/process.d.ts.map +1 -0
  147. package/dist/internal/process.js +10 -0
  148. package/dist/internal/process.js.map +1 -0
  149. package/dist/internal/stdio.d.ts +6 -0
  150. package/dist/internal/stdio.d.ts.map +1 -0
  151. package/dist/internal/stdio.js +15 -0
  152. package/dist/internal/stdio.js.map +1 -0
  153. package/dist/internal/tokenStorage.d.ts +3 -13
  154. package/dist/internal/tokenStorage.d.ts.map +1 -1
  155. package/dist/internal/tokenStorage.js +26 -24
  156. package/dist/internal/tokenStorage.js.map +1 -1
  157. package/dist/internal/userCache.d.ts +1 -1
  158. package/dist/internal/userCache.d.ts.map +1 -1
  159. package/dist/internal/userCache.js +1 -1
  160. package/dist/internal/userCache.js.map +1 -1
  161. package/package.json +30 -30
  162. package/skills/confluence/SKILL.md +143 -0
  163. package/skills/confluence/agents/openai.yaml +4 -0
  164. package/src/AdfPlaceholders.ts +310 -13
  165. package/src/AdfSchemaValidator.ts +2 -4
  166. package/src/AdfWalker.ts +122 -42
  167. package/src/AtlaskitTransformers.ts +2 -4
  168. package/src/Brand.ts +11 -16
  169. package/src/ConfluenceAuth.ts +22 -30
  170. package/src/ConfluenceClient.ts +24 -20
  171. package/src/ConfluenceConfig.ts +14 -14
  172. package/src/GitService.ts +39 -49
  173. package/src/LocalFileSystem.ts +7 -9
  174. package/src/MarkdownConverter.ts +2 -4
  175. package/src/Schemas.ts +13 -12
  176. package/src/SyncEngine.ts +151 -53
  177. package/src/bin.ts +30 -56
  178. package/src/commands/auth.ts +21 -18
  179. package/src/commands/clone.ts +38 -37
  180. package/src/commands/delete.ts +5 -4
  181. package/src/commands/errorHandler.ts +25 -18
  182. package/src/commands/fetch.ts +90 -0
  183. package/src/commands/git.ts +6 -6
  184. package/src/commands/index.ts +1 -0
  185. package/src/commands/layers.ts +53 -33
  186. package/src/commands/new.ts +5 -4
  187. package/src/commands/pageInput.ts +103 -0
  188. package/src/commands/root.ts +59 -0
  189. package/src/commands/sync.ts +7 -6
  190. package/src/internal/NodeLayers.ts +1 -2
  191. package/src/internal/adfMetadata.ts +145 -0
  192. package/src/internal/cleanMarkdown.ts +15 -0
  193. package/src/internal/frontmatter.ts +45 -8
  194. package/src/internal/gitCommands.ts +23 -17
  195. package/src/internal/hashUtils.ts +2 -2
  196. package/src/internal/oauthServer.ts +84 -105
  197. package/src/internal/pathUtils.ts +1 -1
  198. package/src/internal/process.ts +15 -0
  199. package/src/internal/stdio.ts +22 -0
  200. package/src/internal/tokenStorage.ts +39 -29
  201. package/src/internal/userCache.ts +2 -2
  202. package/test/AdfPlaceholders.test.ts +213 -0
  203. package/test/AdfSchemaValidator.test.ts +6 -6
  204. package/test/AdfWalker.test.ts +167 -21
  205. package/test/AtlaskitTransformers.test.ts +4 -4
  206. package/test/Brand.test.ts +11 -11
  207. package/test/GitService.test.ts +6 -2
  208. package/test/MarkdownConverter.test.ts +12 -11
  209. package/test/RoundTrip.test.ts +258 -3
  210. package/test/Schemas.test.ts +40 -40
  211. package/test/adfMetadata.test.ts +110 -0
  212. package/test/cleanMarkdown.test.ts +36 -0
  213. package/test/commandHarness.test.ts +79 -0
  214. package/test/commandHarness.ts +147 -0
  215. package/test/fetch.test.ts +61 -0
  216. package/test/frontmatter.test.ts +41 -0
  217. package/test/integration.test.ts +569 -156
  218. package/test/layers.test.ts +12 -0
  219. package/test/oauthServer.test.ts +4 -5
  220. package/test/pageInput.test.ts +83 -0
  221. package/test/tokenStorage.test.ts +17 -17
  222. package/dist/SchemaConverterError.d.ts +0 -108
  223. package/dist/SchemaConverterError.d.ts.map +0 -1
  224. package/dist/SchemaConverterError.js +0 -84
  225. package/dist/SchemaConverterError.js.map +0 -1
  226. package/dist/ast/BlockNode.d.ts +0 -468
  227. package/dist/ast/BlockNode.d.ts.map +0 -1
  228. package/dist/ast/BlockNode.js +0 -319
  229. package/dist/ast/BlockNode.js.map +0 -1
  230. package/dist/ast/Document.d.ts +0 -244
  231. package/dist/ast/Document.d.ts.map +0 -1
  232. package/dist/ast/Document.js +0 -69
  233. package/dist/ast/Document.js.map +0 -1
  234. package/dist/ast/InlineNode.d.ts +0 -477
  235. package/dist/ast/InlineNode.d.ts.map +0 -1
  236. package/dist/ast/InlineNode.js +0 -263
  237. package/dist/ast/InlineNode.js.map +0 -1
  238. package/dist/ast/MacroNode.d.ts +0 -267
  239. package/dist/ast/MacroNode.d.ts.map +0 -1
  240. package/dist/ast/MacroNode.js +0 -164
  241. package/dist/ast/MacroNode.js.map +0 -1
  242. package/dist/ast/index.d.ts +0 -10
  243. package/dist/ast/index.d.ts.map +0 -1
  244. package/dist/ast/index.js +0 -14
  245. package/dist/ast/index.js.map +0 -1
  246. package/dist/parsers/ConfluenceParser.d.ts +0 -26
  247. package/dist/parsers/ConfluenceParser.d.ts.map +0 -1
  248. package/dist/parsers/ConfluenceParser.js +0 -792
  249. package/dist/parsers/ConfluenceParser.js.map +0 -1
  250. package/dist/parsers/MarkdownParser.d.ts +0 -26
  251. package/dist/parsers/MarkdownParser.d.ts.map +0 -1
  252. package/dist/parsers/MarkdownParser.js +0 -873
  253. package/dist/parsers/MarkdownParser.js.map +0 -1
  254. package/dist/parsers/index.d.ts +0 -8
  255. package/dist/parsers/index.d.ts.map +0 -1
  256. package/dist/parsers/index.js +0 -8
  257. package/dist/parsers/index.js.map +0 -1
  258. package/dist/parsers/preprocessing/ConfluencePreprocessing.d.ts +0 -23
  259. package/dist/parsers/preprocessing/ConfluencePreprocessing.d.ts.map +0 -1
  260. package/dist/parsers/preprocessing/ConfluencePreprocessing.js +0 -323
  261. package/dist/parsers/preprocessing/ConfluencePreprocessing.js.map +0 -1
  262. package/dist/parsers/preprocessing/index.d.ts +0 -7
  263. package/dist/parsers/preprocessing/index.d.ts.map +0 -1
  264. package/dist/parsers/preprocessing/index.js +0 -7
  265. package/dist/parsers/preprocessing/index.js.map +0 -1
  266. package/dist/schemas/ConfluenceSchema.d.ts +0 -21
  267. package/dist/schemas/ConfluenceSchema.d.ts.map +0 -1
  268. package/dist/schemas/ConfluenceSchema.js +0 -38
  269. package/dist/schemas/ConfluenceSchema.js.map +0 -1
  270. package/dist/schemas/ConversionSchema.d.ts +0 -35
  271. package/dist/schemas/ConversionSchema.d.ts.map +0 -1
  272. package/dist/schemas/ConversionSchema.js +0 -208
  273. package/dist/schemas/ConversionSchema.js.map +0 -1
  274. package/dist/schemas/MarkdownSchema.d.ts +0 -21
  275. package/dist/schemas/MarkdownSchema.d.ts.map +0 -1
  276. package/dist/schemas/MarkdownSchema.js +0 -38
  277. package/dist/schemas/MarkdownSchema.js.map +0 -1
  278. package/dist/schemas/hast/HastFromHtml.d.ts +0 -27
  279. package/dist/schemas/hast/HastFromHtml.d.ts.map +0 -1
  280. package/dist/schemas/hast/HastFromHtml.js +0 -107
  281. package/dist/schemas/hast/HastFromHtml.js.map +0 -1
  282. package/dist/schemas/hast/HastSchema.d.ts +0 -195
  283. package/dist/schemas/hast/HastSchema.d.ts.map +0 -1
  284. package/dist/schemas/hast/HastSchema.js +0 -183
  285. package/dist/schemas/hast/HastSchema.js.map +0 -1
  286. package/dist/schemas/hast/index.d.ts +0 -9
  287. package/dist/schemas/hast/index.d.ts.map +0 -1
  288. package/dist/schemas/hast/index.js +0 -3
  289. package/dist/schemas/hast/index.js.map +0 -1
  290. package/dist/schemas/index.d.ts +0 -14
  291. package/dist/schemas/index.d.ts.map +0 -1
  292. package/dist/schemas/index.js +0 -16
  293. package/dist/schemas/index.js.map +0 -1
  294. package/dist/schemas/mdast/MdastFromMarkdown.d.ts +0 -30
  295. package/dist/schemas/mdast/MdastFromMarkdown.d.ts.map +0 -1
  296. package/dist/schemas/mdast/MdastFromMarkdown.js +0 -79
  297. package/dist/schemas/mdast/MdastFromMarkdown.js.map +0 -1
  298. package/dist/schemas/mdast/MdastSchema.d.ts +0 -385
  299. package/dist/schemas/mdast/MdastSchema.d.ts.map +0 -1
  300. package/dist/schemas/mdast/MdastSchema.js +0 -266
  301. package/dist/schemas/mdast/MdastSchema.js.map +0 -1
  302. package/dist/schemas/mdast/index.d.ts +0 -10
  303. package/dist/schemas/mdast/index.d.ts.map +0 -1
  304. package/dist/schemas/mdast/index.js +0 -4
  305. package/dist/schemas/mdast/index.js.map +0 -1
  306. package/dist/schemas/mdast/mdastToString.d.ts +0 -13
  307. package/dist/schemas/mdast/mdastToString.d.ts.map +0 -1
  308. package/dist/schemas/mdast/mdastToString.js +0 -85
  309. package/dist/schemas/mdast/mdastToString.js.map +0 -1
  310. package/dist/schemas/nodes/block/BlockSchema.d.ts +0 -43
  311. package/dist/schemas/nodes/block/BlockSchema.d.ts.map +0 -1
  312. package/dist/schemas/nodes/block/BlockSchema.js +0 -634
  313. package/dist/schemas/nodes/block/BlockSchema.js.map +0 -1
  314. package/dist/schemas/nodes/block/index.d.ts +0 -7
  315. package/dist/schemas/nodes/block/index.d.ts.map +0 -1
  316. package/dist/schemas/nodes/block/index.js +0 -7
  317. package/dist/schemas/nodes/block/index.js.map +0 -1
  318. package/dist/schemas/nodes/index.d.ts +0 -9
  319. package/dist/schemas/nodes/index.d.ts.map +0 -1
  320. package/dist/schemas/nodes/index.js +0 -12
  321. package/dist/schemas/nodes/index.js.map +0 -1
  322. package/dist/schemas/nodes/inline/InlineSchema.d.ts +0 -48
  323. package/dist/schemas/nodes/inline/InlineSchema.d.ts.map +0 -1
  324. package/dist/schemas/nodes/inline/InlineSchema.js +0 -436
  325. package/dist/schemas/nodes/inline/InlineSchema.js.map +0 -1
  326. package/dist/schemas/nodes/inline/index.d.ts +0 -7
  327. package/dist/schemas/nodes/inline/index.d.ts.map +0 -1
  328. package/dist/schemas/nodes/inline/index.js +0 -7
  329. package/dist/schemas/nodes/inline/index.js.map +0 -1
  330. package/dist/schemas/nodes/macro/MacroSchema.d.ts +0 -27
  331. package/dist/schemas/nodes/macro/MacroSchema.d.ts.map +0 -1
  332. package/dist/schemas/nodes/macro/MacroSchema.js +0 -162
  333. package/dist/schemas/nodes/macro/MacroSchema.js.map +0 -1
  334. package/dist/schemas/nodes/macro/index.d.ts +0 -7
  335. package/dist/schemas/nodes/macro/index.d.ts.map +0 -1
  336. package/dist/schemas/nodes/macro/index.js +0 -7
  337. package/dist/schemas/nodes/macro/index.js.map +0 -1
  338. package/dist/schemas/preprocessing/ConfluencePreprocessor.d.ts +0 -53
  339. package/dist/schemas/preprocessing/ConfluencePreprocessor.d.ts.map +0 -1
  340. package/dist/schemas/preprocessing/ConfluencePreprocessor.js +0 -349
  341. package/dist/schemas/preprocessing/ConfluencePreprocessor.js.map +0 -1
  342. package/dist/schemas/preprocessing/index.d.ts +0 -8
  343. package/dist/schemas/preprocessing/index.d.ts.map +0 -1
  344. package/dist/schemas/preprocessing/index.js +0 -2
  345. package/dist/schemas/preprocessing/index.js.map +0 -1
  346. package/dist/serializers/ConfluenceSerializer.d.ts +0 -30
  347. package/dist/serializers/ConfluenceSerializer.d.ts.map +0 -1
  348. package/dist/serializers/ConfluenceSerializer.js +0 -551
  349. package/dist/serializers/ConfluenceSerializer.js.map +0 -1
  350. package/dist/serializers/MarkdownSerializer.d.ts +0 -34
  351. package/dist/serializers/MarkdownSerializer.d.ts.map +0 -1
  352. package/dist/serializers/MarkdownSerializer.js +0 -355
  353. package/dist/serializers/MarkdownSerializer.js.map +0 -1
  354. package/dist/serializers/index.d.ts +0 -8
  355. package/dist/serializers/index.d.ts.map +0 -1
  356. package/dist/serializers/index.js +0 -8
  357. package/dist/serializers/index.js.map +0 -1
@@ -6,107 +6,269 @@
6
6
  * Requires:
7
7
  * - CONFLUENCE_BASE_URL: Confluence base URL
8
8
  * - CONFLUENCE_ROOT_PAGE_ID: Test page ID
9
- * - OAuth tokens in ~/.confluence/ or CONFLUENCE_API_KEY + CONFLUENCE_EMAIL env vars
9
+ * - CONFLUENCE_API_KEY + CONFLUENCE_EMAIL env vars for raw ADF verification
10
10
  */
11
- import { execFileSync } from "node:child_process"
12
- import * as fs from "node:fs"
13
- import * as os from "node:os"
14
- import * as path from "node:path"
11
+ import * as NodeServices from "@effect/platform-node/NodeServices"
12
+ import { Config, Effect, Option } from "effect"
13
+ import * as FileSystem from "effect/FileSystem"
14
+ import * as Path from "effect/Path"
15
+ import * as ChildProcess from "effect/unstable/process/ChildProcess"
16
+ import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"
15
17
  import { afterAll, beforeAll, describe, expect, it } from "vitest"
16
18
 
17
- const CLI_PATH = path.resolve(__dirname, "../dist/bin.js")
18
- const BASE_URL = process.env.CONFLUENCE_BASE_URL
19
- const ROOT_PAGE_ID = process.env.CONFLUENCE_ROOT_PAGE_ID
19
+ let CLI_PATH = ""
20
+ let BASE_URL = ""
21
+ let ROOT_PAGE_ID = ""
22
+ let CONFLUENCE_EMAIL = ""
23
+ let CONFLUENCE_API_KEY = ""
24
+ let HAS_INTEGRATION_CONFIG = false
25
+ let HAS_API_AUTH_CONFIG = false
26
+
27
+ const SHOULD_RUN_INTEGRATION = Effect.runSync(
28
+ Effect.all([
29
+ Config.option(Config.string("CONFLUENCE_BASE_URL")),
30
+ Config.option(Config.string("CONFLUENCE_ROOT_PAGE_ID")),
31
+ Config.option(Config.string("CONFLUENCE_EMAIL")),
32
+ Config.option(Config.string("CONFLUENCE_API_KEY"))
33
+ ]).pipe(
34
+ Effect.map(([baseUrl, rootPageId, email, apiKey]) =>
35
+ Option.isSome(baseUrl) && Option.isSome(rootPageId) && Option.isSome(email) && Option.isSome(apiKey)
36
+ )
37
+ )
38
+ )
20
39
 
21
40
  // Test state
22
41
  interface TestState {
23
42
  testDir: string
24
43
  pageFile: string | null
25
- pageSlug: string | null
26
44
  pageId: string | null
27
45
  }
28
46
 
29
47
  const state: TestState = {
30
48
  testDir: "",
31
49
  pageFile: null,
32
- pageSlug: null,
33
50
  pageId: null
34
51
  }
35
52
 
36
- // === Helper Functions ===
53
+ const timestampForTitle = (date: Date): string => date.toISOString().replace(/[:.]/g, "-")
54
+
55
+ const timestampLine = (label: string, date: Date): string => `${label} at ${date.toISOString()}`
56
+
57
+ const RAW_ROUND_TRIP_NODE_TYPES = [
58
+ "blockCard",
59
+ "bodiedExtension",
60
+ "date",
61
+ "decisionItem",
62
+ "decisionList",
63
+ "embedCard",
64
+ "emoji",
65
+ "expand",
66
+ "extension",
67
+ "inlineCard",
68
+ "inlineExtension",
69
+ "layoutColumn",
70
+ "layoutSection",
71
+ "nestedExpand",
72
+ "panel",
73
+ "status",
74
+ "table",
75
+ "tableCell",
76
+ "tableHeader",
77
+ "tableRow",
78
+ "taskItem",
79
+ "taskList"
80
+ ] as const
81
+
82
+ const RAW_ROUND_TRIP_MARK_TYPES = [
83
+ "alignment",
84
+ "backgroundColor",
85
+ "indentation",
86
+ "subsup",
87
+ "textColor",
88
+ "underline"
89
+ ] as const
90
+
91
+ interface RawAdfEvidence {
92
+ readonly types: Set<string>
93
+ readonly attrSignatures: Set<string>
94
+ readonly markTypes: Set<string>
95
+ readonly markSignatures: Set<string>
96
+ readonly paragraphMarkSignatures: Set<string>
97
+ readonly inlineCardUrls: Set<string>
98
+ }
37
99
 
38
- const runCli = (args: ReadonlyArray<string>, options?: { timeout?: number }): string => {
39
- return execFileSync("node", [CLI_PATH, ...args], {
40
- cwd: state.testDir,
41
- encoding: "utf-8",
42
- timeout: options?.timeout ?? 60000
43
- })
100
+ interface RawAdfSnapshot {
101
+ readonly value: string
102
+ readonly evidence: RawAdfEvidence
44
103
  }
45
104
 
46
- const findFile = (dir: string, predicate: (name: string) => boolean): string | null => {
47
- if (!fs.existsSync(dir)) return null
48
- const entries = fs.readdirSync(dir, { withFileTypes: true })
49
- for (const entry of entries) {
50
- const fullPath = path.join(dir, entry.name)
51
- if (entry.isDirectory()) {
52
- const found = findFile(fullPath, predicate)
53
- if (found) return found
54
- } else if (predicate(entry.name)) {
55
- return fullPath
56
- }
105
+ // === Helper Functions ===
106
+
107
+ const runPlatform = <A, E>(effect: Effect.Effect<A, E, NodeServices.NodeServices>): Promise<A> =>
108
+ Effect.runPromise(effect.pipe(Effect.provide(NodeServices.layer)))
109
+
110
+ const runCli = (args: ReadonlyArray<string>, options?: { timeout?: number }) =>
111
+ Effect.gen(function*() {
112
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
113
+ const command = ChildProcess.make("node", [CLI_PATH, ...args], {
114
+ cwd: state.testDir,
115
+ stderr: "inherit"
116
+ })
117
+ return yield* spawner.string(command)
118
+ }).pipe(Effect.timeout(`${options?.timeout ?? 60000} millis`))
119
+
120
+ const pathExists = (filePath: string) =>
121
+ FileSystem.FileSystem.pipe(
122
+ Effect.flatMap((fs) => fs.exists(filePath))
123
+ )
124
+
125
+ const readText = (filePath: string) =>
126
+ FileSystem.FileSystem.pipe(
127
+ Effect.flatMap((fs) => fs.readFileString(filePath))
128
+ )
129
+
130
+ const writeText = (filePath: string, content: string) =>
131
+ FileSystem.FileSystem.pipe(
132
+ Effect.flatMap((fs) => fs.writeFileString(filePath, content))
133
+ )
134
+
135
+ const removePath = (filePath: string, options?: { readonly recursive?: boolean; readonly force?: boolean }) =>
136
+ FileSystem.FileSystem.pipe(
137
+ Effect.flatMap((fs) => fs.remove(filePath, options))
138
+ )
139
+
140
+ const joinPath = (...parts: ReadonlyArray<string>) =>
141
+ Path.Path.pipe(
142
+ Effect.map((path) => path.join(...parts))
143
+ )
144
+
145
+ const dirname = (filePath: string) =>
146
+ Path.Path.pipe(
147
+ Effect.map((path) => path.dirname(filePath))
148
+ )
149
+
150
+ const initializeTestEnvironment = Effect.gen(function*() {
151
+ const fs = yield* FileSystem.FileSystem
152
+ const path = yield* Path.Path
153
+ CLI_PATH = yield* path.fromFileUrl(new URL("../dist/bin.js", import.meta.url))
154
+ const baseUrl = yield* Config.option(Config.string("CONFLUENCE_BASE_URL"))
155
+ const rootPageId = yield* Config.option(Config.string("CONFLUENCE_ROOT_PAGE_ID"))
156
+ const email = yield* Config.option(Config.string("CONFLUENCE_EMAIL"))
157
+ const apiKey = yield* Config.option(Config.string("CONFLUENCE_API_KEY"))
158
+ if (Option.isNone(baseUrl) || Option.isNone(rootPageId)) {
159
+ HAS_INTEGRATION_CONFIG = false
160
+ return
57
161
  }
58
- return null
59
- }
162
+ BASE_URL = baseUrl.value
163
+ ROOT_PAGE_ID = rootPageId.value
164
+ if (Option.isSome(email) && Option.isSome(apiKey)) {
165
+ CONFLUENCE_EMAIL = email.value
166
+ CONFLUENCE_API_KEY = apiKey.value
167
+ HAS_API_AUTH_CONFIG = true
168
+ }
169
+ HAS_INTEGRATION_CONFIG = true
170
+ state.testDir = yield* fs.makeTempDirectory({ prefix: "confluence-test-" })
171
+ })
60
172
 
61
- const findTemplate = (): string | null => {
62
- const docsDir = path.join(state.testDir, ".confluence/docs")
63
- return findFile(docsDir, (name) => name.toLowerCase().includes("template") && name.endsWith(".md"))
64
- }
173
+ const cleanupTestEnvironment = Effect.gen(function*() {
174
+ if (state.pageId !== null && HAS_API_AUTH_CONFIG) {
175
+ yield* deleteRemotePageIfPresent(state.pageId).pipe(Effect.ignore)
176
+ }
177
+ if (state.testDir === "") {
178
+ return
179
+ }
180
+ const exists = yield* pathExists(state.testDir)
181
+ if (exists) {
182
+ yield* removePath(state.testDir, { recursive: true, force: true })
183
+ }
184
+ })
65
185
 
66
- const findPageBySlug = (slug: string): string | null => {
67
- const docsDir = path.join(state.testDir, ".confluence/docs")
68
- return findFile(docsDir, (name) => name === `${slug}.md`)
69
- }
186
+ const findSeedMarkdownFile = Effect.gen(function*() {
187
+ const fs = yield* FileSystem.FileSystem
188
+ const path = yield* Path.Path
189
+ const docsDir = yield* joinPath(state.testDir, ".confluence/docs")
190
+ const exists = yield* fs.exists(docsDir)
191
+ if (!exists) return null
192
+
193
+ const entries = yield* fs.readDirectory(docsDir, { recursive: true })
194
+ const markdownFiles = entries
195
+ .filter((entry) => path.basename(entry).endsWith(".md"))
196
+ .sort((a, b) => {
197
+ const aName = path.basename(a).toLowerCase()
198
+ const bName = path.basename(b).toLowerCase()
199
+ const aIsTemplate = aName.includes("template")
200
+ const bIsTemplate = bName.includes("template")
201
+ if (aIsTemplate !== bIsTemplate) return aIsTemplate ? -1 : 1
202
+ return b.split(/[\\/]/).length - a.split(/[\\/]/).length
203
+ })
204
+
205
+ const entry = markdownFiles[0]
206
+ if (!entry) return null
207
+ return path.isAbsolute(entry) ? entry : path.join(docsDir, entry)
208
+ })
209
+
210
+ const findPageByPageId = (pageId: string) =>
211
+ Effect.gen(function*() {
212
+ const fs = yield* FileSystem.FileSystem
213
+ const path = yield* Path.Path
214
+ const docsDir = yield* joinPath(state.testDir, ".confluence/docs")
215
+ const exists = yield* fs.exists(docsDir)
216
+ if (!exists) return null
217
+
218
+ const entries = yield* fs.readDirectory(docsDir, { recursive: true })
219
+ for (const entry of entries) {
220
+ if (!path.basename(entry).endsWith(".md")) continue
221
+ const filePath = path.isAbsolute(entry) ? entry : path.join(docsDir, entry)
222
+ const content = yield* readText(filePath)
223
+ if (content.match(new RegExp(`pageId:\\s*["']?${pageId}["']?`))) {
224
+ return filePath
225
+ }
226
+ }
227
+ return null
228
+ })
70
229
 
71
230
  // === Step Functions ===
72
231
 
73
232
  /**
74
233
  * Clone pages from Confluence with full history.
75
234
  */
76
- const clonePages = (): string => {
77
- const output = runCli(["clone", "--root-page-id", ROOT_PAGE_ID, "--base-url", BASE_URL], { timeout: 120000 })
235
+ const clonePages = Effect.gen(function*() {
236
+ const output = yield* runCli(["clone", "--root-page-id", ROOT_PAGE_ID, "--base-url", BASE_URL], { timeout: 120000 })
78
237
 
79
238
  expect(output).toContain("Cloning pages from Confluence")
80
239
  expect(output).toMatch(/Cloned \d+ pages with \d+ commits/)
81
- expect(fs.existsSync(path.join(state.testDir, ".confluence"))).toBe(true)
82
- expect(fs.existsSync(path.join(state.testDir, ".confluence/config.json"))).toBe(true)
83
- expect(fs.existsSync(path.join(state.testDir, ".confluence/.git"))).toBe(true)
240
+ expect(yield* pathExists(yield* joinPath(state.testDir, ".confluence"))).toBe(true)
241
+ expect(yield* pathExists(yield* joinPath(state.testDir, ".confluence/config.json"))).toBe(true)
242
+ expect(yield* pathExists(yield* joinPath(state.testDir, ".confluence/.git"))).toBe(true)
84
243
 
85
244
  return output
86
- }
245
+ })
87
246
 
88
247
  /**
89
248
  * Remove .confluence directory for fresh clone.
90
249
  */
91
- const removeConfluenceDir = (): void => {
92
- fs.rmSync(path.join(state.testDir, ".confluence"), { recursive: true, force: true })
93
- }
250
+ const removeConfluenceDir = Effect.gen(function*() {
251
+ yield* removePath(yield* joinPath(state.testDir, ".confluence"), { recursive: true, force: true })
252
+ })
94
253
 
95
254
  /**
96
- * Create a new page by copying template content.
255
+ * Create a new page by copying content from a cloned seed page.
97
256
  */
98
- const createPageFromTemplate = (): { file: string; slug: string } => {
99
- const templatePath = findTemplate()
100
- expect(templatePath).not.toBeNull()
101
-
102
- const templateContent = fs.readFileSync(templatePath!, "utf-8")
103
- const contentMatch = templateContent.match(/^---[\s\S]*?---\s*([\s\S]*)$/)
104
- const bodyContent = contentMatch ? contentMatch[1]!.trim() : templateContent
105
-
106
- const templateDir = path.dirname(templatePath!)
107
- const timestamp = Date.now()
257
+ const createPageFromSeed = Effect.gen(function*() {
258
+ const seedPath = yield* findSeedMarkdownFile
259
+ expect(seedPath).not.toBeNull()
260
+
261
+ const seedContent = yield* readText(seedPath!)
262
+ const contentMatch = seedContent.match(/^---[\s\S]*?---\s*([\s\S]*)$/)
263
+ const bodyContent = contentMatch ? contentMatch[1]!.trim() : seedContent
264
+ const seedPageId = extractPageIdFromMarkdown(seedContent)
265
+
266
+ const templateDir = yield* dirname(seedPath!)
267
+ const createdAt = new Date()
268
+ const timestamp = timestampForTitle(createdAt)
108
269
  const slug = `integration-test-${timestamp}`
109
- const file = path.join(templateDir, `${slug}.md`)
270
+ const file = yield* joinPath(templateDir, `${slug}.md`)
271
+ const createMarker = timestampLine("Created by integration test", createdAt)
110
272
 
111
273
  const newPageContent = `---
112
274
  title: "Integration Test ${timestamp}"
@@ -115,31 +277,31 @@ title: "Integration Test ${timestamp}"
115
277
  ${bodyContent}
116
278
 
117
279
  ---
118
- Created by integration test at ${new Date().toISOString()}
280
+ ${createMarker}
119
281
  `
120
- fs.writeFileSync(file, newPageContent)
121
- expect(fs.existsSync(file)).toBe(true)
282
+ yield* writeText(file, newPageContent)
283
+ expect(yield* pathExists(file)).toBe(true)
122
284
 
123
285
  state.pageFile = file
124
- state.pageSlug = slug
125
286
 
126
- return { file, slug }
127
- }
287
+ return { file, createMarker, seedPageId, timestamp }
288
+ })
128
289
 
129
290
  /**
130
291
  * Commit current changes.
131
292
  */
132
- const commitChanges = (message: string): string => {
133
- const output = runCli(["commit", "-m", message])
134
- expect(output).toContain("Committed:")
135
- return output
136
- }
293
+ const commitChanges = (message: string) =>
294
+ Effect.gen(function*() {
295
+ const output = yield* runCli(["commit", "-m", message])
296
+ expect(output).toContain("Committed:")
297
+ return output
298
+ })
137
299
 
138
300
  /**
139
301
  * Push changes to Confluence.
140
302
  */
141
- const pushChanges = (): { pushed: number; created: number; deleted: number } => {
142
- const output = runCli(["push"], { timeout: 90000 })
303
+ const pushChanges = Effect.gen(function*() {
304
+ const output = yield* runCli(["push"], { timeout: 90000 })
143
305
 
144
306
  const pushedMatch = output.match(/Pushed:\s*(\d+)/)
145
307
  const createdMatch = output.match(/Created:\s*(\d+)/)
@@ -150,20 +312,23 @@ const pushChanges = (): { pushed: number; created: number; deleted: number } =>
150
312
  created: createdMatch ? parseInt(createdMatch[1]!, 10) : 0,
151
313
  deleted: deletedMatch ? parseInt(deletedMatch[1]!, 10) : 0
152
314
  }
153
- }
315
+ })
154
316
 
155
317
  /**
156
318
  * Pull changes from Confluence.
157
319
  */
158
- const pullChanges = (): string => {
159
- return runCli(["pull"])
160
- }
320
+ const pullChanges = runCli(["pull"])
161
321
 
162
322
  /**
163
323
  * Extract pageId from file front-matter.
164
324
  */
165
- const extractPageId = (filePath: string): string | null => {
166
- const content = fs.readFileSync(filePath, "utf-8")
325
+ const extractPageId = (filePath: string) =>
326
+ Effect.gen(function*() {
327
+ const content = yield* readText(filePath)
328
+ return extractPageIdFromMarkdown(content)
329
+ })
330
+
331
+ const extractPageIdFromMarkdown = (content: string): string | null => {
167
332
  const match = content.match(/pageId:\s*["']?(\d+)/)
168
333
  return match ? match[1]! : null
169
334
  }
@@ -171,99 +336,347 @@ const extractPageId = (filePath: string): string | null => {
171
336
  /**
172
337
  * Modify page content.
173
338
  */
174
- const modifyPage = (filePath: string, marker: string): void => {
175
- const content = fs.readFileSync(filePath, "utf-8")
176
- fs.writeFileSync(filePath, content + `\n\n${marker}\n`)
177
- }
339
+ const modifyPage = (filePath: string, marker: string) =>
340
+ Effect.gen(function*() {
341
+ const content = yield* readText(filePath)
342
+ yield* writeText(filePath, content + `\n\n${marker}\n`)
343
+ })
178
344
 
179
345
  /**
180
346
  * Delete a local file.
181
347
  */
182
- const deleteLocalFile = (filePath: string): void => {
183
- fs.unlinkSync(filePath)
184
- expect(fs.existsSync(filePath)).toBe(false)
348
+ const deleteLocalFile = (filePath: string) =>
349
+ Effect.gen(function*() {
350
+ yield* removePath(filePath)
351
+ expect(yield* pathExists(filePath)).toBe(false)
352
+ })
353
+
354
+ const adfEvidence = (adf: unknown): RawAdfEvidence => {
355
+ const evidence: RawAdfEvidence = {
356
+ types: new Set<string>(),
357
+ attrSignatures: new Set<string>(),
358
+ markTypes: new Set<string>(),
359
+ markSignatures: new Set<string>(),
360
+ paragraphMarkSignatures: new Set<string>(),
361
+ inlineCardUrls: new Set<string>()
362
+ }
363
+ const selectedMarkTypes = new Set<string>(RAW_ROUND_TRIP_MARK_TYPES)
364
+ const selectedNodeTypes = new Set<string>(RAW_ROUND_TRIP_NODE_TYPES)
365
+ const normalizeAttrs = (value: unknown): unknown => {
366
+ if (Array.isArray(value)) return value.map(normalizeAttrs)
367
+ if (value !== null && typeof value === "object") {
368
+ const entries = Object.entries(value as Record<string, unknown>)
369
+ .map(([key, v]) => [key, normalizeAttrs(v)] as const)
370
+ .filter(([key, v]) => {
371
+ if (key === "localId" || key === "macroMetadata") return false
372
+ if (key === "layout" && v === "default") return false
373
+ if (key === "macroId" && v !== null && typeof v === "object") return false
374
+ if (
375
+ (key === "macroParams" || key === "parameters") &&
376
+ v !== null &&
377
+ typeof v === "object" &&
378
+ !Array.isArray(v) &&
379
+ Object.keys(v).length === 0
380
+ ) {
381
+ return false
382
+ }
383
+ return true
384
+ })
385
+ return Object.fromEntries(entries)
386
+ }
387
+ return value
388
+ }
389
+ const stableJson = (value: unknown): string => {
390
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`
391
+ if (value !== null && typeof value === "object") {
392
+ return `{${
393
+ Object.entries(value as Record<string, unknown>)
394
+ .filter(([, v]) => v !== undefined)
395
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
396
+ .map(([k, v]) => `${JSON.stringify(k)}:${stableJson(v)}`)
397
+ .join(",")
398
+ }}`
399
+ }
400
+ return JSON.stringify(value) ?? "null"
401
+ }
402
+ const markSignature = (mark: Record<string, unknown>): string =>
403
+ `${String(mark["type"])}:${stableJson(mark["attrs"] ?? {})}`
404
+ const cardUrl = (attrs: Record<string, unknown>): string | null => {
405
+ const url = attrs["url"]
406
+ if (typeof url === "string") return url
407
+ const data = attrs["data"]
408
+ if (data !== null && typeof data === "object" && !Array.isArray(data)) {
409
+ const dataUrl = (data as Record<string, unknown>)["url"]
410
+ if (typeof dataUrl === "string") return dataUrl
411
+ }
412
+ return null
413
+ }
414
+ const walk = (node: unknown): void => {
415
+ if (node === null || typeof node !== "object") return
416
+ const record = node as Record<string, unknown>
417
+ const type = record["type"]
418
+ if (typeof type === "string") evidence.types.add(type)
419
+ const attrs = record["attrs"]
420
+ if (
421
+ typeof type === "string" &&
422
+ selectedNodeTypes.has(type) &&
423
+ attrs !== null &&
424
+ typeof attrs === "object" &&
425
+ !Array.isArray(attrs)
426
+ ) {
427
+ const normalized = normalizeAttrs(attrs)
428
+ const extensionKey = (normalized as Record<string, unknown>)["extensionKey"]
429
+ if (!(type === "extension" && extensionKey === "toc")) {
430
+ evidence.attrSignatures.add(`${type}:${stableJson(normalized)}`)
431
+ }
432
+ }
433
+ if (type === "inlineCard") {
434
+ if (attrs !== null && typeof attrs === "object" && !Array.isArray(attrs)) {
435
+ const url = cardUrl(attrs as Record<string, unknown>)
436
+ if (url !== null) evidence.inlineCardUrls.add(url)
437
+ }
438
+ }
439
+ const marks = record["marks"]
440
+ if (Array.isArray(marks)) {
441
+ for (const mark of marks) {
442
+ if (mark === null || typeof mark !== "object" || Array.isArray(mark)) continue
443
+ const markRecord = mark as Record<string, unknown>
444
+ const markType = markRecord["type"]
445
+ if (typeof markType !== "string") continue
446
+ evidence.markTypes.add(markType)
447
+ if (selectedMarkTypes.has(markType)) {
448
+ const signature = markSignature(markRecord)
449
+ evidence.markSignatures.add(signature)
450
+ if (type === "paragraph") evidence.paragraphMarkSignatures.add(signature)
451
+ }
452
+ }
453
+ }
454
+ for (const child of Object.values(record)) {
455
+ if (Array.isArray(child)) {
456
+ for (const item of child) walk(item)
457
+ } else {
458
+ walk(child)
459
+ }
460
+ }
461
+ }
462
+ walk(adf)
463
+ return evidence
185
464
  }
186
465
 
187
- // === Tests ===
466
+ const getRemoteAdfSnapshot = (pageId: string) =>
467
+ Effect.tryPromise({
468
+ try: async () => {
469
+ const response = await fetch(`${BASE_URL}/wiki/api/v2/pages/${pageId}?body-format=atlas_doc_format`, {
470
+ headers: {
471
+ Authorization: `Basic ${btoa(`${CONFLUENCE_EMAIL}:${CONFLUENCE_API_KEY}`)}`
472
+ }
473
+ })
474
+ if (!response.ok) {
475
+ throw new Error(`Confluence returned ${response.status} for page ${pageId}`)
476
+ }
477
+ const page = await response.json() as { body?: { atlas_doc_format?: { value?: string } } }
478
+ const value = page.body?.atlas_doc_format?.value ?? "{}"
479
+ const adf = JSON.parse(value) as unknown
480
+ return { value, evidence: adfEvidence(adf) }
481
+ },
482
+ catch: (cause) => cause
483
+ })
188
484
 
189
- describe("CLI Integration - Page Creation Flow", () => {
190
- beforeAll(() => {
191
- state.testDir = fs.mkdtempSync(path.join(os.tmpdir(), "confluence-test-"))
485
+ const deleteRemotePageIfPresent = (pageId: string) =>
486
+ Effect.tryPromise({
487
+ try: async () => {
488
+ const response = await fetch(`${BASE_URL}/wiki/api/v2/pages/${pageId}`, {
489
+ method: "DELETE",
490
+ headers: {
491
+ Authorization: `Basic ${btoa(`${CONFLUENCE_EMAIL}:${CONFLUENCE_API_KEY}`)}`
492
+ }
493
+ })
494
+ if (!response.ok && response.status !== 404) {
495
+ throw new Error(`Confluence returned ${response.status} while cleaning up page ${pageId}`)
496
+ }
497
+ },
498
+ catch: (cause) => cause
192
499
  })
193
500
 
194
- afterAll(() => {
195
- if (state.testDir && fs.existsSync(state.testDir)) {
196
- fs.rmSync(state.testDir, { recursive: true, force: true })
501
+ const expectNativePanelsIfPresent = (pageId: string, markdown: string) =>
502
+ Effect.gen(function*() {
503
+ if (!HAS_API_AUTH_CONFIG || !markdown.includes("adf:panel")) {
504
+ return
197
505
  }
506
+ const snapshot = yield* getRemoteAdfSnapshot(pageId)
507
+ expect(snapshot.evidence.types.has("panel")).toBe(true)
198
508
  })
199
509
 
200
- it("full cycle: clone -> create -> push -> pull -> modify -> push -> re-clone -> delete -> verify", () => {
201
- // 1. Clone pages from Confluence
202
- clonePages()
203
-
204
- // 2. Create new page from template
205
- const { file, slug } = createPageFromTemplate()
206
-
207
- // 3. Commit and push new page
208
- commitChanges("Add integration test page")
209
-
210
- const statusBefore = runCli(["status"])
211
- expect(statusBefore).toContain("Local Only:")
212
-
213
- const pushResult1 = pushChanges()
214
- expect(pushResult1.created).toBe(1)
215
-
216
- // Verify file has pageId after push
217
- const contentAfterPush = fs.readFileSync(file, "utf-8")
218
- expect(contentAfterPush).toMatch(/pageId:\s*["']?\d+/)
219
- expect(contentAfterPush).toMatch(/version:\s*\d+/)
220
- expect(contentAfterPush).toMatch(/contentHash:/)
221
-
222
- const pageId = extractPageId(file)
223
- expect(pageId).not.toBeNull()
224
- state.pageId = pageId
225
-
226
- // 4. Pull should be no-op (already in sync)
227
- const contentBeforePull = fs.readFileSync(file, "utf-8")
228
- pullChanges()
229
- const contentAfterPull = fs.readFileSync(file, "utf-8")
230
- expect(contentAfterPull).toBe(contentBeforePull)
231
-
232
- // 5. Modify page, commit, and push
233
- const modifyMarker = `Modified at ${Date.now()}`
234
- modifyPage(file, modifyMarker)
235
- commitChanges("Modify integration test page")
236
-
237
- const pushResult2 = pushChanges()
238
- expect(pushResult2.pushed).toBe(1)
239
-
240
- const contentAfterModify = fs.readFileSync(file, "utf-8")
241
- expect(contentAfterModify).toContain(modifyMarker)
242
-
243
- // 6. Remove and re-clone - verify idempotency
244
- const contentBeforeReclone = fs.readFileSync(file, "utf-8")
245
- removeConfluenceDir()
246
- clonePages()
247
-
248
- const reclonedFile = findPageBySlug(slug)
249
- expect(reclonedFile).not.toBeNull()
250
- expect(fs.existsSync(reclonedFile!)).toBe(true)
251
-
252
- const contentAfterReclone = fs.readFileSync(reclonedFile!, "utf-8")
253
- expect(contentAfterReclone).toBe(contentBeforeReclone)
510
+ const expectRawRoundTripTypes = (
511
+ before: RawAdfSnapshot,
512
+ after: RawAdfSnapshot
513
+ ) =>
514
+ Effect.sync(() => {
515
+ for (const type of RAW_ROUND_TRIP_NODE_TYPES) {
516
+ if (before.evidence.types.has(type)) {
517
+ expect(after.evidence.types.has(type), `expected raw ADF after push to preserve ${type}`).toBe(true)
518
+ }
519
+ }
520
+ for (const signature of before.evidence.attrSignatures) {
521
+ expect(after.evidence.attrSignatures.has(signature), `expected raw ADF after push to preserve attrs ${signature}`)
522
+ .toBe(true)
523
+ }
524
+ for (const mark of RAW_ROUND_TRIP_MARK_TYPES) {
525
+ if (before.evidence.markTypes.has(mark)) {
526
+ expect(after.evidence.markTypes.has(mark), `expected raw ADF after push to preserve ${mark} marks`).toBe(true)
527
+ }
528
+ }
529
+ for (const signature of before.evidence.markSignatures) {
530
+ expect(after.evidence.markSignatures.has(signature), `expected raw ADF after push to preserve mark ${signature}`)
531
+ .toBe(true)
532
+ }
533
+ for (const signature of before.evidence.paragraphMarkSignatures) {
534
+ expect(
535
+ after.evidence.paragraphMarkSignatures.has(signature),
536
+ `expected raw ADF after push to preserve paragraph mark ${signature}`
537
+ ).toBe(true)
538
+ }
539
+ for (const url of before.evidence.inlineCardUrls) {
540
+ expect(after.evidence.inlineCardUrls.has(url), `expected raw ADF after push to preserve inlineCard ${url}`).toBe(
541
+ true
542
+ )
543
+ }
544
+ })
254
545
 
255
- // 7. Delete page via git workflow
256
- deleteLocalFile(reclonedFile!)
257
- commitChanges("Delete integration test page")
546
+ const expectSidecarMetadata = (filePath: string, pageId: string, markdown: string) =>
547
+ Effect.gen(function*() {
548
+ const dir = yield* dirname(filePath)
549
+ const sidecarPath = yield* joinPath(dir, `${pageId}.adf.json`)
550
+ expect(markdown).toMatch(new RegExp(`ref=\\./${pageId}\\.adf\\.json#[A-Za-z0-9-]+`))
551
+ expect(markdown).not.toMatch(/<!--\s*adf:[^>]+(?:attrs|node|marks)=/)
552
+ expect(yield* pathExists(sidecarPath)).toBe(true)
553
+
554
+ const sidecar = JSON.parse(yield* readText(sidecarPath)) as unknown
555
+ expect(sidecar).toMatchObject({ version: 1 })
556
+
557
+ const record = sidecar !== null && typeof sidecar === "object" && !Array.isArray(sidecar)
558
+ ? sidecar as Record<string, unknown>
559
+ : {}
560
+ const entries = record["entries"] !== null && typeof record["entries"] === "object" &&
561
+ !Array.isArray(record["entries"])
562
+ ? record["entries"] as Record<string, unknown>
563
+ : {}
564
+ expect(Object.keys(entries).length).toBeGreaterThan(0)
565
+ expect(
566
+ Object.values(entries).some((entry) => {
567
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return false
568
+ const value = (entry as Record<string, unknown>)["value"]
569
+ return value !== null && typeof value === "object"
570
+ })
571
+ ).toBe(true)
572
+ })
258
573
 
259
- const pushResult3 = pushChanges()
260
- expect(pushResult3.deleted).toBe(1)
574
+ // === Tests ===
261
575
 
262
- // 8. Verify deletion - re-clone should not include the page
263
- removeConfluenceDir()
264
- clonePages()
576
+ describe("CLI Integration - Page Creation Flow", () => {
577
+ beforeAll(async () => {
578
+ await runPlatform(initializeTestEnvironment)
579
+ })
265
580
 
266
- const deletedFile = findPageBySlug(slug)
267
- expect(deletedFile).toBeNull()
581
+ afterAll(async () => {
582
+ await runPlatform(cleanupTestEnvironment)
268
583
  })
584
+
585
+ it.skipIf(!SHOULD_RUN_INTEGRATION)(
586
+ "full cycle: clone -> create -> push -> pull -> modify -> push -> re-clone -> delete -> verify",
587
+ async () => {
588
+ if (!HAS_INTEGRATION_CONFIG) {
589
+ throw new Error("Confluence integration config was available at test definition but missing at setup")
590
+ }
591
+
592
+ await runPlatform(Effect.gen(function*() {
593
+ // 1. Clone pages from Confluence
594
+ yield* clonePages
595
+
596
+ // 2. Create new page from template
597
+ const { createMarker, file, seedPageId, timestamp } = yield* createPageFromSeed
598
+ const seedRawAdf = seedPageId && HAS_API_AUTH_CONFIG ? yield* getRemoteAdfSnapshot(seedPageId) : null
599
+
600
+ // 3. Commit and push new page
601
+ yield* commitChanges(`Add integration test page ${timestamp}`)
602
+
603
+ const statusBefore = yield* runCli(["status"])
604
+ expect(statusBefore).toContain("Local Only:")
605
+
606
+ const pushResult1 = yield* pushChanges
607
+ expect(pushResult1.created).toBe(1)
608
+
609
+ // Verify file has pageId after push
610
+ const contentAfterPush = yield* readText(file)
611
+ expect(contentAfterPush).toMatch(/pageId:\s*["']?\d+/)
612
+ expect(contentAfterPush).toMatch(/version:\s*\d+/)
613
+ expect(contentAfterPush).toMatch(/contentHash:/)
614
+ expect(contentAfterPush).toContain(createMarker)
615
+
616
+ const pageId = yield* extractPageId(file)
617
+ expect(pageId).not.toBeNull()
618
+ state.pageId = pageId
619
+ yield* expectSidecarMetadata(file, pageId!, contentAfterPush)
620
+ yield* expectNativePanelsIfPresent(pageId!, contentAfterPush)
621
+ const createdRawAdf = HAS_API_AUTH_CONFIG ? yield* getRemoteAdfSnapshot(pageId!) : null
622
+ if (seedRawAdf !== null && createdRawAdf !== null) {
623
+ yield* expectRawRoundTripTypes(seedRawAdf, createdRawAdf)
624
+ }
625
+
626
+ // 4. Pull should be no-op (already in sync)
627
+ const contentBeforePull = yield* readText(file)
628
+ yield* pullChanges
629
+ const contentAfterPull = yield* readText(file)
630
+ expect(contentAfterPull).toBe(contentBeforePull)
631
+
632
+ // 5. Modify page, commit, and push
633
+ const modifiedAt = new Date()
634
+ const modifyMarker = timestampLine("Modified by integration test", modifiedAt)
635
+ yield* modifyPage(file, modifyMarker)
636
+ yield* commitChanges(`Modify integration test page ${timestampForTitle(modifiedAt)}`)
637
+
638
+ const pushResult2 = yield* pushChanges
639
+ expect(pushResult2.pushed).toBe(1)
640
+
641
+ const contentAfterModify = yield* readText(file)
642
+ expect(contentAfterModify).toContain(modifyMarker)
643
+ yield* expectSidecarMetadata(file, pageId!, contentAfterModify)
644
+ yield* expectNativePanelsIfPresent(pageId!, contentAfterModify)
645
+ const modifiedRawAdf = HAS_API_AUTH_CONFIG ? yield* getRemoteAdfSnapshot(pageId!) : null
646
+ if (createdRawAdf !== null && modifiedRawAdf !== null) {
647
+ yield* expectRawRoundTripTypes(createdRawAdf, modifiedRawAdf)
648
+ }
649
+
650
+ // 6. Remove and re-clone - verify idempotency
651
+ const contentBeforeReclone = yield* readText(file)
652
+ yield* removeConfluenceDir
653
+ yield* clonePages
654
+
655
+ const reclonedFile = yield* findPageByPageId(pageId!)
656
+ expect(reclonedFile).not.toBeNull()
657
+ expect(yield* pathExists(reclonedFile!)).toBe(true)
658
+
659
+ const contentAfterReclone = yield* readText(reclonedFile!)
660
+ expect(contentAfterReclone).toBe(contentBeforeReclone)
661
+ expect(contentAfterReclone).toContain(createMarker)
662
+ expect(contentAfterReclone).toContain(modifyMarker)
663
+ yield* expectSidecarMetadata(reclonedFile!, pageId!, contentAfterReclone)
664
+
665
+ // 7. Delete page via git workflow
666
+ yield* deleteLocalFile(reclonedFile!)
667
+ yield* commitChanges(`Delete integration test page ${timestampForTitle(new Date())}`)
668
+
669
+ const pushResult3 = yield* pushChanges
670
+ expect(pushResult3.deleted).toBe(1)
671
+
672
+ // 8. Verify deletion - re-clone should not include the page
673
+ yield* removeConfluenceDir
674
+ yield* clonePages
675
+
676
+ const deletedFile = yield* findPageByPageId(pageId!)
677
+ expect(deletedFile).toBeNull()
678
+ state.pageId = null
679
+ }))
680
+ }
681
+ )
269
682
  })