@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.
- package/CHANGELOG.md +39 -0
- package/LICENSE +21 -0
- package/README.md +22 -13
- package/dist/AdfPlaceholders.d.ts +42 -0
- package/dist/AdfPlaceholders.d.ts.map +1 -0
- package/dist/AdfPlaceholders.js +547 -0
- package/dist/AdfPlaceholders.js.map +1 -0
- package/dist/AdfSchemaValidator.d.ts +37 -0
- package/dist/AdfSchemaValidator.d.ts.map +1 -0
- package/dist/AdfSchemaValidator.js +37 -0
- package/dist/AdfSchemaValidator.js.map +1 -0
- package/dist/AdfWalker.d.ts +39 -0
- package/dist/AdfWalker.d.ts.map +1 -0
- package/dist/AdfWalker.js +527 -0
- package/dist/AdfWalker.js.map +1 -0
- package/dist/AtlaskitTransformers.d.ts +35 -0
- package/dist/AtlaskitTransformers.d.ts.map +1 -0
- package/dist/AtlaskitTransformers.js +48 -0
- package/dist/AtlaskitTransformers.js.map +1 -0
- package/dist/Brand.d.ts +6 -6
- package/dist/Brand.d.ts.map +1 -1
- package/dist/Brand.js +8 -6
- package/dist/Brand.js.map +1 -1
- package/dist/ConfluenceAuth.d.ts +4 -4
- package/dist/ConfluenceAuth.d.ts.map +1 -1
- package/dist/ConfluenceAuth.js +37 -39
- package/dist/ConfluenceAuth.js.map +1 -1
- package/dist/ConfluenceClient.d.ts +7 -17
- package/dist/ConfluenceClient.d.ts.map +1 -1
- package/dist/ConfluenceClient.js +81 -38
- package/dist/ConfluenceClient.js.map +1 -1
- package/dist/ConfluenceConfig.d.ts +3 -3
- package/dist/ConfluenceConfig.d.ts.map +1 -1
- package/dist/ConfluenceConfig.js +13 -11
- package/dist/ConfluenceConfig.js.map +1 -1
- package/dist/ConfluenceError.d.ts +68 -16
- package/dist/ConfluenceError.d.ts.map +1 -1
- package/dist/ConfluenceError.js +30 -1
- package/dist/ConfluenceError.js.map +1 -1
- package/dist/GitError.d.ts +5 -5
- package/dist/GitService.d.ts +11 -3
- package/dist/GitService.d.ts.map +1 -1
- package/dist/GitService.js +22 -27
- package/dist/GitService.js.map +1 -1
- package/dist/LocalFileSystem.d.ts +3 -3
- package/dist/LocalFileSystem.d.ts.map +1 -1
- package/dist/LocalFileSystem.js +6 -6
- package/dist/LocalFileSystem.js.map +1 -1
- package/dist/MarkdownConverter.d.ts +16 -65
- package/dist/MarkdownConverter.d.ts.map +1 -1
- package/dist/MarkdownConverter.js +64 -85
- package/dist/MarkdownConverter.js.map +1 -1
- package/dist/Schemas.d.ts +128 -141
- package/dist/Schemas.d.ts.map +1 -1
- package/dist/Schemas.js +21 -23
- package/dist/Schemas.js.map +1 -1
- package/dist/SyncEngine.d.ts +8 -5
- package/dist/SyncEngine.d.ts.map +1 -1
- package/dist/SyncEngine.js +189 -113
- package/dist/SyncEngine.js.map +1 -1
- package/dist/bin.js +23 -35
- package/dist/bin.js.map +1 -1
- package/dist/commands/auth.d.ts +2 -14
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +11 -16
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/clone.d.ts +4 -6
- package/dist/commands/clone.d.ts.map +1 -1
- package/dist/commands/clone.js +34 -32
- package/dist/commands/clone.js.map +1 -1
- package/dist/commands/delete.d.ts +2 -10
- package/dist/commands/delete.d.ts.map +1 -1
- package/dist/commands/delete.js +5 -4
- package/dist/commands/delete.js.map +1 -1
- package/dist/commands/errorHandler.d.ts +2 -1
- package/dist/commands/errorHandler.d.ts.map +1 -1
- package/dist/commands/errorHandler.js +22 -15
- package/dist/commands/errorHandler.js.map +1 -1
- package/dist/commands/fetch.d.ts +27 -0
- package/dist/commands/fetch.d.ts.map +1 -0
- package/dist/commands/fetch.js +48 -0
- package/dist/commands/fetch.js.map +1 -0
- package/dist/commands/git.d.ts +7 -10
- package/dist/commands/git.d.ts.map +1 -1
- package/dist/commands/git.js +6 -6
- package/dist/commands/git.js.map +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/layers.d.ts +10 -9
- package/dist/commands/layers.d.ts.map +1 -1
- package/dist/commands/layers.js +41 -30
- package/dist/commands/layers.js.map +1 -1
- package/dist/commands/new.d.ts +2 -6
- package/dist/commands/new.d.ts.map +1 -1
- package/dist/commands/new.js +5 -4
- package/dist/commands/new.js.map +1 -1
- package/dist/commands/pageInput.d.ts +19 -0
- package/dist/commands/pageInput.d.ts.map +1 -0
- package/dist/commands/pageInput.js +68 -0
- package/dist/commands/pageInput.js.map +1 -0
- package/dist/commands/root.d.ts +8 -0
- package/dist/commands/root.d.ts.map +1 -0
- package/dist/commands/root.js +29 -0
- package/dist/commands/root.js.map +1 -0
- package/dist/commands/sync.d.ts +6 -9
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +5 -6
- package/dist/commands/sync.js.map +1 -1
- package/dist/index.d.ts +3 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -13
- package/dist/index.js.map +1 -1
- package/dist/internal/NodeLayers.d.ts.map +1 -1
- package/dist/internal/NodeLayers.js +1 -2
- package/dist/internal/NodeLayers.js.map +1 -1
- package/dist/internal/adfMetadata.d.ts +30 -0
- package/dist/internal/adfMetadata.d.ts.map +1 -0
- package/dist/internal/adfMetadata.js +126 -0
- package/dist/internal/adfMetadata.js.map +1 -0
- package/dist/internal/cleanMarkdown.d.ts +5 -0
- package/dist/internal/cleanMarkdown.d.ts.map +1 -0
- package/dist/internal/cleanMarkdown.js +13 -0
- package/dist/internal/cleanMarkdown.js.map +1 -0
- package/dist/internal/frontmatter.d.ts.map +1 -1
- package/dist/internal/frontmatter.js +41 -8
- package/dist/internal/frontmatter.js.map +1 -1
- package/dist/internal/gitCommands.d.ts +9 -3
- package/dist/internal/gitCommands.d.ts.map +1 -1
- package/dist/internal/gitCommands.js +18 -9
- package/dist/internal/gitCommands.js.map +1 -1
- package/dist/internal/hashUtils.d.ts +1 -1
- package/dist/internal/hashUtils.d.ts.map +1 -1
- package/dist/internal/hashUtils.js +1 -1
- package/dist/internal/hashUtils.js.map +1 -1
- package/dist/internal/oauthServer.d.ts +10 -5
- package/dist/internal/oauthServer.d.ts.map +1 -1
- package/dist/internal/oauthServer.js +19 -40
- package/dist/internal/oauthServer.js.map +1 -1
- package/dist/internal/pathUtils.d.ts +1 -1
- package/dist/internal/pathUtils.d.ts.map +1 -1
- package/dist/internal/pathUtils.js +1 -1
- package/dist/internal/pathUtils.js.map +1 -1
- package/dist/internal/process.d.ts +15 -0
- package/dist/internal/process.d.ts.map +1 -0
- package/dist/internal/process.js +10 -0
- package/dist/internal/process.js.map +1 -0
- package/dist/internal/stdio.d.ts +6 -0
- package/dist/internal/stdio.d.ts.map +1 -0
- package/dist/internal/stdio.js +15 -0
- package/dist/internal/stdio.js.map +1 -0
- package/dist/internal/tokenStorage.d.ts +3 -13
- package/dist/internal/tokenStorage.d.ts.map +1 -1
- package/dist/internal/tokenStorage.js +26 -24
- package/dist/internal/tokenStorage.js.map +1 -1
- package/dist/internal/userCache.d.ts +1 -1
- package/dist/internal/userCache.d.ts.map +1 -1
- package/dist/internal/userCache.js +1 -1
- package/dist/internal/userCache.js.map +1 -1
- package/package.json +30 -30
- package/skills/confluence/SKILL.md +143 -0
- package/skills/confluence/agents/openai.yaml +4 -0
- package/src/AdfPlaceholders.ts +310 -13
- package/src/AdfSchemaValidator.ts +2 -4
- package/src/AdfWalker.ts +122 -42
- package/src/AtlaskitTransformers.ts +2 -4
- package/src/Brand.ts +11 -16
- package/src/ConfluenceAuth.ts +22 -30
- package/src/ConfluenceClient.ts +24 -20
- package/src/ConfluenceConfig.ts +14 -14
- package/src/GitService.ts +39 -49
- package/src/LocalFileSystem.ts +7 -9
- package/src/MarkdownConverter.ts +2 -4
- package/src/Schemas.ts +13 -12
- package/src/SyncEngine.ts +151 -53
- package/src/bin.ts +30 -56
- package/src/commands/auth.ts +21 -18
- package/src/commands/clone.ts +38 -37
- package/src/commands/delete.ts +5 -4
- package/src/commands/errorHandler.ts +25 -18
- package/src/commands/fetch.ts +90 -0
- package/src/commands/git.ts +6 -6
- package/src/commands/index.ts +1 -0
- package/src/commands/layers.ts +53 -33
- package/src/commands/new.ts +5 -4
- package/src/commands/pageInput.ts +103 -0
- package/src/commands/root.ts +59 -0
- package/src/commands/sync.ts +7 -6
- package/src/internal/NodeLayers.ts +1 -2
- package/src/internal/adfMetadata.ts +145 -0
- package/src/internal/cleanMarkdown.ts +15 -0
- package/src/internal/frontmatter.ts +45 -8
- package/src/internal/gitCommands.ts +23 -17
- package/src/internal/hashUtils.ts +2 -2
- package/src/internal/oauthServer.ts +84 -105
- package/src/internal/pathUtils.ts +1 -1
- package/src/internal/process.ts +15 -0
- package/src/internal/stdio.ts +22 -0
- package/src/internal/tokenStorage.ts +39 -29
- package/src/internal/userCache.ts +2 -2
- package/test/AdfPlaceholders.test.ts +213 -0
- package/test/AdfSchemaValidator.test.ts +6 -6
- package/test/AdfWalker.test.ts +167 -21
- package/test/AtlaskitTransformers.test.ts +4 -4
- package/test/Brand.test.ts +11 -11
- package/test/GitService.test.ts +6 -2
- package/test/MarkdownConverter.test.ts +12 -11
- package/test/RoundTrip.test.ts +258 -3
- package/test/Schemas.test.ts +40 -40
- package/test/adfMetadata.test.ts +110 -0
- package/test/cleanMarkdown.test.ts +36 -0
- package/test/commandHarness.test.ts +79 -0
- package/test/commandHarness.ts +147 -0
- package/test/fetch.test.ts +61 -0
- package/test/frontmatter.test.ts +41 -0
- package/test/integration.test.ts +569 -156
- package/test/layers.test.ts +12 -0
- package/test/oauthServer.test.ts +4 -5
- package/test/pageInput.test.ts +83 -0
- package/test/tokenStorage.test.ts +17 -17
- package/dist/SchemaConverterError.d.ts +0 -108
- package/dist/SchemaConverterError.d.ts.map +0 -1
- package/dist/SchemaConverterError.js +0 -84
- package/dist/SchemaConverterError.js.map +0 -1
- package/dist/ast/BlockNode.d.ts +0 -468
- package/dist/ast/BlockNode.d.ts.map +0 -1
- package/dist/ast/BlockNode.js +0 -319
- package/dist/ast/BlockNode.js.map +0 -1
- package/dist/ast/Document.d.ts +0 -244
- package/dist/ast/Document.d.ts.map +0 -1
- package/dist/ast/Document.js +0 -69
- package/dist/ast/Document.js.map +0 -1
- package/dist/ast/InlineNode.d.ts +0 -477
- package/dist/ast/InlineNode.d.ts.map +0 -1
- package/dist/ast/InlineNode.js +0 -263
- package/dist/ast/InlineNode.js.map +0 -1
- package/dist/ast/MacroNode.d.ts +0 -267
- package/dist/ast/MacroNode.d.ts.map +0 -1
- package/dist/ast/MacroNode.js +0 -164
- package/dist/ast/MacroNode.js.map +0 -1
- package/dist/ast/index.d.ts +0 -10
- package/dist/ast/index.d.ts.map +0 -1
- package/dist/ast/index.js +0 -14
- package/dist/ast/index.js.map +0 -1
- package/dist/parsers/ConfluenceParser.d.ts +0 -26
- package/dist/parsers/ConfluenceParser.d.ts.map +0 -1
- package/dist/parsers/ConfluenceParser.js +0 -792
- package/dist/parsers/ConfluenceParser.js.map +0 -1
- package/dist/parsers/MarkdownParser.d.ts +0 -26
- package/dist/parsers/MarkdownParser.d.ts.map +0 -1
- package/dist/parsers/MarkdownParser.js +0 -873
- package/dist/parsers/MarkdownParser.js.map +0 -1
- package/dist/parsers/index.d.ts +0 -8
- package/dist/parsers/index.d.ts.map +0 -1
- package/dist/parsers/index.js +0 -8
- package/dist/parsers/index.js.map +0 -1
- package/dist/parsers/preprocessing/ConfluencePreprocessing.d.ts +0 -23
- package/dist/parsers/preprocessing/ConfluencePreprocessing.d.ts.map +0 -1
- package/dist/parsers/preprocessing/ConfluencePreprocessing.js +0 -323
- package/dist/parsers/preprocessing/ConfluencePreprocessing.js.map +0 -1
- package/dist/parsers/preprocessing/index.d.ts +0 -7
- package/dist/parsers/preprocessing/index.d.ts.map +0 -1
- package/dist/parsers/preprocessing/index.js +0 -7
- package/dist/parsers/preprocessing/index.js.map +0 -1
- package/dist/schemas/ConfluenceSchema.d.ts +0 -21
- package/dist/schemas/ConfluenceSchema.d.ts.map +0 -1
- package/dist/schemas/ConfluenceSchema.js +0 -38
- package/dist/schemas/ConfluenceSchema.js.map +0 -1
- package/dist/schemas/ConversionSchema.d.ts +0 -35
- package/dist/schemas/ConversionSchema.d.ts.map +0 -1
- package/dist/schemas/ConversionSchema.js +0 -208
- package/dist/schemas/ConversionSchema.js.map +0 -1
- package/dist/schemas/MarkdownSchema.d.ts +0 -21
- package/dist/schemas/MarkdownSchema.d.ts.map +0 -1
- package/dist/schemas/MarkdownSchema.js +0 -38
- package/dist/schemas/MarkdownSchema.js.map +0 -1
- package/dist/schemas/hast/HastFromHtml.d.ts +0 -27
- package/dist/schemas/hast/HastFromHtml.d.ts.map +0 -1
- package/dist/schemas/hast/HastFromHtml.js +0 -107
- package/dist/schemas/hast/HastFromHtml.js.map +0 -1
- package/dist/schemas/hast/HastSchema.d.ts +0 -195
- package/dist/schemas/hast/HastSchema.d.ts.map +0 -1
- package/dist/schemas/hast/HastSchema.js +0 -183
- package/dist/schemas/hast/HastSchema.js.map +0 -1
- package/dist/schemas/hast/index.d.ts +0 -9
- package/dist/schemas/hast/index.d.ts.map +0 -1
- package/dist/schemas/hast/index.js +0 -3
- package/dist/schemas/hast/index.js.map +0 -1
- package/dist/schemas/index.d.ts +0 -14
- package/dist/schemas/index.d.ts.map +0 -1
- package/dist/schemas/index.js +0 -16
- package/dist/schemas/index.js.map +0 -1
- package/dist/schemas/mdast/MdastFromMarkdown.d.ts +0 -30
- package/dist/schemas/mdast/MdastFromMarkdown.d.ts.map +0 -1
- package/dist/schemas/mdast/MdastFromMarkdown.js +0 -79
- package/dist/schemas/mdast/MdastFromMarkdown.js.map +0 -1
- package/dist/schemas/mdast/MdastSchema.d.ts +0 -385
- package/dist/schemas/mdast/MdastSchema.d.ts.map +0 -1
- package/dist/schemas/mdast/MdastSchema.js +0 -266
- package/dist/schemas/mdast/MdastSchema.js.map +0 -1
- package/dist/schemas/mdast/index.d.ts +0 -10
- package/dist/schemas/mdast/index.d.ts.map +0 -1
- package/dist/schemas/mdast/index.js +0 -4
- package/dist/schemas/mdast/index.js.map +0 -1
- package/dist/schemas/mdast/mdastToString.d.ts +0 -13
- package/dist/schemas/mdast/mdastToString.d.ts.map +0 -1
- package/dist/schemas/mdast/mdastToString.js +0 -85
- package/dist/schemas/mdast/mdastToString.js.map +0 -1
- package/dist/schemas/nodes/block/BlockSchema.d.ts +0 -43
- package/dist/schemas/nodes/block/BlockSchema.d.ts.map +0 -1
- package/dist/schemas/nodes/block/BlockSchema.js +0 -634
- package/dist/schemas/nodes/block/BlockSchema.js.map +0 -1
- package/dist/schemas/nodes/block/index.d.ts +0 -7
- package/dist/schemas/nodes/block/index.d.ts.map +0 -1
- package/dist/schemas/nodes/block/index.js +0 -7
- package/dist/schemas/nodes/block/index.js.map +0 -1
- package/dist/schemas/nodes/index.d.ts +0 -9
- package/dist/schemas/nodes/index.d.ts.map +0 -1
- package/dist/schemas/nodes/index.js +0 -12
- package/dist/schemas/nodes/index.js.map +0 -1
- package/dist/schemas/nodes/inline/InlineSchema.d.ts +0 -48
- package/dist/schemas/nodes/inline/InlineSchema.d.ts.map +0 -1
- package/dist/schemas/nodes/inline/InlineSchema.js +0 -436
- package/dist/schemas/nodes/inline/InlineSchema.js.map +0 -1
- package/dist/schemas/nodes/inline/index.d.ts +0 -7
- package/dist/schemas/nodes/inline/index.d.ts.map +0 -1
- package/dist/schemas/nodes/inline/index.js +0 -7
- package/dist/schemas/nodes/inline/index.js.map +0 -1
- package/dist/schemas/nodes/macro/MacroSchema.d.ts +0 -27
- package/dist/schemas/nodes/macro/MacroSchema.d.ts.map +0 -1
- package/dist/schemas/nodes/macro/MacroSchema.js +0 -162
- package/dist/schemas/nodes/macro/MacroSchema.js.map +0 -1
- package/dist/schemas/nodes/macro/index.d.ts +0 -7
- package/dist/schemas/nodes/macro/index.d.ts.map +0 -1
- package/dist/schemas/nodes/macro/index.js +0 -7
- package/dist/schemas/nodes/macro/index.js.map +0 -1
- package/dist/schemas/preprocessing/ConfluencePreprocessor.d.ts +0 -53
- package/dist/schemas/preprocessing/ConfluencePreprocessor.d.ts.map +0 -1
- package/dist/schemas/preprocessing/ConfluencePreprocessor.js +0 -349
- package/dist/schemas/preprocessing/ConfluencePreprocessor.js.map +0 -1
- package/dist/schemas/preprocessing/index.d.ts +0 -8
- package/dist/schemas/preprocessing/index.d.ts.map +0 -1
- package/dist/schemas/preprocessing/index.js +0 -2
- package/dist/schemas/preprocessing/index.js.map +0 -1
- package/dist/serializers/ConfluenceSerializer.d.ts +0 -30
- package/dist/serializers/ConfluenceSerializer.d.ts.map +0 -1
- package/dist/serializers/ConfluenceSerializer.js +0 -551
- package/dist/serializers/ConfluenceSerializer.js.map +0 -1
- package/dist/serializers/MarkdownSerializer.d.ts +0 -34
- package/dist/serializers/MarkdownSerializer.d.ts.map +0 -1
- package/dist/serializers/MarkdownSerializer.js +0 -355
- package/dist/serializers/MarkdownSerializer.js.map +0 -1
- package/dist/serializers/index.d.ts +0 -8
- package/dist/serializers/index.d.ts.map +0 -1
- package/dist/serializers/index.js +0 -8
- package/dist/serializers/index.js.map +0 -1
package/test/integration.test.ts
CHANGED
|
@@ -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
|
-
* -
|
|
9
|
+
* - CONFLUENCE_API_KEY + CONFLUENCE_EMAIL env vars for raw ADF verification
|
|
10
10
|
*/
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
import * as
|
|
14
|
-
import * as
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
67
|
-
const
|
|
68
|
-
|
|
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 = ()
|
|
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(
|
|
82
|
-
expect(
|
|
83
|
-
expect(
|
|
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 = ()
|
|
92
|
-
|
|
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
|
|
255
|
+
* Create a new page by copying content from a cloned seed page.
|
|
97
256
|
*/
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
expect(
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
const contentMatch =
|
|
104
|
-
const bodyContent = contentMatch ? contentMatch[1]!.trim() :
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
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 =
|
|
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
|
-
|
|
280
|
+
${createMarker}
|
|
119
281
|
`
|
|
120
|
-
|
|
121
|
-
expect(
|
|
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,
|
|
127
|
-
}
|
|
287
|
+
return { file, createMarker, seedPageId, timestamp }
|
|
288
|
+
})
|
|
128
289
|
|
|
129
290
|
/**
|
|
130
291
|
* Commit current changes.
|
|
131
292
|
*/
|
|
132
|
-
const commitChanges = (message: string)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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 = ()
|
|
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 = ()
|
|
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)
|
|
166
|
-
|
|
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)
|
|
175
|
-
|
|
176
|
-
|
|
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)
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
260
|
-
expect(pushResult3.deleted).toBe(1)
|
|
574
|
+
// === Tests ===
|
|
261
575
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
576
|
+
describe("CLI Integration - Page Creation Flow", () => {
|
|
577
|
+
beforeAll(async () => {
|
|
578
|
+
await runPlatform(initializeTestEnvironment)
|
|
579
|
+
})
|
|
265
580
|
|
|
266
|
-
|
|
267
|
-
|
|
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
|
})
|