@zzclub/pipeline 0.4.0 → 0.9.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/README.md +102 -3
- package/package.json +1 -1
- package/src/adapter-types.ts +6 -0
- package/src/adapters/builtin-image-renderer.ts +7 -1
- package/src/adapters/builtin-markdown-renderer.ts +1 -0
- package/src/commands/analytics.test.ts +155 -0
- package/src/commands/analytics.ts +223 -0
- package/src/commands/init.ts +42 -3
- package/src/commands/publish.ts +44 -67
- package/src/commands/republish.ts +146 -0
- package/src/commands/sync-blog.ts +19 -3
- package/src/commands/topic.test.ts +146 -0
- package/src/commands/topic.ts +717 -0
- package/src/commands/wechat-export.ts +2 -0
- package/src/commands/wx-draft-delete.ts +36 -0
- package/src/commands/wx-drafts.ts +47 -0
- package/src/config.ts +0 -0
- package/src/db.test.ts +47 -0
- package/src/db.ts +95 -0
- package/src/imgx/assets/icons/ancientone-logo.png +0 -0
- package/src/imgx/assets/templates/wechat-cover-split.html +1 -1
- package/src/imgx/render-card.ts +1 -5
- package/src/plugins.ts +11 -1
- package/src/providers/blog.ts +9 -3
- package/src/providers/index.test.ts +1 -1
- package/src/providers/index.ts +29 -5
- package/src/providers/publish-core.ts +124 -0
- package/src/providers/wechat.ts +260 -28
- package/src/routes.ts +1 -1
- package/src/schema/analytics.test.ts +54 -0
- package/src/schema/analytics.ts +19 -0
- package/src/schema/config.ts +25 -1
- package/src/schema/state.ts +10 -0
- package/src/schema/topic.test.ts +69 -0
- package/src/schema/topic.ts +55 -0
- package/src/state.ts +1 -0
- package/src/text.test.ts +11 -6
- package/src/text.ts +27 -9
- package/src/wechat-preview/assets/browser-dist/.vite/manifest.json +2 -7
- package/src/wechat-preview/assets/browser-dist/assets/{style-BYNXnUvc.css → style-vhHYSP8T.css} +1 -1
- package/src/wechat-preview/assets/browser-dist/editor-export.js +27 -27
- package/src/wechat-preview/browser/editor-export.css +248 -159
- package/src/wechat-preview/index.ts +28 -11
- package/src/wechat-preview/themes.ts +70 -24
- package/src/wechat-preview/wechat-formatter.test.ts +0 -1
- package/src/wechat-preview/wechat-formatter.ts +48 -23
- package/src/workflow.test.ts +294 -1
- package/src/wechat-preview/assets/browser-dist/assets/SweiCurveLegCJKsc-Regular-B3Ut5bwH.woff +0 -0
package/src/commands/publish.ts
CHANGED
|
@@ -16,14 +16,14 @@
|
|
|
16
16
|
import { parseArgs, requireArg, optionalArg, flagArg } from "../args";
|
|
17
17
|
import { printResult, renderPublish } from "../output";
|
|
18
18
|
import { loadConfig, resolveWorkspacePaths } from "../config";
|
|
19
|
-
import { getPublishProvider } from "../providers";
|
|
20
19
|
import {
|
|
21
20
|
readState,
|
|
22
21
|
validateForPhase,
|
|
23
22
|
writeState,
|
|
24
|
-
type
|
|
23
|
+
type PublishTarget,
|
|
25
24
|
type RoutePrimary,
|
|
26
25
|
} from "../state";
|
|
26
|
+
import { executePublishTargets } from "../providers/publish-core";
|
|
27
27
|
|
|
28
28
|
export async function publish(args: string[]): Promise<void> {
|
|
29
29
|
const parsed = parseArgs(args);
|
|
@@ -34,7 +34,7 @@ Usage: zzhub-pipeline publish [options]
|
|
|
34
34
|
|
|
35
35
|
Options:
|
|
36
36
|
--state Path to state JSON (required)
|
|
37
|
-
--route Only publish this route (optional; default: all
|
|
37
|
+
--route Only publish this route (optional; default: all targets)
|
|
38
38
|
--dry-run Print commands without executing (optional)
|
|
39
39
|
`.trim());
|
|
40
40
|
return;
|
|
@@ -58,72 +58,45 @@ Options:
|
|
|
58
58
|
);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
existing &&
|
|
74
|
-
existing.status === "success" &&
|
|
75
|
-
existing.content_version === state.artifacts.content_version &&
|
|
76
|
-
existing.render_version === state.artifacts.render_version
|
|
77
|
-
) {
|
|
78
|
-
console.error(`[publish] Skipping ${route}: already published at current version`);
|
|
79
|
-
continue;
|
|
61
|
+
// Determine targets
|
|
62
|
+
let targets: PublishTarget[];
|
|
63
|
+
if (routeOverride) {
|
|
64
|
+
targets = [{ route: routeOverride, account: state.route.account }];
|
|
65
|
+
} else if (state.publish_targets.length > 0) {
|
|
66
|
+
targets = state.publish_targets;
|
|
67
|
+
} else {
|
|
68
|
+
// Derive from route (backward compat)
|
|
69
|
+
targets = [{ route: state.route.primary, account: state.route.account }];
|
|
70
|
+
for (const extra of state.route.extras) {
|
|
71
|
+
targets.push({ route: extra, account: state.route.account });
|
|
80
72
|
}
|
|
73
|
+
}
|
|
81
74
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
// Upsert result
|
|
92
|
-
const idx = results.findIndex((r) => r.route === route);
|
|
93
|
-
if (idx >= 0) {
|
|
94
|
-
results[idx] = result;
|
|
95
|
-
} else {
|
|
96
|
-
results.push(result);
|
|
97
|
-
}
|
|
75
|
+
const { results, errors } = await executePublishTargets({
|
|
76
|
+
state,
|
|
77
|
+
targets,
|
|
78
|
+
dryRun,
|
|
79
|
+
config,
|
|
80
|
+
workspacePaths,
|
|
81
|
+
});
|
|
98
82
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
published_at: null,
|
|
109
|
-
content_version: state.artifacts.content_version,
|
|
110
|
-
render_version: state.artifacts.render_version,
|
|
111
|
-
};
|
|
112
|
-
const idx = results.findIndex((r) => r.route === route);
|
|
113
|
-
if (idx >= 0) {
|
|
114
|
-
results[idx] = failResult;
|
|
115
|
-
} else {
|
|
116
|
-
results.push(failResult);
|
|
117
|
-
}
|
|
118
|
-
hasUnfinishedPublish = true;
|
|
83
|
+
// Upsert results into state
|
|
84
|
+
for (const result of results) {
|
|
85
|
+
const idx = state.publish.results.findIndex(
|
|
86
|
+
(r) => r.route === result.route && r.account === result.account,
|
|
87
|
+
);
|
|
88
|
+
if (idx >= 0) {
|
|
89
|
+
state.publish.results[idx] = result;
|
|
90
|
+
} else {
|
|
91
|
+
state.publish.results.push(result);
|
|
119
92
|
}
|
|
120
93
|
}
|
|
121
94
|
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
95
|
+
// Check if all targets are done
|
|
96
|
+
const allDone = targets.every((target) => {
|
|
97
|
+
const r = state.publish.results.find(
|
|
98
|
+
(x) => x.route === target.route && x.account === target.account,
|
|
99
|
+
);
|
|
127
100
|
return r && (r.status === "success" || r.status === "skipped");
|
|
128
101
|
});
|
|
129
102
|
|
|
@@ -131,7 +104,7 @@ Options:
|
|
|
131
104
|
state.mode = "done";
|
|
132
105
|
state.phase.publish = { status: "done", error: null };
|
|
133
106
|
state.phase.current = "done";
|
|
134
|
-
} else
|
|
107
|
+
} else {
|
|
135
108
|
state.mode = "active";
|
|
136
109
|
state.phase.publish = { status: "pending", error: null };
|
|
137
110
|
state.phase.current = "publish";
|
|
@@ -139,9 +112,13 @@ Options:
|
|
|
139
112
|
|
|
140
113
|
await writeState(statePath, state);
|
|
141
114
|
|
|
142
|
-
|
|
143
|
-
publish_results: results,
|
|
115
|
+
const output: any = {
|
|
116
|
+
publish_results: state.publish.results,
|
|
144
117
|
mode: state.mode,
|
|
145
118
|
phase: state.phase.current,
|
|
146
|
-
}
|
|
119
|
+
};
|
|
120
|
+
if (errors.length > 0) {
|
|
121
|
+
output.errors = errors;
|
|
122
|
+
}
|
|
123
|
+
printResult(output, renderPublish);
|
|
147
124
|
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* republish — Add publish targets and execute for new targets only.
|
|
3
|
+
*
|
|
4
|
+
* Used to publish a completed task to additional accounts/platforms
|
|
5
|
+
* without re-rendering.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* zzhub-pipeline republish \
|
|
9
|
+
* --state /path/to/state.json \
|
|
10
|
+
* [--account ancientone] \
|
|
11
|
+
* [--targets "wechat-article@ancientone,blog@default"] \
|
|
12
|
+
* [--dry-run]
|
|
13
|
+
*
|
|
14
|
+
* Output: Updated state with new publish.results entries
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { parseArgs, requireArg, optionalArg, flagArg } from "../args";
|
|
18
|
+
import { printResult } from "../output";
|
|
19
|
+
import { loadConfig, resolveWorkspacePaths } from "../config";
|
|
20
|
+
import {
|
|
21
|
+
readState,
|
|
22
|
+
writeState,
|
|
23
|
+
type PublishTarget,
|
|
24
|
+
type RoutePrimary,
|
|
25
|
+
} from "../state";
|
|
26
|
+
import { executePublishTargets } from "../providers/publish-core";
|
|
27
|
+
|
|
28
|
+
export async function republish(args: string[]): Promise<void> {
|
|
29
|
+
const parsed = parseArgs(args);
|
|
30
|
+
|
|
31
|
+
if (parsed.help) {
|
|
32
|
+
console.log(`
|
|
33
|
+
Usage: zzhub-pipeline republish [options]
|
|
34
|
+
|
|
35
|
+
Add publish targets and execute for new targets only.
|
|
36
|
+
Used to publish a completed task to additional accounts/platforms.
|
|
37
|
+
|
|
38
|
+
Options:
|
|
39
|
+
--state Path to state JSON (required)
|
|
40
|
+
--account Add target with state.route.primary and this account (optional)
|
|
41
|
+
--targets Comma-separated: route@account,route@account (optional)
|
|
42
|
+
--dry-run Print commands without executing (optional)
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
# Add single target (same route, different account)
|
|
46
|
+
zzhub-pipeline republish --state path --account ancientone
|
|
47
|
+
|
|
48
|
+
# Add multiple targets
|
|
49
|
+
zzhub-pipeline republish --state path --targets "wechat-article@ancientone,blog@default"
|
|
50
|
+
|
|
51
|
+
# Mix both
|
|
52
|
+
zzhub-pipeline republish --state path --account ancientone --targets "blog@default"
|
|
53
|
+
`.trim());
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const statePath = requireArg(parsed, "state", "state JSON path");
|
|
58
|
+
const accountArg = optionalArg(parsed, "account");
|
|
59
|
+
const targetsArg = optionalArg(parsed, "targets");
|
|
60
|
+
const dryRun = flagArg(parsed, "dry-run");
|
|
61
|
+
|
|
62
|
+
const state = await readState(statePath);
|
|
63
|
+
const config = loadConfig();
|
|
64
|
+
const workspacePaths = resolveWorkspacePaths(state.workspace_root, config);
|
|
65
|
+
|
|
66
|
+
// Validate prerequisites
|
|
67
|
+
if (!state.asset_path) {
|
|
68
|
+
throw new Error("asset_path not set. Cannot republish.");
|
|
69
|
+
}
|
|
70
|
+
if (state.content_review.status !== "passed") {
|
|
71
|
+
throw new Error("content_review must be 'passed' before republish.");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Parse new targets
|
|
75
|
+
const newTargets: PublishTarget[] = [];
|
|
76
|
+
|
|
77
|
+
// --account: add {state.route.primary, account}
|
|
78
|
+
if (accountArg) {
|
|
79
|
+
newTargets.push({
|
|
80
|
+
route: state.route.primary,
|
|
81
|
+
account: accountArg,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// --targets: parse "route@account,route@account,..."
|
|
86
|
+
if (targetsArg) {
|
|
87
|
+
const parts = targetsArg.split(",").map((t) => t.trim());
|
|
88
|
+
for (const part of parts) {
|
|
89
|
+
const atIdx = part.indexOf("@");
|
|
90
|
+
if (atIdx !== -1) {
|
|
91
|
+
const route = part.slice(0, atIdx).trim() as RoutePrimary;
|
|
92
|
+
const account = part.slice(atIdx + 1).trim();
|
|
93
|
+
newTargets.push({ route, account });
|
|
94
|
+
} else {
|
|
95
|
+
// No @: use state.route.primary as route, part as account
|
|
96
|
+
newTargets.push({
|
|
97
|
+
route: state.route.primary,
|
|
98
|
+
account: part.trim(),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (newTargets.length === 0) {
|
|
105
|
+
throw new Error("No targets specified. Use --account or --targets.");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Append to publish_targets (dedupe handled by executePublishTargets)
|
|
109
|
+
state.publish_targets = [...state.publish_targets, ...newTargets];
|
|
110
|
+
|
|
111
|
+
// Execute (executePublishTargets handles dedupe and idempotent filter)
|
|
112
|
+
const { results, errors } = await executePublishTargets({
|
|
113
|
+
state,
|
|
114
|
+
targets: newTargets,
|
|
115
|
+
dryRun,
|
|
116
|
+
config,
|
|
117
|
+
workspacePaths,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Upsert results
|
|
121
|
+
for (const result of results) {
|
|
122
|
+
const idx = state.publish.results.findIndex(
|
|
123
|
+
(r) => r.route === result.route && r.account === result.account,
|
|
124
|
+
);
|
|
125
|
+
if (idx >= 0) {
|
|
126
|
+
state.publish.results[idx] = result;
|
|
127
|
+
} else {
|
|
128
|
+
state.publish.results.push(result);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Mode stays done (or whatever it was)
|
|
133
|
+
state.updated_at = new Date().toISOString();
|
|
134
|
+
|
|
135
|
+
await writeState(statePath, state);
|
|
136
|
+
|
|
137
|
+
const output: any = {
|
|
138
|
+
publish_results: state.publish.results,
|
|
139
|
+
mode: state.mode,
|
|
140
|
+
new_targets: newTargets,
|
|
141
|
+
};
|
|
142
|
+
if (errors.length > 0) {
|
|
143
|
+
output.errors = errors;
|
|
144
|
+
}
|
|
145
|
+
printResult(output);
|
|
146
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseArgs, requireArg, flagArg } from "../args";
|
|
2
2
|
import { printResult, renderSyncBlog } from "../output";
|
|
3
3
|
import { loadConfig, resolveWorkspacePaths } from "../config";
|
|
4
|
-
import { readState } from "../state";
|
|
4
|
+
import { readState, updateState } from "../state";
|
|
5
5
|
import { publishBlogRoute } from "../providers/blog";
|
|
6
6
|
|
|
7
7
|
export async function syncBlog(args: string[]): Promise<void> {
|
|
@@ -20,13 +20,16 @@ Options:
|
|
|
20
20
|
|
|
21
21
|
const statePath = requireArg(parsed, "state", "state JSON path");
|
|
22
22
|
const dryRun = flagArg(parsed, "dry-run");
|
|
23
|
+
|
|
24
|
+
const config = loadConfig();
|
|
25
|
+
|
|
26
|
+
// Run the actual publish (may have network side effects)
|
|
23
27
|
const state = await readState(statePath);
|
|
24
28
|
|
|
25
29
|
if (!state.asset_path) {
|
|
26
30
|
throw new Error("asset_path not set. Run prepare-finalize first.");
|
|
27
31
|
}
|
|
28
32
|
|
|
29
|
-
const config = loadConfig();
|
|
30
33
|
const workspacePaths = resolveWorkspacePaths(state.workspace_root, config);
|
|
31
34
|
const result = await publishBlogRoute({
|
|
32
35
|
state,
|
|
@@ -35,5 +38,18 @@ Options:
|
|
|
35
38
|
workspacePaths,
|
|
36
39
|
});
|
|
37
40
|
|
|
38
|
-
|
|
41
|
+
// Write result back to state
|
|
42
|
+
const finalState = await updateState(statePath, (s) => {
|
|
43
|
+
const idx = s.publish.results.findIndex((r) => r.route === "blog");
|
|
44
|
+
if (idx >= 0) {
|
|
45
|
+
s.publish.results[idx] = result;
|
|
46
|
+
} else {
|
|
47
|
+
s.publish.results.push(result);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
printResult({
|
|
52
|
+
...result,
|
|
53
|
+
mode: finalState.mode,
|
|
54
|
+
}, renderSyncBlog);
|
|
39
55
|
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { mkdtemp, rm } from "fs/promises";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { tmpdir } from "os";
|
|
5
|
+
import { addTopic, listTopics, updateTopic, scheduleTopic, retroTopic, abandonTopic } from "./topic";
|
|
6
|
+
|
|
7
|
+
describe("topic commands", () => {
|
|
8
|
+
let workspace: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
workspace = await mkdtemp(join(tmpdir(), "zzhub-test-"));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
await rm(workspace, { recursive: true, force: true });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("addTopic", () => {
|
|
19
|
+
test("creates a new topic with defaults", async () => {
|
|
20
|
+
const result = await addTopic(workspace, {
|
|
21
|
+
title: "Test Topic",
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(result.topic_id).toMatch(/^topic_/);
|
|
25
|
+
expect(result.title).toBe("Test Topic");
|
|
26
|
+
expect(result.status).toBe("backlog");
|
|
27
|
+
expect(result.priority).toBe("medium");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("creates topic with all fields", async () => {
|
|
31
|
+
const result = await addTopic(workspace, {
|
|
32
|
+
title: "AI Tools",
|
|
33
|
+
description: "About AI",
|
|
34
|
+
priority: "high",
|
|
35
|
+
tags: ["AI", "tools"],
|
|
36
|
+
notes: "Some notes",
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(result.priority).toBe("high");
|
|
40
|
+
expect(result.tags).toEqual(["AI", "tools"]);
|
|
41
|
+
expect(result.notes).toBe("Some notes");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("listTopics", () => {
|
|
46
|
+
test("lists all topics", async () => {
|
|
47
|
+
await addTopic(workspace, { title: "Topic 1" });
|
|
48
|
+
await addTopic(workspace, { title: "Topic 2" });
|
|
49
|
+
|
|
50
|
+
const topics = await listTopics(workspace, {});
|
|
51
|
+
expect(topics.length).toBe(2);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("filters by status", async () => {
|
|
55
|
+
await addTopic(workspace, { title: "Topic 1", status: "backlog" });
|
|
56
|
+
await addTopic(workspace, { title: "Topic 2", status: "scheduled" });
|
|
57
|
+
|
|
58
|
+
const topics = await listTopics(workspace, { status: "backlog" });
|
|
59
|
+
expect(topics.length).toBe(1);
|
|
60
|
+
expect(topics[0].title).toBe("Topic 1");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("filters by priority", async () => {
|
|
64
|
+
await addTopic(workspace, { title: "Topic 1", priority: "high" });
|
|
65
|
+
await addTopic(workspace, { title: "Topic 2", priority: "low" });
|
|
66
|
+
|
|
67
|
+
const topics = await listTopics(workspace, { priority: "high" });
|
|
68
|
+
expect(topics.length).toBe(1);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("limits results", async () => {
|
|
72
|
+
await addTopic(workspace, { title: "Topic 1" });
|
|
73
|
+
await addTopic(workspace, { title: "Topic 2" });
|
|
74
|
+
await addTopic(workspace, { title: "Topic 3" });
|
|
75
|
+
|
|
76
|
+
const topics = await listTopics(workspace, { limit: 2 });
|
|
77
|
+
expect(topics.length).toBe(2);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("updateTopic", () => {
|
|
82
|
+
test("updates topic fields", async () => {
|
|
83
|
+
const topic = await addTopic(workspace, { title: "Original" });
|
|
84
|
+
|
|
85
|
+
const updated = await updateTopic(workspace, topic.topic_id, {
|
|
86
|
+
status: "evaluating",
|
|
87
|
+
ai_score: 85,
|
|
88
|
+
ai_reason: "Good topic",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(updated.status).toBe("evaluating");
|
|
92
|
+
expect(updated.ai_score).toBe(85);
|
|
93
|
+
expect(updated.ai_reason).toBe("Good topic");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("throws if topic not found", async () => {
|
|
97
|
+
expect(
|
|
98
|
+
updateTopic(workspace, "nonexistent", { status: "scheduled" })
|
|
99
|
+
).rejects.toThrow("Topic not found");
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("scheduleTopic", () => {
|
|
104
|
+
test("schedules topic with date", async () => {
|
|
105
|
+
const topic = await addTopic(workspace, { title: "Test" });
|
|
106
|
+
|
|
107
|
+
const scheduled = await scheduleTopic(workspace, topic.topic_id, {
|
|
108
|
+
scheduled_date: "2026-06-15",
|
|
109
|
+
target_account: "default",
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(scheduled.status).toBe("scheduled");
|
|
113
|
+
expect(scheduled.scheduled_date).toBe("2026-06-15");
|
|
114
|
+
expect(scheduled.target_account).toBe("default");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("retroTopic", () => {
|
|
119
|
+
test("adds retro and marks published", async () => {
|
|
120
|
+
const topic = await addTopic(workspace, { title: "Test" });
|
|
121
|
+
|
|
122
|
+
const retroed = await retroTopic(workspace, topic.topic_id, {
|
|
123
|
+
performance: "good",
|
|
124
|
+
lessons: "Learned something",
|
|
125
|
+
metrics_snapshot: { reads: 1500 },
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(retroed.status).toBe("published");
|
|
129
|
+
expect(retroed.retro_performance).toBe("good");
|
|
130
|
+
expect(retroed.retro_lessons).toBe("Learned something");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("abandonTopic", () => {
|
|
135
|
+
test("marks topic as abandoned", async () => {
|
|
136
|
+
const topic = await addTopic(workspace, { title: "Test" });
|
|
137
|
+
|
|
138
|
+
const abandoned = await abandonTopic(workspace, topic.topic_id, {
|
|
139
|
+
reason: "Topic outdated",
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(abandoned.status).toBe("abandoned");
|
|
143
|
+
expect(abandoned.notes).toContain("Topic outdated");
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|