novel-writer-cli 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/checkpoint-quickstart-phase.test.js +49 -0
- package/dist/__tests__/cli-instructions-novel-ask-gate.test.js +83 -0
- package/dist/__tests__/cli-repair-reset-quickstart.test.js +194 -0
- package/dist/__tests__/cli-version-flag.test.js +38 -0
- package/dist/__tests__/init.test.js +9 -6
- package/dist/__tests__/instructions-review-novel-ask-gate.test.js +31 -0
- package/dist/__tests__/orchestrator-state-routing.test.js +10 -6
- package/dist/__tests__/quickstart-pipeline.test.js +346 -0
- package/dist/__tests__/safe-path-symlink.test.js +41 -0
- package/dist/__tests__/validate-quickstart-prereqs.test.js +73 -0
- package/dist/advance.js +88 -3
- package/dist/checkpoint.js +25 -4
- package/dist/cli.js +130 -4
- package/dist/init.js +2 -1
- package/dist/instructions.js +162 -1
- package/dist/next-step.js +227 -4
- package/dist/quickstart-validators.js +84 -0
- package/dist/quickstart.js +16 -0
- package/dist/safe-path.js +23 -1
- package/dist/validate.js +72 -1
- package/docs/user/README.md +0 -1
- package/package.json +1 -1
- package/scripts/sync-final-spec-skills.mjs +65 -0
- package/skills/cli-step/SKILL.md +186 -32
- package/skills/continue/SKILL.md +30 -326
- package/skills/shared/thin-adapter-loop.md +67 -0
- package/skills/start/SKILL.md +23 -440
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { readCheckpoint } from "../checkpoint.js";
|
|
7
|
+
async function writeText(absPath, contents) {
|
|
8
|
+
await mkdir(dirname(absPath), { recursive: true });
|
|
9
|
+
await writeFile(absPath, contents, "utf8");
|
|
10
|
+
}
|
|
11
|
+
async function writeJson(absPath, payload) {
|
|
12
|
+
await writeText(absPath, `${JSON.stringify(payload, null, 2)}\n`);
|
|
13
|
+
}
|
|
14
|
+
test("readCheckpoint rejects invalid quickstart_phase string", async () => {
|
|
15
|
+
const rootDir = await mkdtemp(join(tmpdir(), "novel-checkpoint-quickstart-phase-"));
|
|
16
|
+
await writeJson(join(rootDir, ".checkpoint.json"), {
|
|
17
|
+
last_completed_chapter: 0,
|
|
18
|
+
current_volume: 1,
|
|
19
|
+
orchestrator_state: "QUICK_START",
|
|
20
|
+
pipeline_stage: null,
|
|
21
|
+
inflight_chapter: null,
|
|
22
|
+
quickstart_phase: "banana"
|
|
23
|
+
});
|
|
24
|
+
await assert.rejects(() => readCheckpoint(rootDir), /quickstart_phase must be one of:/);
|
|
25
|
+
});
|
|
26
|
+
test("readCheckpoint rejects non-string quickstart_phase", async () => {
|
|
27
|
+
const rootDir = await mkdtemp(join(tmpdir(), "novel-checkpoint-quickstart-phase-non-string-"));
|
|
28
|
+
await writeJson(join(rootDir, ".checkpoint.json"), {
|
|
29
|
+
last_completed_chapter: 0,
|
|
30
|
+
current_volume: 1,
|
|
31
|
+
orchestrator_state: "QUICK_START",
|
|
32
|
+
pipeline_stage: null,
|
|
33
|
+
inflight_chapter: null,
|
|
34
|
+
quickstart_phase: 42
|
|
35
|
+
});
|
|
36
|
+
await assert.rejects(() => readCheckpoint(rootDir), /quickstart_phase must be a string/);
|
|
37
|
+
});
|
|
38
|
+
test("readCheckpoint accepts legacy checkpoint without quickstart_phase", async () => {
|
|
39
|
+
const rootDir = await mkdtemp(join(tmpdir(), "novel-checkpoint-quickstart-phase-legacy-missing-"));
|
|
40
|
+
await writeJson(join(rootDir, ".checkpoint.json"), {
|
|
41
|
+
last_completed_chapter: 0,
|
|
42
|
+
current_volume: 1,
|
|
43
|
+
orchestrator_state: "QUICK_START",
|
|
44
|
+
pipeline_stage: null,
|
|
45
|
+
inflight_chapter: null
|
|
46
|
+
});
|
|
47
|
+
const checkpoint = await readCheckpoint(rootDir);
|
|
48
|
+
assert.equal(checkpoint.quickstart_phase ?? null, null);
|
|
49
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { main } from "../cli.js";
|
|
7
|
+
async function writeText(absPath, contents) {
|
|
8
|
+
await mkdir(dirname(absPath), { recursive: true });
|
|
9
|
+
await writeFile(absPath, contents, "utf8");
|
|
10
|
+
}
|
|
11
|
+
async function writeJson(absPath, payload) {
|
|
12
|
+
await writeText(absPath, `${JSON.stringify(payload, null, 2)}\n`);
|
|
13
|
+
}
|
|
14
|
+
async function runCli(argv) {
|
|
15
|
+
let stdout = "";
|
|
16
|
+
let stderr = "";
|
|
17
|
+
const origOut = process.stdout.write;
|
|
18
|
+
const origErr = process.stderr.write;
|
|
19
|
+
process.stdout.write = (chunk) => {
|
|
20
|
+
stdout += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
|
|
21
|
+
return true;
|
|
22
|
+
};
|
|
23
|
+
process.stderr.write = (chunk) => {
|
|
24
|
+
stderr += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
|
|
25
|
+
return true;
|
|
26
|
+
};
|
|
27
|
+
const prevExitCode = process.exitCode;
|
|
28
|
+
try {
|
|
29
|
+
const code = await main(argv);
|
|
30
|
+
return { code, stdout, stderr };
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
process.exitCode = prevExitCode;
|
|
34
|
+
process.stdout.write = origOut;
|
|
35
|
+
process.stderr.write = origErr;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
test("novel instructions rejects --novel-ask-file without --answer-path (json mode)", async () => {
|
|
39
|
+
const rootDir = await mkdtemp(join(tmpdir(), "novel-cli-novel-ask-missing-answer-"));
|
|
40
|
+
await writeJson(join(rootDir, ".checkpoint.json"), {
|
|
41
|
+
last_completed_chapter: 0,
|
|
42
|
+
current_volume: 1,
|
|
43
|
+
orchestrator_state: "INIT",
|
|
44
|
+
pipeline_stage: null,
|
|
45
|
+
inflight_chapter: null
|
|
46
|
+
});
|
|
47
|
+
const res = await runCli([
|
|
48
|
+
"--json",
|
|
49
|
+
"--project",
|
|
50
|
+
rootDir,
|
|
51
|
+
"instructions",
|
|
52
|
+
"quickstart:world",
|
|
53
|
+
"--novel-ask-file",
|
|
54
|
+
"staging/novel-ask/question.json"
|
|
55
|
+
]);
|
|
56
|
+
assert.equal(res.code, 2);
|
|
57
|
+
const payload = JSON.parse(res.stdout.trim());
|
|
58
|
+
assert.equal(payload.ok, false);
|
|
59
|
+
assert.match(payload.error.message, /provide both --novel-ask-file and --answer-path/);
|
|
60
|
+
});
|
|
61
|
+
test("novel instructions rejects --answer-path without --novel-ask-file (json mode)", async () => {
|
|
62
|
+
const rootDir = await mkdtemp(join(tmpdir(), "novel-cli-novel-ask-missing-ask-"));
|
|
63
|
+
await writeJson(join(rootDir, ".checkpoint.json"), {
|
|
64
|
+
last_completed_chapter: 0,
|
|
65
|
+
current_volume: 1,
|
|
66
|
+
orchestrator_state: "INIT",
|
|
67
|
+
pipeline_stage: null,
|
|
68
|
+
inflight_chapter: null
|
|
69
|
+
});
|
|
70
|
+
const res = await runCli([
|
|
71
|
+
"--json",
|
|
72
|
+
"--project",
|
|
73
|
+
rootDir,
|
|
74
|
+
"instructions",
|
|
75
|
+
"quickstart:world",
|
|
76
|
+
"--answer-path",
|
|
77
|
+
"staging/novel-ask/answer.json"
|
|
78
|
+
]);
|
|
79
|
+
assert.equal(res.code, 2);
|
|
80
|
+
const payload = JSON.parse(res.stdout.trim());
|
|
81
|
+
assert.equal(payload.ok, false);
|
|
82
|
+
assert.match(payload.error.message, /provide both --novel-ask-file and --answer-path/);
|
|
83
|
+
});
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { main } from "../cli.js";
|
|
7
|
+
import { readCheckpoint } from "../checkpoint.js";
|
|
8
|
+
async function writeText(absPath, contents) {
|
|
9
|
+
await mkdir(dirname(absPath), { recursive: true });
|
|
10
|
+
await writeFile(absPath, contents, "utf8");
|
|
11
|
+
}
|
|
12
|
+
async function writeJson(absPath, payload) {
|
|
13
|
+
await writeText(absPath, `${JSON.stringify(payload, null, 2)}\n`);
|
|
14
|
+
}
|
|
15
|
+
async function runCli(argv) {
|
|
16
|
+
let stdout = "";
|
|
17
|
+
let stderr = "";
|
|
18
|
+
const origOut = process.stdout.write;
|
|
19
|
+
const origErr = process.stderr.write;
|
|
20
|
+
process.stdout.write = (chunk) => {
|
|
21
|
+
stdout += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
|
|
22
|
+
return true;
|
|
23
|
+
};
|
|
24
|
+
process.stderr.write = (chunk) => {
|
|
25
|
+
stderr += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
|
|
26
|
+
return true;
|
|
27
|
+
};
|
|
28
|
+
const prevExitCode = process.exitCode;
|
|
29
|
+
try {
|
|
30
|
+
process.exitCode = undefined;
|
|
31
|
+
const code = await main(argv);
|
|
32
|
+
return { code, stdout, stderr };
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
process.exitCode = prevExitCode;
|
|
36
|
+
process.stdout.write = origOut;
|
|
37
|
+
process.stderr.write = origErr;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
test("novel repair previews reset-quickstart without --force (json mode)", async () => {
|
|
41
|
+
const rootDir = await mkdtemp(join(tmpdir(), "novel-cli-repair-quickstart-preview-"));
|
|
42
|
+
await writeJson(join(rootDir, ".checkpoint.json"), {
|
|
43
|
+
last_completed_chapter: 0,
|
|
44
|
+
current_volume: 1,
|
|
45
|
+
orchestrator_state: "QUICK_START",
|
|
46
|
+
pipeline_stage: null,
|
|
47
|
+
inflight_chapter: null,
|
|
48
|
+
quickstart_phase: "characters"
|
|
49
|
+
});
|
|
50
|
+
const res = await runCli(["--json", "--project", rootDir, "repair", "--reset-quickstart"]);
|
|
51
|
+
assert.equal(res.code, 0);
|
|
52
|
+
assert.equal(res.stderr, "");
|
|
53
|
+
const payload = JSON.parse(res.stdout.trim());
|
|
54
|
+
assert.equal(payload.ok, true);
|
|
55
|
+
assert.equal(payload.command, "repair");
|
|
56
|
+
assert.equal(payload.data.rootDir, rootDir);
|
|
57
|
+
assert.deepEqual(payload.data.actions, ["reset_quickstart"]);
|
|
58
|
+
assert.equal(payload.data.applied, false);
|
|
59
|
+
assert.equal(payload.data.before_present, true);
|
|
60
|
+
assert.equal(payload.data.after_present, true);
|
|
61
|
+
assert.equal(payload.data.changed, false);
|
|
62
|
+
assert.equal(payload.data.would_change, true);
|
|
63
|
+
assert.equal(payload.data.before, "characters");
|
|
64
|
+
assert.equal(payload.data.after, "characters");
|
|
65
|
+
assert.equal(payload.data.target_after, null);
|
|
66
|
+
const checkpoint = await readCheckpoint(rootDir);
|
|
67
|
+
assert.equal(checkpoint.quickstart_phase, "characters");
|
|
68
|
+
});
|
|
69
|
+
test("novel repair --reset-quickstart --force sets quickstart_phase=null (json mode)", async () => {
|
|
70
|
+
const rootDir = await mkdtemp(join(tmpdir(), "novel-cli-repair-quickstart-force-"));
|
|
71
|
+
await writeJson(join(rootDir, ".checkpoint.json"), {
|
|
72
|
+
last_completed_chapter: 0,
|
|
73
|
+
current_volume: 1,
|
|
74
|
+
orchestrator_state: "QUICK_START",
|
|
75
|
+
pipeline_stage: null,
|
|
76
|
+
inflight_chapter: null,
|
|
77
|
+
quickstart_phase: "characters"
|
|
78
|
+
});
|
|
79
|
+
const res = await runCli(["--json", "--project", rootDir, "repair", "--reset-quickstart", "--force"]);
|
|
80
|
+
assert.equal(res.code, 0);
|
|
81
|
+
assert.equal(res.stderr, "");
|
|
82
|
+
const payload = JSON.parse(res.stdout.trim());
|
|
83
|
+
assert.equal(payload.ok, true);
|
|
84
|
+
assert.equal(payload.command, "repair");
|
|
85
|
+
assert.equal(payload.data.rootDir, rootDir);
|
|
86
|
+
assert.deepEqual(payload.data.actions, ["reset_quickstart"]);
|
|
87
|
+
assert.equal(payload.data.applied, true);
|
|
88
|
+
assert.equal(payload.data.before_present, true);
|
|
89
|
+
assert.equal(payload.data.after_present, true);
|
|
90
|
+
assert.equal(payload.data.changed, true);
|
|
91
|
+
assert.equal(payload.data.would_change, true);
|
|
92
|
+
assert.equal(payload.data.before, "characters");
|
|
93
|
+
assert.equal(payload.data.after, null);
|
|
94
|
+
assert.equal(payload.data.target_after, null);
|
|
95
|
+
const checkpoint = await readCheckpoint(rootDir);
|
|
96
|
+
assert.equal(checkpoint.quickstart_phase, null);
|
|
97
|
+
const raw = await readFile(join(rootDir, ".checkpoint.json"), "utf8");
|
|
98
|
+
assert.match(raw, /\"quickstart_phase\": null/);
|
|
99
|
+
await assert.rejects(() => stat(join(rootDir, ".novel.lock")), /ENOENT/);
|
|
100
|
+
});
|
|
101
|
+
test("novel repair --reset-quickstart --force normalizes missing quickstart_phase to null (json mode)", async () => {
|
|
102
|
+
const rootDir = await mkdtemp(join(tmpdir(), "novel-cli-repair-quickstart-missing-"));
|
|
103
|
+
await writeJson(join(rootDir, ".checkpoint.json"), {
|
|
104
|
+
last_completed_chapter: 0,
|
|
105
|
+
current_volume: 1,
|
|
106
|
+
orchestrator_state: "QUICK_START",
|
|
107
|
+
pipeline_stage: null,
|
|
108
|
+
inflight_chapter: null
|
|
109
|
+
});
|
|
110
|
+
const res = await runCli(["--json", "--project", rootDir, "repair", "--reset-quickstart", "--force"]);
|
|
111
|
+
assert.equal(res.code, 0);
|
|
112
|
+
assert.equal(res.stderr, "");
|
|
113
|
+
const payload = JSON.parse(res.stdout.trim());
|
|
114
|
+
assert.equal(payload.ok, true);
|
|
115
|
+
assert.equal(payload.command, "repair");
|
|
116
|
+
assert.equal(payload.data.applied, true);
|
|
117
|
+
assert.equal(payload.data.before_present, false);
|
|
118
|
+
assert.equal(payload.data.after_present, true);
|
|
119
|
+
assert.equal(payload.data.changed, true);
|
|
120
|
+
assert.equal(payload.data.would_change, true);
|
|
121
|
+
assert.equal(payload.data.before, null);
|
|
122
|
+
assert.equal(payload.data.after, null);
|
|
123
|
+
const checkpoint = await readCheckpoint(rootDir);
|
|
124
|
+
assert.equal(checkpoint.quickstart_phase, null);
|
|
125
|
+
});
|
|
126
|
+
test("novel repair --reset-quickstart --force is idempotent when already null (json mode)", async () => {
|
|
127
|
+
const rootDir = await mkdtemp(join(tmpdir(), "novel-cli-repair-quickstart-noop-"));
|
|
128
|
+
await writeJson(join(rootDir, ".checkpoint.json"), {
|
|
129
|
+
last_completed_chapter: 0,
|
|
130
|
+
current_volume: 1,
|
|
131
|
+
orchestrator_state: "QUICK_START",
|
|
132
|
+
pipeline_stage: null,
|
|
133
|
+
inflight_chapter: null,
|
|
134
|
+
quickstart_phase: null
|
|
135
|
+
});
|
|
136
|
+
const res = await runCli(["--json", "--project", rootDir, "repair", "--reset-quickstart", "--force"]);
|
|
137
|
+
assert.equal(res.code, 0);
|
|
138
|
+
const payload = JSON.parse(res.stdout.trim());
|
|
139
|
+
assert.equal(payload.ok, true);
|
|
140
|
+
assert.equal(payload.data.applied, true);
|
|
141
|
+
assert.equal(payload.data.changed, false);
|
|
142
|
+
assert.equal(payload.data.would_change, false);
|
|
143
|
+
assert.equal(payload.data.before, null);
|
|
144
|
+
assert.equal(payload.data.after, null);
|
|
145
|
+
});
|
|
146
|
+
test("novel repair --reset-quickstart fails gracefully when checkpoint missing (json mode)", async () => {
|
|
147
|
+
const rootDir = await mkdtemp(join(tmpdir(), "novel-cli-repair-quickstart-no-checkpoint-"));
|
|
148
|
+
const res = await runCli(["--json", "--project", rootDir, "repair", "--reset-quickstart", "--force"]);
|
|
149
|
+
assert.equal(res.code, 2);
|
|
150
|
+
const payload = JSON.parse(res.stdout.trim());
|
|
151
|
+
assert.equal(payload.ok, false);
|
|
152
|
+
assert.equal(payload.command, "repair");
|
|
153
|
+
});
|
|
154
|
+
test("novel repair --reset-quickstart fails gracefully when checkpoint is corrupt JSON (json mode)", async () => {
|
|
155
|
+
const rootDir = await mkdtemp(join(tmpdir(), "novel-cli-repair-quickstart-corrupt-"));
|
|
156
|
+
await writeText(join(rootDir, ".checkpoint.json"), "{ broken json !!!}");
|
|
157
|
+
const res = await runCli(["--json", "--project", rootDir, "repair", "--reset-quickstart", "--force"]);
|
|
158
|
+
assert.ok(res.code !== 0, `expected non-zero exit code, got ${res.code}`);
|
|
159
|
+
const payload = JSON.parse(res.stdout.trim());
|
|
160
|
+
assert.equal(payload.ok, false);
|
|
161
|
+
assert.equal(payload.command, "repair");
|
|
162
|
+
});
|
|
163
|
+
test("novel repair preview reports after_present=true even when field is missing (json mode)", async () => {
|
|
164
|
+
const rootDir = await mkdtemp(join(tmpdir(), "novel-cli-repair-quickstart-preview-missing-"));
|
|
165
|
+
await writeJson(join(rootDir, ".checkpoint.json"), {
|
|
166
|
+
last_completed_chapter: 0,
|
|
167
|
+
current_volume: 1,
|
|
168
|
+
orchestrator_state: "QUICK_START",
|
|
169
|
+
pipeline_stage: null,
|
|
170
|
+
inflight_chapter: null
|
|
171
|
+
});
|
|
172
|
+
const res = await runCli(["--json", "--project", rootDir, "repair", "--reset-quickstart"]);
|
|
173
|
+
assert.equal(res.code, 0);
|
|
174
|
+
const payload = JSON.parse(res.stdout.trim());
|
|
175
|
+
assert.equal(payload.data.before_present, false);
|
|
176
|
+
assert.equal(payload.data.after_present, true);
|
|
177
|
+
assert.equal(payload.data.would_change, true);
|
|
178
|
+
});
|
|
179
|
+
test("novel repair rejects missing actions (json mode)", async () => {
|
|
180
|
+
const rootDir = await mkdtemp(join(tmpdir(), "novel-cli-repair-missing-action-"));
|
|
181
|
+
await writeJson(join(rootDir, ".checkpoint.json"), {
|
|
182
|
+
last_completed_chapter: 0,
|
|
183
|
+
current_volume: 1,
|
|
184
|
+
orchestrator_state: "INIT",
|
|
185
|
+
pipeline_stage: null,
|
|
186
|
+
inflight_chapter: null
|
|
187
|
+
});
|
|
188
|
+
const res = await runCli(["--json", "--project", rootDir, "repair"]);
|
|
189
|
+
assert.equal(res.code, 2);
|
|
190
|
+
const payload = JSON.parse(res.stdout.trim());
|
|
191
|
+
assert.equal(payload.ok, false);
|
|
192
|
+
assert.equal(payload.command, "repair");
|
|
193
|
+
assert.match(payload.error.message, /no actions specified/i);
|
|
194
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import { main } from "../cli.js";
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
const pkg = require("../../package.json");
|
|
7
|
+
async function runCli(argv) {
|
|
8
|
+
let stdout = "";
|
|
9
|
+
let stderr = "";
|
|
10
|
+
const origOut = process.stdout.write;
|
|
11
|
+
const origErr = process.stderr.write;
|
|
12
|
+
process.stdout.write = (chunk) => {
|
|
13
|
+
stdout += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
|
|
14
|
+
return true;
|
|
15
|
+
};
|
|
16
|
+
process.stderr.write = (chunk) => {
|
|
17
|
+
stderr += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
|
|
18
|
+
return true;
|
|
19
|
+
};
|
|
20
|
+
const prevExitCode = process.exitCode;
|
|
21
|
+
try {
|
|
22
|
+
const code = await main(argv);
|
|
23
|
+
return { code, stdout, stderr };
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
process.exitCode = prevExitCode;
|
|
27
|
+
process.stdout.write = origOut;
|
|
28
|
+
process.stderr.write = origErr;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
test("novel --version prints the package version", async () => {
|
|
32
|
+
const expected = typeof pkg.version === "string" ? pkg.version : null;
|
|
33
|
+
assert.ok(expected, "Expected package.json to contain a string version.");
|
|
34
|
+
const res = await runCli(["--version"]);
|
|
35
|
+
assert.equal(res.code, 0);
|
|
36
|
+
assert.equal(res.stdout.trim(), expected);
|
|
37
|
+
assert.equal(res.stderr, "");
|
|
38
|
+
});
|
|
@@ -58,26 +58,28 @@ test("initProject creates a runnable skeleton with all checkpoint fields", async
|
|
|
58
58
|
assert.equal(result.rootDir, rootDir);
|
|
59
59
|
// Exact created set (non-minimal = checkpoint + 4 templates)
|
|
60
60
|
assert.deepEqual(result.created.sort(), [".checkpoint.json", "ai-blacklist.json", "brief.md", "style-profile.json", "web-novel-cliche-lint.json"].sort());
|
|
61
|
-
// All
|
|
62
|
-
assert.equal(result.ensuredDirs.length,
|
|
61
|
+
// All staging dirs ensured
|
|
62
|
+
assert.equal(result.ensuredDirs.length, 10);
|
|
63
63
|
assert.ok(result.ensuredDirs.includes("staging/chapters"));
|
|
64
64
|
assert.ok(result.ensuredDirs.includes("staging/manifests"));
|
|
65
65
|
assert.ok(result.ensuredDirs.includes("staging/volumes"));
|
|
66
66
|
assert.ok(result.ensuredDirs.includes("staging/foreshadowing"));
|
|
67
|
+
assert.ok(result.ensuredDirs.includes("staging/quickstart"));
|
|
67
68
|
// Verify ALL checkpoint fields
|
|
68
69
|
const checkpoint = await readCheckpoint(rootDir);
|
|
69
70
|
assert.equal(checkpoint.last_completed_chapter, 0);
|
|
70
71
|
assert.equal(checkpoint.current_volume, 1);
|
|
71
|
-
assert.equal(checkpoint.
|
|
72
|
+
assert.equal(checkpoint.orchestrator_state, "INIT");
|
|
73
|
+
assert.equal(checkpoint.pipeline_stage, null);
|
|
72
74
|
assert.equal(checkpoint.volume_pipeline_stage, null);
|
|
73
75
|
assert.equal(checkpoint.inflight_chapter, null);
|
|
74
76
|
assert.equal(checkpoint.revision_count, 0);
|
|
75
77
|
assert.equal(checkpoint.hook_fix_count, 0);
|
|
76
78
|
assert.equal(checkpoint.title_fix_count, 0);
|
|
77
79
|
assert.ok(typeof checkpoint.last_checkpoint_time === "string" && checkpoint.last_checkpoint_time.length > 0);
|
|
78
|
-
// Integration: next step should be
|
|
80
|
+
// Integration: next step should be quickstart:world
|
|
79
81
|
const next = await computeNextStep(rootDir, checkpoint);
|
|
80
|
-
assert.equal(next.step, "
|
|
82
|
+
assert.equal(next.step, "quickstart:world");
|
|
81
83
|
// All staging dirs exist
|
|
82
84
|
for (const relDir of [
|
|
83
85
|
"staging/chapters",
|
|
@@ -88,7 +90,8 @@ test("initProject creates a runnable skeleton with all checkpoint fields", async
|
|
|
88
90
|
"staging/storylines",
|
|
89
91
|
"staging/volumes",
|
|
90
92
|
"staging/foreshadowing",
|
|
91
|
-
"staging/manifests"
|
|
93
|
+
"staging/manifests",
|
|
94
|
+
"staging/quickstart"
|
|
92
95
|
]) {
|
|
93
96
|
await assertDir(join(rootDir, relDir));
|
|
94
97
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { buildInstructionPacket } from "../instructions.js";
|
|
7
|
+
test("buildInstructionPacket rejects NOVEL_ASK gate for review steps", async () => {
|
|
8
|
+
const rootDir = await mkdtemp(join(tmpdir(), "novel-review-novel-ask-"));
|
|
9
|
+
const questionSpec = {
|
|
10
|
+
version: 1,
|
|
11
|
+
topic: "review_gate",
|
|
12
|
+
questions: [
|
|
13
|
+
{
|
|
14
|
+
id: "ok_to_continue",
|
|
15
|
+
header: "Continue?",
|
|
16
|
+
question: "Continue the review pipeline?",
|
|
17
|
+
kind: "single_choice",
|
|
18
|
+
required: true,
|
|
19
|
+
options: [{ label: "yes", description: "Proceed" }]
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
};
|
|
23
|
+
await assert.rejects(async () => buildInstructionPacket({
|
|
24
|
+
rootDir,
|
|
25
|
+
checkpoint: { last_completed_chapter: 0, current_volume: 1, orchestrator_state: "VOL_REVIEW" },
|
|
26
|
+
step: { kind: "review", phase: "collect" },
|
|
27
|
+
embedMode: null,
|
|
28
|
+
writeManifest: false,
|
|
29
|
+
novelAskGate: { novel_ask: questionSpec, answer_path: "staging/novel-ask/review.json" }
|
|
30
|
+
}), /NOVEL_ASK gate is not supported for review steps/);
|
|
31
|
+
});
|
|
@@ -62,15 +62,17 @@ test("readCheckpoint rejects inflight_chapter=0", async () => {
|
|
|
62
62
|
});
|
|
63
63
|
await assert.rejects(() => readCheckpoint(rootDir), /inflight_chapter must be an int >= 1/);
|
|
64
64
|
});
|
|
65
|
-
test("computeNextStep
|
|
65
|
+
test("computeNextStep routes INIT to quickstart pipeline", async () => {
|
|
66
66
|
const rootDir = await mkdtemp(join(tmpdir(), "novel-orchestrator-init-"));
|
|
67
|
-
|
|
67
|
+
const next = await computeNextStep(rootDir, {
|
|
68
68
|
last_completed_chapter: 0,
|
|
69
69
|
current_volume: 1,
|
|
70
70
|
orchestrator_state: "INIT",
|
|
71
71
|
pipeline_stage: null,
|
|
72
72
|
inflight_chapter: null
|
|
73
|
-
})
|
|
73
|
+
});
|
|
74
|
+
assert.equal(next.step, "quickstart:world");
|
|
75
|
+
assert.equal(next.reason, "init:quickstart:world");
|
|
74
76
|
});
|
|
75
77
|
test("computeNextStep throws when pipeline_stage=committed but inflight_chapter is set", async () => {
|
|
76
78
|
const rootDir = await mkdtemp(join(tmpdir(), "novel-orchestrator-committed-inflight-"));
|
|
@@ -82,15 +84,17 @@ test("computeNextStep throws when pipeline_stage=committed but inflight_chapter
|
|
|
82
84
|
inflight_chapter: 7
|
|
83
85
|
}), /Checkpoint inconsistent: pipeline_stage=committed but inflight_chapter=7/);
|
|
84
86
|
});
|
|
85
|
-
test("computeNextStep
|
|
87
|
+
test("computeNextStep routes QUICK_START to quickstart pipeline", async () => {
|
|
86
88
|
const rootDir = await mkdtemp(join(tmpdir(), "novel-orchestrator-quickstart-"));
|
|
87
|
-
|
|
89
|
+
const next = await computeNextStep(rootDir, {
|
|
88
90
|
last_completed_chapter: 0,
|
|
89
91
|
current_volume: 1,
|
|
90
92
|
orchestrator_state: "QUICK_START",
|
|
91
93
|
pipeline_stage: null,
|
|
92
94
|
inflight_chapter: null
|
|
93
|
-
})
|
|
95
|
+
});
|
|
96
|
+
assert.equal(next.step, "quickstart:world");
|
|
97
|
+
assert.equal(next.reason, "quickstart:world");
|
|
94
98
|
});
|
|
95
99
|
test("computeNextStep prefixes reason for ERROR_RETRY", async () => {
|
|
96
100
|
const rootDir = await mkdtemp(join(tmpdir(), "novel-orchestrator-error-retry-"));
|