clawspec 1.0.16 → 1.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +16 -0
  2. package/README.zh-CN.md +16 -0
  3. package/package.json +2 -3
  4. package/src/acp/client.ts +17 -1
  5. package/src/orchestrator/helpers.ts +1 -0
  6. package/src/orchestrator/service.ts +143 -13
  7. package/src/watchers/manager.ts +89 -7
  8. package/src/worker/io-helper.ts +6 -5
  9. package/test/acp-client.test.ts +0 -309
  10. package/test/acpx-dependency.test.ts +0 -133
  11. package/test/assistant-journal.test.ts +0 -203
  12. package/test/command-surface.test.ts +0 -23
  13. package/test/config.test.ts +0 -77
  14. package/test/detach-attach.test.ts +0 -98
  15. package/test/file-lock.test.ts +0 -88
  16. package/test/fs-utils.test.ts +0 -22
  17. package/test/helpers/harness.ts +0 -301
  18. package/test/helpers.test.ts +0 -108
  19. package/test/keywords.test.ts +0 -92
  20. package/test/notifier.test.ts +0 -29
  21. package/test/openspec-dependency.test.ts +0 -68
  22. package/test/paths-utils.test.ts +0 -30
  23. package/test/pause-cancel.test.ts +0 -55
  24. package/test/planning-journal.test.ts +0 -155
  25. package/test/plugin-registration.test.ts +0 -35
  26. package/test/project-memory.test.ts +0 -42
  27. package/test/proposal.test.ts +0 -24
  28. package/test/queue-planning.test.ts +0 -322
  29. package/test/queue-work.test.ts +0 -220
  30. package/test/recovery.test.ts +0 -576
  31. package/test/service-archive.test.ts +0 -87
  32. package/test/shell-command.test.ts +0 -48
  33. package/test/state-store.test.ts +0 -74
  34. package/test/tasks-and-checkpoint.test.ts +0 -60
  35. package/test/use-project.test.ts +0 -67
  36. package/test/watcher-planning.test.ts +0 -504
  37. package/test/watcher-work.test.ts +0 -1741
  38. package/test/worker-command.test.ts +0 -66
  39. package/test/worker-io-helper.test.ts +0 -97
  40. package/test/worker-skills.test.ts +0 -12
@@ -1,92 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import {
4
- parseClawSpecKeyword,
5
- extractEmbeddedClawSpecKeyword,
6
- isClawSpecKeywordText,
7
- } from "../src/control/keywords.ts";
8
-
9
- test("parseClawSpecKeyword recognizes all primary commands", () => {
10
- const commands = [
11
- ["cs-plan", "plan"],
12
- ["cs-work", "work"],
13
- ["cs-attach", "attach"],
14
- ["cs-detach", "detach"],
15
- ["cs-deattach", "detach"],
16
- ["cs-pause", "pause"],
17
- ["cs-continue", "continue"],
18
- ["cs-status", "status"],
19
- ["cs-cancel", "cancel"],
20
- ] as const;
21
-
22
- for (const [input, expectedKind] of commands) {
23
- const result = parseClawSpecKeyword(input);
24
- assert.equal(result?.kind, expectedKind, `${input} should be kind "${expectedKind}"`);
25
- assert.equal(result?.command, input.toLowerCase());
26
- }
27
- });
28
-
29
- test("parseClawSpecKeyword handles args", () => {
30
- const result = parseClawSpecKeyword("cs-continue now please");
31
- assert.equal(result?.kind, "continue");
32
- assert.equal(result?.args, "now please");
33
- assert.equal(result?.raw, "cs-continue now please");
34
- });
35
-
36
- test("parseClawSpecKeyword returns null for non-cs- prefixed text", () => {
37
- assert.equal(parseClawSpecKeyword("hello world"), null);
38
- assert.equal(parseClawSpecKeyword("/clawspec proposal"), null);
39
- assert.equal(parseClawSpecKeyword(""), null);
40
- assert.equal(parseClawSpecKeyword("cs-nonexistent"), null);
41
- });
42
-
43
- test("parseClawSpecKeyword is case-insensitive for command", () => {
44
- const result = parseClawSpecKeyword("CS-PLAN");
45
- assert.equal(result?.kind, "plan");
46
- assert.equal(result?.command, "cs-plan");
47
- });
48
-
49
- test("parseClawSpecKeyword trims input", () => {
50
- const result = parseClawSpecKeyword(" cs-work ");
51
- assert.equal(result?.kind, "work");
52
- });
53
-
54
- test("parseClawSpecKeyword accepts slash-prefixed and punctuated control words", () => {
55
- assert.equal(parseClawSpecKeyword("/cs-work")?.kind, "work");
56
- assert.equal(parseClawSpecKeyword("`cs-plan`")?.kind, "plan");
57
- assert.equal(parseClawSpecKeyword("cs-work。")?.kind, "work");
58
- });
59
-
60
- test("isClawSpecKeywordText returns boolean correctly", () => {
61
- assert.equal(isClawSpecKeywordText("cs-plan"), true);
62
- assert.equal(isClawSpecKeywordText("hello"), false);
63
- });
64
-
65
- test("extractEmbeddedClawSpecKeyword finds keyword in multiline text", () => {
66
- const text = "Here is some context\n\ncs-work\n\nMore text after";
67
- const result = extractEmbeddedClawSpecKeyword(text);
68
- assert.equal(result?.kind, "work");
69
- });
70
-
71
- test("extractEmbeddedClawSpecKeyword finds keyword with args in multiline text", () => {
72
- const text = "Please do this:\ncs-continue hello world\nThanks!";
73
- const result = extractEmbeddedClawSpecKeyword(text);
74
- assert.equal(result?.kind, "continue");
75
- assert.equal(result?.args, "hello world");
76
- });
77
-
78
- test("extractEmbeddedClawSpecKeyword finds slash-prefixed keyword in multiline text", () => {
79
- const text = "Summary:\n/cs-work\nProceed please.";
80
- const result = extractEmbeddedClawSpecKeyword(text);
81
- assert.equal(result?.kind, "work");
82
- });
83
-
84
- test("extractEmbeddedClawSpecKeyword returns direct match for single-line input", () => {
85
- const result = extractEmbeddedClawSpecKeyword("cs-status");
86
- assert.equal(result?.kind, "status");
87
- });
88
-
89
- test("extractEmbeddedClawSpecKeyword returns null when no keyword present", () => {
90
- assert.equal(extractEmbeddedClawSpecKeyword("no keywords here"), null);
91
- assert.equal(extractEmbeddedClawSpecKeyword("just a normal message\nwith multiple lines"), null);
92
- });
@@ -1,29 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import { ClawSpecNotifier } from "../src/watchers/notifier.ts";
4
- import { createLogger } from "./helpers/harness.ts";
5
-
6
- test("notifier formats Discord channel target correctly", async () => {
7
- const sent: Array<{ to: string; text: string }> = [];
8
- const notifier = new ClawSpecNotifier({
9
- api: {
10
- config: {},
11
- runtime: {
12
- channel: {
13
- discord: {
14
- sendMessageDiscord: async (to: string, text: string) => {
15
- sent.push({ to, text });
16
- },
17
- },
18
- telegram: {},
19
- slack: {},
20
- signal: {},
21
- },
22
- },
23
- } as any,
24
- logger: createLogger() as any,
25
- });
26
-
27
- await notifier.send("discord:1474686041939251210:default:main", "hello");
28
- assert.deepEqual(sent, [{ to: "channel:1474686041939251210", text: "hello" }]);
29
- });
@@ -1,68 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import path from "node:path";
4
- import { buildOpenSpecInstallMessage, ensureOpenSpecCli } from "../src/dependencies/openspec.ts";
5
-
6
- const ROOT_PREFIX = process.platform === "win32" ? "C:\\clawspec-test" : "/tmp/clawspec-test";
7
-
8
- const LOCAL_COMMAND = path.join(
9
- ROOT_PREFIX,
10
- "node_modules",
11
- ".bin",
12
- process.platform === "win32" ? "openspec.cmd" : "openspec",
13
- );
14
-
15
- test("ensureOpenSpecCli uses the global openspec command when available", async () => {
16
- const calls: Array<{ command: string; args: string[] }> = [];
17
- const result = await ensureOpenSpecCli({
18
- pluginRoot: ROOT_PREFIX,
19
- runner: async ({ command, args }) => {
20
- calls.push({ command, args });
21
- if (command === LOCAL_COMMAND) {
22
- return { code: 1, stdout: "", stderr: "not found" };
23
- }
24
- if (command === "openspec") {
25
- return { code: 0, stdout: "1.2.0\n", stderr: "" };
26
- }
27
- return { code: 1, stdout: "", stderr: "unexpected command" };
28
- },
29
- });
30
-
31
- assert.equal(result.source, "global");
32
- assert.equal(result.version, "1.2.0");
33
- assert.equal(calls.some((call) => call.command === "npm"), false);
34
- });
35
-
36
- test("ensureOpenSpecCli installs plugin-local openspec when local and global are unavailable", async () => {
37
- const calls: Array<{ command: string; args: string[] }> = [];
38
- const result = await ensureOpenSpecCli({
39
- pluginRoot: ROOT_PREFIX,
40
- runner: async ({ command, args }) => {
41
- calls.push({ command, args });
42
- if (command === LOCAL_COMMAND) {
43
- const versionChecks = calls.filter((call) => call.command === LOCAL_COMMAND && call.args[0] === "--version").length;
44
- if (versionChecks >= 2) {
45
- return { code: 0, stdout: "1.2.3\n", stderr: "" };
46
- }
47
- return { code: 1, stdout: "", stderr: "not found" };
48
- }
49
- if (command === "openspec") {
50
- return { code: 1, stdout: "", stderr: "not found" };
51
- }
52
- if (command === "npm") {
53
- return { code: 0, stdout: "installed", stderr: "" };
54
- }
55
- return { code: 1, stdout: "", stderr: "unexpected command" };
56
- },
57
- });
58
-
59
- assert.equal(result.source, "local");
60
- assert.equal(result.version, "1.2.3");
61
- assert.equal(calls.some((call) => call.command === "npm"), true);
62
- });
63
-
64
- test("buildOpenSpecInstallMessage includes install command", () => {
65
- const message = buildOpenSpecInstallMessage("not found");
66
- assert.match(message, /npm install -g @fission-ai\/openspec/);
67
- assert.match(message, /not found/);
68
- });
@@ -1,30 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import { resolveUserPath } from "../src/utils/paths.ts";
6
-
7
- test("resolveUserPath expands home directory shorthand", () => {
8
- const actual = resolveUserPath("~/Desktop/workspace/ai_workspace", "/tmp/base");
9
- const expected = path.join(os.homedir(), "Desktop", "workspace", "ai_workspace");
10
- assert.equal(actual, expected);
11
- });
12
-
13
- test("resolveUserPath resolves relative path against base directory", () => {
14
- const base = path.join(os.homedir(), "clawspec", "workspace");
15
- const actual = resolveUserPath("demo-app", base);
16
- assert.equal(actual, path.resolve(base, "demo-app"));
17
- });
18
-
19
- test("resolveUserPath keeps POSIX absolute path absolute", () => {
20
- const absolute = "/tmp/clawspec-posix-absolute";
21
- const actual = resolveUserPath(absolute, "/tmp/base");
22
- assert.equal(actual, path.normalize(absolute));
23
- });
24
-
25
- test("resolveUserPath keeps Windows drive absolute path absolute on all platforms", () => {
26
- const absolute = "C:\\Users\\dev\\workspace\\demo";
27
- const actual = resolveUserPath(absolute, "/tmp/base");
28
- assert.equal(actual, path.normalize(absolute));
29
- });
30
-
@@ -1,55 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import { createServiceHarness, seedPlanningProject } from "./helpers/harness.ts";
4
-
5
- test("pause requests watcher interrupt", async () => {
6
- const harness = await createServiceHarness("clawspec-pause-");
7
- const { service, stateStore, watcherManager, repoPath, workspacePath, changeDir } = harness;
8
- const channelKey = "discord:pause:default:main";
9
-
10
- await seedPlanningProject(stateStore, channelKey, {
11
- workspacePath,
12
- repoPath,
13
- projectName: "demo-app",
14
- changeName: "pause-change",
15
- changeDir,
16
- phase: "implementing",
17
- status: "running",
18
- planningDirty: false,
19
- execution: { action: "work", state: "running", mode: "apply" },
20
- });
21
-
22
- const result = await service.pauseProject(channelKey);
23
- const project = await stateStore.getActiveProject(channelKey);
24
-
25
- assert.match(result.text ?? "", /Pause Requested/);
26
- assert.equal(project?.pauseRequested, true);
27
- assert.deepEqual(watcherManager.interruptCalls, [{ channelKey, reason: "paused by user" }]);
28
- assert.deepEqual(watcherManager.wakeCalls, [channelKey]);
29
- });
30
-
31
- test("cancel requests watcher interrupt", async () => {
32
- const harness = await createServiceHarness("clawspec-cancel-");
33
- const { service, stateStore, watcherManager, repoPath, workspacePath, changeDir } = harness;
34
- const channelKey = "discord:cancel:default:main";
35
-
36
- await seedPlanningProject(stateStore, channelKey, {
37
- workspacePath,
38
- repoPath,
39
- projectName: "demo-app",
40
- changeName: "cancel-change",
41
- changeDir,
42
- phase: "implementing",
43
- status: "running",
44
- planningDirty: false,
45
- execution: { action: "work", state: "running", mode: "apply" },
46
- });
47
-
48
- const result = await service.cancelProject(channelKey);
49
- const project = await stateStore.getActiveProject(channelKey);
50
-
51
- assert.match(result.text ?? "", /Cancellation Requested/);
52
- assert.equal(project?.cancelRequested, true);
53
- assert.deepEqual(watcherManager.interruptCalls, [{ channelKey, reason: "cancelled by user" }]);
54
- assert.deepEqual(watcherManager.wakeCalls, [channelKey]);
55
- });
@@ -1,155 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import { mkdtemp } from "node:fs/promises";
6
- import { PlanningJournalStore } from "../src/planning/journal.ts";
7
-
8
- test("planning journal reports clean when snapshot matches current content", async () => {
9
- const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-journal-clean-"));
10
- const journalPath = path.join(tempRoot, "planning-journal.jsonl");
11
- const snapshotPath = path.join(tempRoot, "planning-journal.snapshot.json");
12
- const store = new PlanningJournalStore(journalPath);
13
-
14
- await store.append({
15
- timestamp: "2026-03-23T07:00:00.000Z",
16
- changeName: "hello",
17
- role: "user",
18
- text: "keep the two existing endpoints",
19
- });
20
- await store.writeSnapshot(snapshotPath, "hello", "2026-03-23T07:05:00.000Z");
21
-
22
- assert.equal(await store.hasUnsyncedChanges("hello", snapshotPath), false);
23
- });
24
-
25
- test("planning journal reports dirty when a new entry is appended after the snapshot", async () => {
26
- const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-journal-dirty-"));
27
- const journalPath = path.join(tempRoot, "planning-journal.jsonl");
28
- const snapshotPath = path.join(tempRoot, "planning-journal.snapshot.json");
29
- const store = new PlanningJournalStore(journalPath);
30
-
31
- await store.append({
32
- timestamp: "2026-03-23T07:00:00.000Z",
33
- changeName: "hello",
34
- role: "user",
35
- text: "keep the two existing endpoints",
36
- });
37
- await store.writeSnapshot(snapshotPath, "hello", "2026-03-23T07:05:00.000Z");
38
- await store.append({
39
- timestamp: "2026-03-23T07:10:00.000Z",
40
- changeName: "hello",
41
- role: "user",
42
- text: "add two new interfaces to the same change",
43
- });
44
-
45
- assert.equal(await store.hasUnsyncedChanges("hello", snapshotPath), true);
46
- });
47
-
48
- test("planning journal falls back to lastSyncedAt when no snapshot exists yet", async () => {
49
- const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-journal-fallback-"));
50
- const journalPath = path.join(tempRoot, "planning-journal.jsonl");
51
- const snapshotPath = path.join(tempRoot, "planning-journal.snapshot.json");
52
- const store = new PlanningJournalStore(journalPath);
53
-
54
- await store.append({
55
- timestamp: "2026-03-23T07:00:00.000Z",
56
- changeName: "hello",
57
- role: "user",
58
- text: "keep the two existing endpoints",
59
- });
60
-
61
- assert.equal(
62
- await store.hasUnsyncedChanges("hello", snapshotPath, "2026-03-23T07:05:00.000Z"),
63
- false,
64
- );
65
- assert.equal(
66
- await store.hasUnsyncedChanges("hello", snapshotPath, "2026-03-23T06:55:00.000Z"),
67
- true,
68
- );
69
- });
70
-
71
- test("snapshot is always written after planning sync regardless of journal dirty state", async () => {
72
- const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-snapshot-always-"));
73
- const journalPath = path.join(tempRoot, "planning-journal.jsonl");
74
- const snapshotPath = path.join(tempRoot, "planning-journal.snapshot.json");
75
- const store = new PlanningJournalStore(journalPath);
76
-
77
- await store.append({
78
- timestamp: "2026-03-27T03:00:00.000Z",
79
- changeName: "test",
80
- role: "user",
81
- text: "initial requirement",
82
- });
83
-
84
- const snapshot1 = await store.writeSnapshot(snapshotPath, "test", "2026-03-27T03:05:00.000Z");
85
- assert.equal(snapshot1.entryCount, 1);
86
-
87
- await store.append({
88
- timestamp: "2026-03-27T03:10:00.000Z",
89
- changeName: "test",
90
- role: "assistant",
91
- text: "planning sync response",
92
- });
93
-
94
- const snapshot2 = await store.writeSnapshot(snapshotPath, "test", "2026-03-27T03:15:00.000Z");
95
- assert.equal(snapshot2.entryCount, 2);
96
- assert.equal(await store.hasUnsyncedChanges("test", snapshotPath), false);
97
- });
98
-
99
- test("snapshot correctly captures all journal entries including assistant messages", async () => {
100
- const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-snapshot-complete-"));
101
- const journalPath = path.join(tempRoot, "planning-journal.jsonl");
102
- const snapshotPath = path.join(tempRoot, "planning-journal.snapshot.json");
103
- const store = new PlanningJournalStore(journalPath);
104
-
105
- await store.append({
106
- timestamp: "2026-03-27T03:00:00.000Z",
107
- changeName: "test",
108
- role: "user",
109
- text: "user requirement",
110
- });
111
- await store.append({
112
- timestamp: "2026-03-27T03:05:00.000Z",
113
- changeName: "test",
114
- role: "assistant",
115
- text: "assistant response",
116
- });
117
-
118
- const snapshot = await store.writeSnapshot(snapshotPath, "test");
119
- const digest = await store.digest("test");
120
-
121
- assert.equal(snapshot.entryCount, digest.entryCount);
122
- assert.equal(snapshot.lastEntryAt, digest.lastEntryAt);
123
- assert.equal(snapshot.contentHash, digest.contentHash);
124
- });
125
-
126
- test("snapshot sync after cs-plan, add requirement, cs-plan again", async () => {
127
- const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-replan-"));
128
- const journalPath = path.join(tempRoot, "planning-journal.jsonl");
129
- const snapshotPath = path.join(tempRoot, "planning-journal.snapshot.json");
130
- const store = new PlanningJournalStore(journalPath);
131
-
132
- await store.append({
133
- timestamp: "2026-03-27T06:00:00.000Z",
134
- changeName: "test",
135
- role: "user",
136
- text: "initial requirement",
137
- });
138
-
139
- const snapshot1 = await store.writeSnapshot(snapshotPath, "test", "2026-03-27T06:05:00.000Z");
140
- assert.equal(snapshot1.entryCount, 1);
141
- assert.equal(await store.hasUnsyncedChanges("test", snapshotPath), false);
142
-
143
- await store.append({
144
- timestamp: "2026-03-27T06:10:00.000Z",
145
- changeName: "test",
146
- role: "user",
147
- text: "additional requirement",
148
- });
149
-
150
- assert.equal(await store.hasUnsyncedChanges("test", snapshotPath), true);
151
-
152
- const snapshot2 = await store.writeSnapshot(snapshotPath, "test", "2026-03-27T06:15:00.000Z");
153
- assert.equal(snapshot2.entryCount, 2);
154
- assert.equal(await store.hasUnsyncedChanges("test", snapshotPath), false);
155
- });
@@ -1,35 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import { mkdtemp } from "node:fs/promises";
6
- import plugin from "../src/index.ts";
7
-
8
- test("plugin registers the clawspec command", async () => {
9
- const stateDir = await mkdtemp(path.join(os.tmpdir(), "clawspec-plugin-register-"));
10
- const commandNames: string[] = [];
11
-
12
- plugin.register({
13
- pluginConfig: {},
14
- config: {},
15
- logger: {
16
- info() {},
17
- warn() {},
18
- error() {},
19
- debug() {},
20
- },
21
- runtime: {
22
- state: {
23
- resolveStateDir: () => stateDir,
24
- },
25
- },
26
- registerService() {},
27
- registerCli() {},
28
- registerCommand(command: { name: string }) {
29
- commandNames.push(command.name);
30
- },
31
- on() {},
32
- } as any);
33
-
34
- assert.deepEqual(commandNames, ["clawspec"]);
35
- });
@@ -1,42 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import { mkdtemp, mkdir } from "node:fs/promises";
6
- import {
7
- DuplicateRememberedProjectError,
8
- ProjectMemoryStore,
9
- RememberedProjectPathInvalidError,
10
- } from "../src/memory/store.ts";
11
-
12
- test("project memory store remembers, lists, overwrites, and validates paths", async () => {
13
- const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-memory-"));
14
- const memoryFile = path.join(tempRoot, "project-memory.json");
15
- const repoA = path.join(tempRoot, "repo-a");
16
- const repoB = path.join(tempRoot, "repo-b");
17
- await mkdir(repoA, { recursive: true });
18
- await mkdir(repoB, { recursive: true });
19
-
20
- const store = new ProjectMemoryStore(memoryFile);
21
- await store.initialize();
22
-
23
- const created = await store.remember("demo", repoA);
24
- assert.equal(created.created, true);
25
- assert.equal(created.overwritten, false);
26
-
27
- const entries = await store.list();
28
- assert.equal(entries.length, 1);
29
- assert.equal(entries[0].repoPath, repoA);
30
-
31
- await assert.rejects(() => store.remember("demo", repoB), DuplicateRememberedProjectError);
32
-
33
- const overwritten = await store.remember("demo", repoB, { overwrite: true });
34
- assert.equal(overwritten.overwritten, true);
35
- assert.equal((await store.resolveForUse("demo")).repoPath, repoB);
36
-
37
- const staleFile = path.join(tempRoot, "stale-memory.json");
38
- const staleStore = new ProjectMemoryStore(staleFile);
39
- await staleStore.initialize();
40
- await staleStore.remember("stale", path.join(tempRoot, "missing"));
41
- await assert.rejects(() => staleStore.resolveForUse("stale"), RememberedProjectPathInvalidError);
42
- });
@@ -1,24 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import path from "node:path";
4
- import { pathExists } from "../src/utils/fs.ts";
5
- import { createServiceHarness } from "./helpers/harness.ts";
6
-
7
- test("proposal creates scaffold and rollback baseline", async () => {
8
- const harness = await createServiceHarness("clawspec-proposal-");
9
- const { service, stateStore, workspacePath } = harness;
10
- const channelKey = "discord:proposal:default:main";
11
-
12
- await service.startProject(channelKey);
13
- await service.useProject(channelKey, "demo-app");
14
- const result = await service.proposalProject(channelKey, "support-weather Add weather endpoints");
15
- const project = await stateStore.getActiveProject(channelKey);
16
- const changeDir = path.join(workspacePath, "demo-app", "openspec", "changes", "support-weather");
17
-
18
- assert.match(result.text ?? "", /Proposal Ready/);
19
- assert.equal(project?.changeName, "support-weather");
20
- assert.equal(project?.status, "ready");
21
- assert.equal(project?.phase, "proposal");
22
- assert.equal(await pathExists(path.join(changeDir, ".openspec.yaml")), true);
23
- assert.equal(await pathExists(project?.rollback?.manifestPath ?? ""), true);
24
- });