agile-context-engineering 0.2.2 → 0.5.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/.claude-plugin/plugin.json +10 -0
- package/CHANGELOG.md +82 -0
- package/README.md +27 -18
- package/agents/ace-product-owner.md +1 -1
- package/agents/ace-technical-application-architect.md +28 -0
- package/agents/ace-wiki-mapper.md +144 -29
- package/bin/install.js +67 -63
- package/hooks/ace-check-update.js +17 -9
- package/package.json +7 -5
- package/shared/lib/ace-core.js +308 -0
- package/shared/lib/ace-core.test.js +308 -0
- package/shared/lib/ace-github.js +753 -0
- package/shared/lib/ace-story.js +400 -0
- package/shared/lib/ace-story.test.js +250 -0
- package/{agile-context-engineering → shared}/utils/ui-formatting.md +299 -299
- package/skills/execute-story/SKILL.md +110 -0
- package/skills/execute-story/script.js +305 -0
- package/skills/execute-story/script.test.js +261 -0
- package/skills/execute-story/walkthrough-template.xml +255 -0
- package/{agile-context-engineering/workflows/execute-story.xml → skills/execute-story/workflow.xml} +83 -9
- package/skills/help/SKILL.md +69 -0
- package/skills/help/script.js +318 -0
- package/skills/help/script.test.js +183 -0
- package/{agile-context-engineering/workflows/help.xml → skills/help/workflow.xml} +8 -8
- package/skills/init-coding-standards/SKILL.md +72 -0
- package/{agile-context-engineering/templates/wiki/coding-standards.xml → skills/init-coding-standards/coding-standards-template.xml} +38 -0
- package/skills/init-coding-standards/script.js +59 -0
- package/skills/init-coding-standards/script.test.js +70 -0
- package/{agile-context-engineering/workflows/init-coding-standards.xml → skills/init-coding-standards/workflow.xml} +4 -9
- package/skills/map-cross-cutting/SKILL.md +89 -0
- package/skills/map-cross-cutting/workflow.xml +330 -0
- package/skills/map-guide/SKILL.md +89 -0
- package/skills/map-guide/workflow.xml +320 -0
- package/skills/map-pattern/SKILL.md +89 -0
- package/skills/map-pattern/workflow.xml +331 -0
- package/skills/map-story/SKILL.md +127 -0
- package/skills/map-story/templates/guide.xml +137 -0
- package/skills/map-story/templates/pattern.xml +159 -0
- package/skills/map-story/templates/system-cross-cutting.xml +197 -0
- package/skills/map-story/templates/walkthrough.xml +255 -0
- package/{agile-context-engineering/workflows/map-story.xml → skills/map-story/workflow.xml} +258 -9
- package/skills/map-subsystem/SKILL.md +111 -0
- package/skills/map-subsystem/script.js +60 -0
- package/skills/map-subsystem/script.test.js +68 -0
- package/skills/map-subsystem/templates/decizions.xml +115 -0
- package/skills/map-subsystem/templates/guide.xml +137 -0
- package/{agile-context-engineering/templates/wiki → skills/map-subsystem/templates}/module-discovery.xml +3 -3
- package/skills/map-subsystem/templates/pattern.xml +159 -0
- package/skills/map-subsystem/templates/system-cross-cutting.xml +197 -0
- package/skills/map-subsystem/templates/system.xml +381 -0
- package/skills/map-subsystem/templates/walkthrough.xml +255 -0
- package/{agile-context-engineering/workflows/map-subsystem.xml → skills/map-subsystem/workflow.xml} +17 -21
- package/skills/map-sys-doc/SKILL.md +90 -0
- package/skills/map-sys-doc/system.xml +381 -0
- package/skills/map-sys-doc/workflow.xml +336 -0
- package/skills/map-system/SKILL.md +85 -0
- package/skills/map-system/script.js +84 -0
- package/skills/map-system/script.test.js +73 -0
- package/skills/map-system/templates/wiki-readme.xml +297 -0
- package/{agile-context-engineering/workflows/map-system.xml → skills/map-system/workflow.xml} +11 -16
- package/skills/map-walkthrough/SKILL.md +92 -0
- package/skills/map-walkthrough/walkthrough.xml +255 -0
- package/skills/map-walkthrough/workflow.xml +457 -0
- package/skills/plan-backlog/SKILL.md +75 -0
- package/skills/plan-backlog/script.js +136 -0
- package/skills/plan-backlog/script.test.js +83 -0
- package/{agile-context-engineering/workflows/plan-backlog.xml → skills/plan-backlog/workflow.xml} +13 -21
- package/skills/plan-feature/SKILL.md +76 -0
- package/skills/plan-feature/script.js +148 -0
- package/skills/plan-feature/script.test.js +80 -0
- package/{agile-context-engineering/workflows/plan-feature.xml → skills/plan-feature/workflow.xml} +21 -29
- package/skills/plan-product-vision/SKILL.md +75 -0
- package/skills/plan-product-vision/script.js +60 -0
- package/skills/plan-product-vision/script.test.js +69 -0
- package/{agile-context-engineering/workflows/plan-product-vision.xml → skills/plan-product-vision/workflow.xml} +4 -9
- package/skills/plan-story/SKILL.md +116 -0
- package/skills/plan-story/script.js +326 -0
- package/skills/plan-story/script.test.js +240 -0
- package/skills/plan-story/story-template.xml +451 -0
- package/{agile-context-engineering/workflows/plan-story.xml → skills/plan-story/workflow.xml} +1285 -909
- package/skills/research-external-solution/SKILL.md +107 -0
- package/skills/research-external-solution/script.js +238 -0
- package/skills/research-external-solution/script.test.js +134 -0
- package/{agile-context-engineering/workflows/research-external-solution.xml → skills/research-external-solution/workflow.xml} +4 -6
- package/skills/research-integration-solution/SKILL.md +98 -0
- package/{agile-context-engineering/templates/product/story-integration-solution.xml → skills/research-integration-solution/integration-solution-template.xml} +1 -0
- package/skills/research-integration-solution/script.js +231 -0
- package/skills/research-integration-solution/script.test.js +134 -0
- package/{agile-context-engineering/workflows/research-integration-solution.xml → skills/research-integration-solution/workflow.xml} +4 -5
- package/skills/research-story-wiki/SKILL.md +92 -0
- package/skills/research-story-wiki/script.js +231 -0
- package/skills/research-story-wiki/script.test.js +138 -0
- package/{agile-context-engineering/templates/product/story-wiki.xml → skills/research-story-wiki/story-wiki-template.xml} +4 -0
- package/{agile-context-engineering/workflows/research-story-wiki.xml → skills/research-story-wiki/workflow.xml} +5 -6
- package/skills/research-technical-solution/SKILL.md +103 -0
- package/skills/research-technical-solution/script.js +231 -0
- package/skills/research-technical-solution/script.test.js +134 -0
- package/{agile-context-engineering/workflows/research-technical-solution.xml → skills/research-technical-solution/workflow.xml} +4 -5
- package/skills/review-story/SKILL.md +100 -0
- package/skills/review-story/script.js +257 -0
- package/skills/review-story/script.test.js +169 -0
- package/skills/review-story/story-template.xml +451 -0
- package/{agile-context-engineering/workflows/review-story.xml → skills/review-story/workflow.xml} +1 -3
- package/skills/update/SKILL.md +53 -0
- package/{agile-context-engineering/workflows/update.xml → skills/update/workflow.xml} +237 -207
- package/agile-context-engineering/src/ace-tools.js +0 -2881
- package/agile-context-engineering/src/ace-tools.test.js +0 -1089
- package/agile-context-engineering/templates/_command.md +0 -54
- package/agile-context-engineering/templates/_workflow.xml +0 -17
- package/agile-context-engineering/templates/config.json +0 -0
- package/agile-context-engineering/templates/product/integration-solution.xml +0 -0
- package/agile-context-engineering/templates/wiki/wiki-readme.xml +0 -276
- package/commands/ace/execute-story.md +0 -137
- package/commands/ace/help.md +0 -93
- package/commands/ace/init-coding-standards.md +0 -83
- package/commands/ace/map-story.md +0 -156
- package/commands/ace/map-subsystem.md +0 -138
- package/commands/ace/map-system.md +0 -92
- package/commands/ace/plan-backlog.md +0 -83
- package/commands/ace/plan-feature.md +0 -89
- package/commands/ace/plan-product-vision.md +0 -81
- package/commands/ace/plan-story.md +0 -145
- package/commands/ace/research-external-solution.md +0 -138
- package/commands/ace/research-integration-solution.md +0 -135
- package/commands/ace/research-story-wiki.md +0 -116
- package/commands/ace/research-technical-solution.md +0 -147
- package/commands/ace/review-story.md +0 -109
- package/commands/ace/update.md +0 -54
- /package/{agile-context-engineering → shared}/utils/questioning.xml +0 -0
- /package/{agile-context-engineering/templates/product/story.xml → skills/execute-story/story-template.xml} +0 -0
- /package/{agile-context-engineering/templates/wiki → skills/map-cross-cutting}/system-cross-cutting.xml +0 -0
- /package/{agile-context-engineering/templates/wiki → skills/map-guide}/guide.xml +0 -0
- /package/{agile-context-engineering/templates/wiki → skills/map-pattern}/pattern.xml +0 -0
- /package/{agile-context-engineering/templates/wiki → skills/map-story/templates}/decizions.xml +0 -0
- /package/{agile-context-engineering/templates/wiki → skills/map-story/templates}/system.xml +0 -0
- /package/{agile-context-engineering/templates/wiki → skills/map-story/templates}/tech-debt-index.xml +0 -0
- /package/{agile-context-engineering/templates/wiki → skills/map-subsystem/templates}/subsystem-architecture.xml +0 -0
- /package/{agile-context-engineering/templates/wiki → skills/map-subsystem/templates}/subsystem-structure.xml +0 -0
- /package/{agile-context-engineering/templates/wiki → skills/map-system/templates}/system-architecture.xml +0 -0
- /package/{agile-context-engineering/templates/wiki → skills/map-system/templates}/system-structure.xml +0 -0
- /package/{agile-context-engineering/templates/wiki → skills/map-system/templates}/testing-framework.xml +0 -0
- /package/{agile-context-engineering/templates/product/product-backlog.xml → skills/plan-backlog/product-backlog-template.xml} +0 -0
- /package/{agile-context-engineering/templates/product/feature.xml → skills/plan-feature/feature-template.xml} +0 -0
- /package/{agile-context-engineering/templates/product/product-vision.xml → skills/plan-product-vision/product-vision-template.xml} +0 -0
- /package/{agile-context-engineering/templates/product/external-solution.xml → skills/research-external-solution/external-solution-template.xml} +0 -0
- /package/{agile-context-engineering/templates/product/story-technical-solution.xml → skills/research-technical-solution/technical-solution-template.xml} +0 -0
|
@@ -1,1089 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ACE Tools Tests
|
|
3
|
-
*
|
|
4
|
-
* Inspired by GSD's gsd-tools.test.js (https://github.com/gsd-build/get-shit-done).
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const { test, describe, beforeEach, afterEach } = require('node:test');
|
|
8
|
-
const assert = require('node:assert');
|
|
9
|
-
const fs = require('fs');
|
|
10
|
-
const path = require('path');
|
|
11
|
-
const { execSync } = require('child_process');
|
|
12
|
-
|
|
13
|
-
const TOOLS_PATH = path.join(__dirname, 'ace-tools.js');
|
|
14
|
-
|
|
15
|
-
// Helper to run ace-tools command
|
|
16
|
-
function runAceTools(args, cwd = process.cwd()) {
|
|
17
|
-
try {
|
|
18
|
-
const result = execSync(`node "${TOOLS_PATH}" ${args}`, {
|
|
19
|
-
cwd,
|
|
20
|
-
encoding: 'utf-8',
|
|
21
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
22
|
-
});
|
|
23
|
-
return { success: true, output: result.trim() };
|
|
24
|
-
} catch (err) {
|
|
25
|
-
return {
|
|
26
|
-
success: false,
|
|
27
|
-
output: err.stdout?.toString().trim() || '',
|
|
28
|
-
error: err.stderr?.toString().trim() || err.message,
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Create temp directory structure
|
|
34
|
-
function createTempProject() {
|
|
35
|
-
const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'ace-test-'));
|
|
36
|
-
return tmpDir;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function createTempProjectWithAce() {
|
|
40
|
-
const tmpDir = createTempProject();
|
|
41
|
-
fs.mkdirSync(path.join(tmpDir, '.ace'), { recursive: true });
|
|
42
|
-
return tmpDir;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function cleanup(tmpDir) {
|
|
46
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// ─── generate-slug ────────────────────────────────────────────────────────────
|
|
50
|
-
|
|
51
|
-
describe('generate-slug command', () => {
|
|
52
|
-
test('converts text to lowercase slug', () => {
|
|
53
|
-
const result = runAceTools('generate-slug "Hello World"');
|
|
54
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
55
|
-
const parsed = JSON.parse(result.output);
|
|
56
|
-
assert.strictEqual(parsed.slug, 'hello-world');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test('handles special characters', () => {
|
|
60
|
-
const result = runAceTools('generate-slug "User Authentication & Login!!!"');
|
|
61
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
62
|
-
const parsed = JSON.parse(result.output);
|
|
63
|
-
assert.strictEqual(parsed.slug, 'user-authentication-login');
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test('trims leading and trailing dashes', () => {
|
|
67
|
-
const result = runAceTools('generate-slug "---hello---"');
|
|
68
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
69
|
-
const parsed = JSON.parse(result.output);
|
|
70
|
-
assert.strictEqual(parsed.slug, 'hello');
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test('returns raw slug with --raw flag', () => {
|
|
74
|
-
const result = runAceTools('generate-slug "My Epic Name" --raw');
|
|
75
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
76
|
-
assert.strictEqual(result.output, 'my-epic-name');
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test('errors when no text provided', () => {
|
|
80
|
-
const result = runAceTools('generate-slug');
|
|
81
|
-
assert.strictEqual(result.success, false);
|
|
82
|
-
assert.ok(result.error.includes('text required'), `Expected error about text, got: ${result.error}`);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test('handles multi-word args without quotes', () => {
|
|
86
|
-
const result = runAceTools('generate-slug Platform Foundation Setup');
|
|
87
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
88
|
-
const parsed = JSON.parse(result.output);
|
|
89
|
-
assert.strictEqual(parsed.slug, 'platform-foundation-setup');
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
// ─── current-timestamp ────────────────────────────────────────────────────────
|
|
94
|
-
|
|
95
|
-
describe('current-timestamp command', () => {
|
|
96
|
-
test('returns full ISO timestamp by default', () => {
|
|
97
|
-
const result = runAceTools('current-timestamp');
|
|
98
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
99
|
-
const parsed = JSON.parse(result.output);
|
|
100
|
-
assert.strictEqual(parsed.format, 'full');
|
|
101
|
-
assert.ok(parsed.timestamp.match(/^\d{4}-\d{2}-\d{2}T/), `Expected ISO format, got: ${parsed.timestamp}`);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test('returns date-only with date format', () => {
|
|
105
|
-
const result = runAceTools('current-timestamp date');
|
|
106
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
107
|
-
const parsed = JSON.parse(result.output);
|
|
108
|
-
assert.strictEqual(parsed.format, 'date');
|
|
109
|
-
assert.ok(parsed.timestamp.match(/^\d{4}-\d{2}-\d{2}$/), `Expected date format, got: ${parsed.timestamp}`);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test('returns filename-safe with filename format', () => {
|
|
113
|
-
const result = runAceTools('current-timestamp filename');
|
|
114
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
115
|
-
const parsed = JSON.parse(result.output);
|
|
116
|
-
assert.strictEqual(parsed.format, 'filename');
|
|
117
|
-
assert.ok(!parsed.timestamp.includes(':'), `Filename format should not contain colons: ${parsed.timestamp}`);
|
|
118
|
-
assert.ok(parsed.timestamp.includes('_'), `Filename format should contain underscore separator: ${parsed.timestamp}`);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test('returns raw value with --raw flag', () => {
|
|
122
|
-
const result = runAceTools('current-timestamp date --raw');
|
|
123
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
124
|
-
assert.ok(result.output.match(/^\d{4}-\d{2}-\d{2}$/), `Expected raw date, got: ${result.output}`);
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// ─── resolve-model ────────────────────────────────────────────────────────────
|
|
129
|
-
|
|
130
|
-
describe('resolve-model command', () => {
|
|
131
|
-
let tmpDir;
|
|
132
|
-
|
|
133
|
-
beforeEach(() => {
|
|
134
|
-
tmpDir = createTempProject();
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
afterEach(() => {
|
|
138
|
-
cleanup(tmpDir);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
test('returns quality model for ace-project-researcher', () => {
|
|
142
|
-
const result = runAceTools('resolve-model ace-project-researcher', tmpDir);
|
|
143
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
144
|
-
const parsed = JSON.parse(result.output);
|
|
145
|
-
assert.strictEqual(parsed.model, 'opus');
|
|
146
|
-
assert.strictEqual(parsed.agent, 'ace-project-researcher');
|
|
147
|
-
assert.strictEqual(parsed.profile, 'quality');
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
test('returns quality model for ace-research-synthesizer', () => {
|
|
151
|
-
const result = runAceTools('resolve-model ace-research-synthesizer', tmpDir);
|
|
152
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
153
|
-
const parsed = JSON.parse(result.output);
|
|
154
|
-
assert.strictEqual(parsed.model, 'sonnet');
|
|
155
|
-
assert.strictEqual(parsed.profile, 'quality');
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
test('returns quality model for ace-product-owner', () => {
|
|
159
|
-
const result = runAceTools('resolve-model ace-product-owner', tmpDir);
|
|
160
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
161
|
-
const parsed = JSON.parse(result.output);
|
|
162
|
-
assert.strictEqual(parsed.model, 'opus');
|
|
163
|
-
assert.strictEqual(parsed.profile, 'quality');
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
test('respects budget profile from config', () => {
|
|
167
|
-
fs.mkdirSync(path.join(tmpDir, '.ace'), { recursive: true });
|
|
168
|
-
fs.writeFileSync(path.join(tmpDir, '.ace', 'config.json'), JSON.stringify({
|
|
169
|
-
model_profile: 'budget',
|
|
170
|
-
}));
|
|
171
|
-
|
|
172
|
-
const result = runAceTools('resolve-model ace-product-owner', tmpDir);
|
|
173
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
174
|
-
const parsed = JSON.parse(result.output);
|
|
175
|
-
assert.strictEqual(parsed.model, 'sonnet');
|
|
176
|
-
assert.strictEqual(parsed.profile, 'budget');
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
test('returns sonnet for unknown agent type', () => {
|
|
180
|
-
const result = runAceTools('resolve-model unknown-agent', tmpDir);
|
|
181
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
182
|
-
const parsed = JSON.parse(result.output);
|
|
183
|
-
assert.strictEqual(parsed.model, 'sonnet');
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
test('returns raw model name with --raw flag', () => {
|
|
187
|
-
const result = runAceTools('resolve-model ace-product-owner --raw', tmpDir);
|
|
188
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
189
|
-
assert.strictEqual(result.output, 'opus');
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
test('errors when no agent type provided', () => {
|
|
193
|
-
const result = runAceTools('resolve-model');
|
|
194
|
-
assert.strictEqual(result.success, false);
|
|
195
|
-
assert.ok(result.error.includes('agent-type required'), `Expected error about agent-type, got: ${result.error}`);
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
// ─── verify-path-exists ───────────────────────────────────────────────────────
|
|
200
|
-
|
|
201
|
-
describe('verify-path-exists command', () => {
|
|
202
|
-
let tmpDir;
|
|
203
|
-
|
|
204
|
-
beforeEach(() => {
|
|
205
|
-
tmpDir = createTempProject();
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
afterEach(() => {
|
|
209
|
-
cleanup(tmpDir);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
test('returns true for existing directory', () => {
|
|
213
|
-
fs.mkdirSync(path.join(tmpDir, '.ace'), { recursive: true });
|
|
214
|
-
const result = runAceTools('verify-path-exists .ace', tmpDir);
|
|
215
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
216
|
-
const parsed = JSON.parse(result.output);
|
|
217
|
-
assert.strictEqual(parsed.exists, true);
|
|
218
|
-
assert.strictEqual(parsed.path, '.ace');
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
test('returns false for non-existent path', () => {
|
|
222
|
-
const result = runAceTools('verify-path-exists .ace/config.json', tmpDir);
|
|
223
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
224
|
-
const parsed = JSON.parse(result.output);
|
|
225
|
-
assert.strictEqual(parsed.exists, false);
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
test('returns true for existing file', () => {
|
|
229
|
-
fs.mkdirSync(path.join(tmpDir, '.ace'), { recursive: true });
|
|
230
|
-
fs.writeFileSync(path.join(tmpDir, '.ace', 'config.json'), '{}');
|
|
231
|
-
const result = runAceTools('verify-path-exists .ace/config.json', tmpDir);
|
|
232
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
233
|
-
const parsed = JSON.parse(result.output);
|
|
234
|
-
assert.strictEqual(parsed.exists, true);
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
test('returns raw true/false with --raw flag', () => {
|
|
238
|
-
const result = runAceTools('verify-path-exists nonexistent --raw', tmpDir);
|
|
239
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
240
|
-
assert.strictEqual(result.output, 'false');
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
test('errors when no path provided', () => {
|
|
244
|
-
const result = runAceTools('verify-path-exists', tmpDir);
|
|
245
|
-
assert.strictEqual(result.success, false);
|
|
246
|
-
assert.ok(result.error.includes('path required'), `Expected error about path, got: ${result.error}`);
|
|
247
|
-
});
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
// ─── load-config ──────────────────────────────────────────────────────────────
|
|
251
|
-
|
|
252
|
-
describe('load-config command', () => {
|
|
253
|
-
let tmpDir;
|
|
254
|
-
|
|
255
|
-
beforeEach(() => {
|
|
256
|
-
tmpDir = createTempProject();
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
afterEach(() => {
|
|
260
|
-
cleanup(tmpDir);
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
test('returns defaults when no config file exists', () => {
|
|
264
|
-
const result = runAceTools('load-config', tmpDir);
|
|
265
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
266
|
-
const config = JSON.parse(result.output);
|
|
267
|
-
assert.strictEqual(config.version, '0.1.0');
|
|
268
|
-
assert.strictEqual(config.projectName, '');
|
|
269
|
-
assert.strictEqual(config.storage, 'local');
|
|
270
|
-
assert.strictEqual(config.commit_docs, true);
|
|
271
|
-
assert.strictEqual(config.github.enabled, false);
|
|
272
|
-
assert.strictEqual(config.github.repo, null);
|
|
273
|
-
assert.strictEqual(config.github.labels.epic, 'ace:epic');
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
test('reads existing config and merges with defaults', () => {
|
|
277
|
-
fs.mkdirSync(path.join(tmpDir, '.ace'), { recursive: true });
|
|
278
|
-
fs.writeFileSync(path.join(tmpDir, '.ace', 'config.json'), JSON.stringify({
|
|
279
|
-
projectName: 'Test Project',
|
|
280
|
-
storage: 'github',
|
|
281
|
-
github: { enabled: true, repo: 'owner/repo' },
|
|
282
|
-
}));
|
|
283
|
-
|
|
284
|
-
const result = runAceTools('load-config', tmpDir);
|
|
285
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
286
|
-
const config = JSON.parse(result.output);
|
|
287
|
-
assert.strictEqual(config.projectName, 'Test Project');
|
|
288
|
-
assert.strictEqual(config.storage, 'github');
|
|
289
|
-
assert.strictEqual(config.github.enabled, true);
|
|
290
|
-
assert.strictEqual(config.github.repo, 'owner/repo');
|
|
291
|
-
// Defaults still applied for unset fields
|
|
292
|
-
assert.strictEqual(config.version, '0.1.0');
|
|
293
|
-
assert.strictEqual(config.github.labels.epic, 'ace:epic');
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
test('handles malformed JSON gracefully', () => {
|
|
297
|
-
fs.mkdirSync(path.join(tmpDir, '.ace'), { recursive: true });
|
|
298
|
-
fs.writeFileSync(path.join(tmpDir, '.ace', 'config.json'), 'not json');
|
|
299
|
-
|
|
300
|
-
const result = runAceTools('load-config', tmpDir);
|
|
301
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
302
|
-
const config = JSON.parse(result.output);
|
|
303
|
-
// Should return defaults
|
|
304
|
-
assert.strictEqual(config.version, '0.1.0');
|
|
305
|
-
assert.strictEqual(config.projectName, '');
|
|
306
|
-
});
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
// ─── init new-project ─────────────────────────────────────────────────────────
|
|
310
|
-
|
|
311
|
-
describe('init new-project command', () => {
|
|
312
|
-
let tmpDir;
|
|
313
|
-
|
|
314
|
-
beforeEach(() => {
|
|
315
|
-
tmpDir = createTempProject();
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
afterEach(() => {
|
|
319
|
-
cleanup(tmpDir);
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
test('detects empty project (greenfield)', () => {
|
|
323
|
-
const result = runAceTools('init new-project', tmpDir);
|
|
324
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
325
|
-
const data = JSON.parse(result.output);
|
|
326
|
-
|
|
327
|
-
assert.strictEqual(data.project_exists, false);
|
|
328
|
-
assert.strictEqual(data.has_codebase_map, false);
|
|
329
|
-
assert.strictEqual(data.planning_exists, false);
|
|
330
|
-
assert.strictEqual(data.is_brownfield, false);
|
|
331
|
-
assert.strictEqual(data.has_existing_code, false);
|
|
332
|
-
assert.strictEqual(data.has_package_file, false);
|
|
333
|
-
assert.strictEqual(data.needs_codebase_map, false);
|
|
334
|
-
assert.strictEqual(data.has_git, false);
|
|
335
|
-
assert.strictEqual(data.commit_docs, true);
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
test('detects existing code files (brownfield)', () => {
|
|
339
|
-
fs.writeFileSync(path.join(tmpDir, 'index.js'), 'console.log("hello");');
|
|
340
|
-
fs.writeFileSync(path.join(tmpDir, 'package.json'), '{}');
|
|
341
|
-
|
|
342
|
-
const result = runAceTools('init new-project', tmpDir);
|
|
343
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
344
|
-
const data = JSON.parse(result.output);
|
|
345
|
-
|
|
346
|
-
assert.strictEqual(data.has_existing_code, true);
|
|
347
|
-
assert.strictEqual(data.has_package_file, true);
|
|
348
|
-
assert.strictEqual(data.is_brownfield, true);
|
|
349
|
-
assert.strictEqual(data.needs_codebase_map, true);
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
test('detects package file without code files', () => {
|
|
353
|
-
fs.writeFileSync(path.join(tmpDir, 'package.json'), '{}');
|
|
354
|
-
|
|
355
|
-
const result = runAceTools('init new-project', tmpDir);
|
|
356
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
357
|
-
const data = JSON.parse(result.output);
|
|
358
|
-
|
|
359
|
-
assert.strictEqual(data.has_existing_code, false);
|
|
360
|
-
assert.strictEqual(data.has_package_file, true);
|
|
361
|
-
assert.strictEqual(data.is_brownfield, true);
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
test('detects nested code files up to depth 3', () => {
|
|
365
|
-
const nested = path.join(tmpDir, 'src', 'lib', 'utils');
|
|
366
|
-
fs.mkdirSync(nested, { recursive: true });
|
|
367
|
-
fs.writeFileSync(path.join(nested, 'helper.ts'), 'export const x = 1;');
|
|
368
|
-
|
|
369
|
-
const result = runAceTools('init new-project', tmpDir);
|
|
370
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
371
|
-
const data = JSON.parse(result.output);
|
|
372
|
-
|
|
373
|
-
assert.strictEqual(data.has_existing_code, true);
|
|
374
|
-
assert.strictEqual(data.needs_codebase_map, true);
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
test('ignores node_modules directory', () => {
|
|
378
|
-
fs.mkdirSync(path.join(tmpDir, 'node_modules', 'pkg'), { recursive: true });
|
|
379
|
-
fs.writeFileSync(path.join(tmpDir, 'node_modules', 'pkg', 'index.js'), 'module.exports = {};');
|
|
380
|
-
|
|
381
|
-
const result = runAceTools('init new-project', tmpDir);
|
|
382
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
383
|
-
const data = JSON.parse(result.output);
|
|
384
|
-
|
|
385
|
-
assert.strictEqual(data.has_existing_code, false);
|
|
386
|
-
assert.strictEqual(data.needs_codebase_map, false);
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
test('detects ACE already initialized', () => {
|
|
390
|
-
fs.mkdirSync(path.join(tmpDir, '.docs', 'product'), { recursive: true });
|
|
391
|
-
fs.writeFileSync(path.join(tmpDir, '.docs', 'product', 'product-vision.md'), '# My Product');
|
|
392
|
-
|
|
393
|
-
const result = runAceTools('init new-project', tmpDir);
|
|
394
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
395
|
-
const data = JSON.parse(result.output);
|
|
396
|
-
|
|
397
|
-
assert.strictEqual(data.project_exists, true);
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
test('detects git repository', () => {
|
|
401
|
-
fs.mkdirSync(path.join(tmpDir, '.git'), { recursive: true });
|
|
402
|
-
|
|
403
|
-
const result = runAceTools('init new-project', tmpDir);
|
|
404
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
405
|
-
const data = JSON.parse(result.output);
|
|
406
|
-
|
|
407
|
-
assert.strictEqual(data.has_git, true);
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
test('commit_docs defaults to true', () => {
|
|
411
|
-
const result = runAceTools('init new-project', tmpDir);
|
|
412
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
413
|
-
const data = JSON.parse(result.output);
|
|
414
|
-
|
|
415
|
-
assert.strictEqual(data.commit_docs, true);
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
test('includes pre-resolved models for init agents', () => {
|
|
419
|
-
const result = runAceTools('init new-project', tmpDir);
|
|
420
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
421
|
-
const data = JSON.parse(result.output);
|
|
422
|
-
|
|
423
|
-
assert.strictEqual(data.product_owner_model, 'opus');
|
|
424
|
-
assert.strictEqual(data.researcher_model, 'opus');
|
|
425
|
-
assert.strictEqual(data.synthesizer_model, 'sonnet');
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
test('models respect config profile', () => {
|
|
429
|
-
fs.mkdirSync(path.join(tmpDir, '.ace'), { recursive: true });
|
|
430
|
-
fs.writeFileSync(path.join(tmpDir, '.ace', 'config.json'), JSON.stringify({
|
|
431
|
-
model_profile: 'budget',
|
|
432
|
-
}));
|
|
433
|
-
|
|
434
|
-
const result = runAceTools('init new-project', tmpDir);
|
|
435
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
436
|
-
const data = JSON.parse(result.output);
|
|
437
|
-
|
|
438
|
-
assert.strictEqual(data.product_owner_model, 'sonnet');
|
|
439
|
-
assert.strictEqual(data.researcher_model, 'haiku');
|
|
440
|
-
assert.strictEqual(data.synthesizer_model, 'haiku');
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
test('detects Python project files', () => {
|
|
444
|
-
fs.writeFileSync(path.join(tmpDir, 'requirements.txt'), 'flask==2.0');
|
|
445
|
-
fs.writeFileSync(path.join(tmpDir, 'app.py'), 'from flask import Flask');
|
|
446
|
-
|
|
447
|
-
const result = runAceTools('init new-project', tmpDir);
|
|
448
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
449
|
-
const data = JSON.parse(result.output);
|
|
450
|
-
|
|
451
|
-
assert.strictEqual(data.has_existing_code, true);
|
|
452
|
-
assert.strictEqual(data.has_package_file, true);
|
|
453
|
-
assert.strictEqual(data.is_brownfield, true);
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
test('detects Go project files', () => {
|
|
457
|
-
fs.writeFileSync(path.join(tmpDir, 'go.mod'), 'module example.com/foo');
|
|
458
|
-
fs.writeFileSync(path.join(tmpDir, 'main.go'), 'package main');
|
|
459
|
-
|
|
460
|
-
const result = runAceTools('init new-project', tmpDir);
|
|
461
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
462
|
-
const data = JSON.parse(result.output);
|
|
463
|
-
|
|
464
|
-
assert.strictEqual(data.has_existing_code, true);
|
|
465
|
-
assert.strictEqual(data.has_package_file, true);
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
test('detects Rust project files', () => {
|
|
469
|
-
fs.writeFileSync(path.join(tmpDir, 'Cargo.toml'), '[package]');
|
|
470
|
-
fs.mkdirSync(path.join(tmpDir, 'src'), { recursive: true });
|
|
471
|
-
fs.writeFileSync(path.join(tmpDir, 'src', 'main.rs'), 'fn main() {}');
|
|
472
|
-
|
|
473
|
-
const result = runAceTools('init new-project', tmpDir);
|
|
474
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
475
|
-
const data = JSON.parse(result.output);
|
|
476
|
-
|
|
477
|
-
assert.strictEqual(data.has_existing_code, true);
|
|
478
|
-
assert.strictEqual(data.has_package_file, true);
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
test('needs_codebase_map is false when codebase dir exists', () => {
|
|
482
|
-
fs.mkdirSync(path.join(tmpDir, '.ace', 'codebase'), { recursive: true });
|
|
483
|
-
fs.writeFileSync(path.join(tmpDir, 'index.js'), 'console.log("hello");');
|
|
484
|
-
|
|
485
|
-
const result = runAceTools('init new-project', tmpDir);
|
|
486
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
487
|
-
const data = JSON.parse(result.output);
|
|
488
|
-
|
|
489
|
-
assert.strictEqual(data.has_existing_code, true);
|
|
490
|
-
assert.strictEqual(data.has_codebase_map, true);
|
|
491
|
-
assert.strictEqual(data.needs_codebase_map, false);
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
test('commit_docs respects config override', () => {
|
|
495
|
-
fs.mkdirSync(path.join(tmpDir, '.ace'), { recursive: true });
|
|
496
|
-
fs.writeFileSync(path.join(tmpDir, '.ace', 'config.json'), JSON.stringify({
|
|
497
|
-
commit_docs: false,
|
|
498
|
-
}));
|
|
499
|
-
|
|
500
|
-
const result = runAceTools('init new-project', tmpDir);
|
|
501
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
502
|
-
const data = JSON.parse(result.output);
|
|
503
|
-
|
|
504
|
-
assert.strictEqual(data.commit_docs, false);
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
test('detects C# project files', () => {
|
|
508
|
-
fs.writeFileSync(path.join(tmpDir, 'App.cs'), 'namespace App {}');
|
|
509
|
-
|
|
510
|
-
const result = runAceTools('init new-project', tmpDir);
|
|
511
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
512
|
-
const data = JSON.parse(result.output);
|
|
513
|
-
|
|
514
|
-
assert.strictEqual(data.has_existing_code, true);
|
|
515
|
-
assert.strictEqual(data.needs_codebase_map, true);
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
test('detects .csproj as package file', () => {
|
|
519
|
-
fs.writeFileSync(path.join(tmpDir, 'MyApp.csproj'), '<Project Sdk="Microsoft.NET.Sdk" />');
|
|
520
|
-
|
|
521
|
-
const result = runAceTools('init new-project', tmpDir);
|
|
522
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
523
|
-
const data = JSON.parse(result.output);
|
|
524
|
-
|
|
525
|
-
assert.strictEqual(data.has_package_file, true);
|
|
526
|
-
assert.strictEqual(data.is_brownfield, true);
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
test('detects .sln as package file', () => {
|
|
530
|
-
fs.writeFileSync(path.join(tmpDir, 'MyApp.sln'), 'Microsoft Visual Studio Solution File');
|
|
531
|
-
|
|
532
|
-
const result = runAceTools('init new-project', tmpDir);
|
|
533
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
534
|
-
const data = JSON.parse(result.output);
|
|
535
|
-
|
|
536
|
-
assert.strictEqual(data.has_package_file, true);
|
|
537
|
-
assert.strictEqual(data.is_brownfield, true);
|
|
538
|
-
});
|
|
539
|
-
|
|
540
|
-
test('detects Java project with build.gradle', () => {
|
|
541
|
-
fs.writeFileSync(path.join(tmpDir, 'build.gradle'), 'plugins {}');
|
|
542
|
-
|
|
543
|
-
const result = runAceTools('init new-project', tmpDir);
|
|
544
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
545
|
-
const data = JSON.parse(result.output);
|
|
546
|
-
|
|
547
|
-
assert.strictEqual(data.has_package_file, true);
|
|
548
|
-
});
|
|
549
|
-
|
|
550
|
-
test('has_gh_cli is boolean', () => {
|
|
551
|
-
const result = runAceTools('init new-project', tmpDir);
|
|
552
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
553
|
-
const data = JSON.parse(result.output);
|
|
554
|
-
|
|
555
|
-
assert.strictEqual(typeof data.has_gh_cli, 'boolean');
|
|
556
|
-
});
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
// ─── ensure-settings ──────────────────────────────────────────────────────────
|
|
560
|
-
|
|
561
|
-
describe('ensure-settings command', () => {
|
|
562
|
-
let tmpDir;
|
|
563
|
-
|
|
564
|
-
beforeEach(() => {
|
|
565
|
-
tmpDir = createTempProject();
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
afterEach(() => {
|
|
569
|
-
cleanup(tmpDir);
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
test('creates settings.json with defaults when missing', () => {
|
|
573
|
-
const result = runAceTools('ensure-settings', tmpDir);
|
|
574
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
575
|
-
const data = JSON.parse(result.output);
|
|
576
|
-
|
|
577
|
-
assert.strictEqual(data.created, true);
|
|
578
|
-
assert.strictEqual(data.settings.model_profile, 'balanced');
|
|
579
|
-
assert.strictEqual(data.settings.commit_docs, true);
|
|
580
|
-
assert.strictEqual(data.settings.github_project.enabled, false);
|
|
581
|
-
assert.strictEqual(data.settings.github_project.gh_installed, false);
|
|
582
|
-
assert.strictEqual(data.settings.github_project.repo, '');
|
|
583
|
-
assert.strictEqual(data.settings.github_project.project_number, null);
|
|
584
|
-
assert.strictEqual(data.settings.github_project.owner, '');
|
|
585
|
-
|
|
586
|
-
// Verify file was actually created
|
|
587
|
-
const settingsPath = path.join(tmpDir, '.ace', 'settings.json');
|
|
588
|
-
assert.ok(fs.existsSync(settingsPath), 'settings.json should exist on disk');
|
|
589
|
-
const onDisk = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
590
|
-
assert.strictEqual(onDisk.model_profile, 'balanced');
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
test('creates .ace directory if it does not exist', () => {
|
|
594
|
-
const aceDir = path.join(tmpDir, '.ace');
|
|
595
|
-
assert.ok(!fs.existsSync(aceDir), '.ace dir should not exist yet');
|
|
596
|
-
|
|
597
|
-
const result = runAceTools('ensure-settings', tmpDir);
|
|
598
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
599
|
-
const data = JSON.parse(result.output);
|
|
600
|
-
|
|
601
|
-
assert.strictEqual(data.created, true);
|
|
602
|
-
assert.ok(fs.existsSync(aceDir), '.ace dir should be created');
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
test('does not overwrite existing settings.json', () => {
|
|
606
|
-
fs.mkdirSync(path.join(tmpDir, '.ace'), { recursive: true });
|
|
607
|
-
const customSettings = {
|
|
608
|
-
model_profile: 'quality',
|
|
609
|
-
commit_docs: false,
|
|
610
|
-
github_project: {
|
|
611
|
-
enabled: true,
|
|
612
|
-
gh_installed: true,
|
|
613
|
-
repo: 'owner/repo',
|
|
614
|
-
project_number: 5,
|
|
615
|
-
owner: 'owner',
|
|
616
|
-
},
|
|
617
|
-
};
|
|
618
|
-
fs.writeFileSync(path.join(tmpDir, '.ace', 'settings.json'), JSON.stringify(customSettings, null, 2));
|
|
619
|
-
|
|
620
|
-
const result = runAceTools('ensure-settings', tmpDir);
|
|
621
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
622
|
-
const data = JSON.parse(result.output);
|
|
623
|
-
|
|
624
|
-
assert.strictEqual(data.created, false);
|
|
625
|
-
assert.strictEqual(data.settings.model_profile, 'quality');
|
|
626
|
-
assert.strictEqual(data.settings.commit_docs, false);
|
|
627
|
-
assert.strictEqual(data.settings.github_project.enabled, true);
|
|
628
|
-
assert.strictEqual(data.settings.github_project.project_number, 5);
|
|
629
|
-
});
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
// ─── init setup-github ────────────────────────────────────────────────────────
|
|
633
|
-
|
|
634
|
-
describe('init setup-github command', () => {
|
|
635
|
-
let tmpDir;
|
|
636
|
-
|
|
637
|
-
beforeEach(() => {
|
|
638
|
-
tmpDir = createTempProject();
|
|
639
|
-
});
|
|
640
|
-
|
|
641
|
-
afterEach(() => {
|
|
642
|
-
cleanup(tmpDir);
|
|
643
|
-
});
|
|
644
|
-
|
|
645
|
-
test('returns gh_installed as boolean', () => {
|
|
646
|
-
const result = runAceTools('init setup-github', tmpDir);
|
|
647
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
648
|
-
const data = JSON.parse(result.output);
|
|
649
|
-
|
|
650
|
-
assert.strictEqual(typeof data.gh_installed, 'boolean');
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
test('returns projects as array', () => {
|
|
654
|
-
const result = runAceTools('init setup-github', tmpDir);
|
|
655
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
656
|
-
const data = JSON.parse(result.output);
|
|
657
|
-
|
|
658
|
-
assert.ok(Array.isArray(data.projects), 'projects should be an array');
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
test('returns current_settings object', () => {
|
|
662
|
-
const result = runAceTools('init setup-github', tmpDir);
|
|
663
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
664
|
-
const data = JSON.parse(result.output);
|
|
665
|
-
|
|
666
|
-
assert.ok(data.current_settings !== undefined, 'current_settings should be present');
|
|
667
|
-
assert.strictEqual(typeof data.current_settings.enabled, 'boolean');
|
|
668
|
-
});
|
|
669
|
-
|
|
670
|
-
test('returns repo and owner as strings', () => {
|
|
671
|
-
const result = runAceTools('init setup-github', tmpDir);
|
|
672
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
673
|
-
const data = JSON.parse(result.output);
|
|
674
|
-
|
|
675
|
-
assert.strictEqual(typeof data.repo, 'string');
|
|
676
|
-
assert.strictEqual(typeof data.owner, 'string');
|
|
677
|
-
});
|
|
678
|
-
});
|
|
679
|
-
|
|
680
|
-
// ─── write-github-settings ────────────────────────────────────────────────────
|
|
681
|
-
|
|
682
|
-
describe('write-github-settings command', () => {
|
|
683
|
-
let tmpDir;
|
|
684
|
-
|
|
685
|
-
beforeEach(() => {
|
|
686
|
-
tmpDir = createTempProjectWithAce();
|
|
687
|
-
// Seed a settings.json with defaults
|
|
688
|
-
const defaults = {
|
|
689
|
-
model_profile: 'balanced',
|
|
690
|
-
commit_docs: true,
|
|
691
|
-
github_project: {
|
|
692
|
-
enabled: false,
|
|
693
|
-
gh_installed: false,
|
|
694
|
-
repo: '',
|
|
695
|
-
project_number: null,
|
|
696
|
-
owner: '',
|
|
697
|
-
},
|
|
698
|
-
};
|
|
699
|
-
fs.writeFileSync(path.join(tmpDir, '.ace', 'settings.json'), JSON.stringify(defaults, null, 2));
|
|
700
|
-
});
|
|
701
|
-
|
|
702
|
-
afterEach(() => {
|
|
703
|
-
cleanup(tmpDir);
|
|
704
|
-
});
|
|
705
|
-
|
|
706
|
-
test('writes key=value pairs to settings.json', () => {
|
|
707
|
-
const result = runAceTools('write-github-settings enabled=true repo=owner/repo project_number=3 owner=owner gh_installed=true', tmpDir);
|
|
708
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
709
|
-
const data = JSON.parse(result.output);
|
|
710
|
-
|
|
711
|
-
assert.strictEqual(data.written, true);
|
|
712
|
-
assert.strictEqual(data.settings.github_project.enabled, true);
|
|
713
|
-
assert.strictEqual(data.settings.github_project.repo, 'owner/repo');
|
|
714
|
-
assert.strictEqual(data.settings.github_project.project_number, 3);
|
|
715
|
-
assert.strictEqual(data.settings.github_project.owner, 'owner');
|
|
716
|
-
assert.strictEqual(data.settings.github_project.gh_installed, true);
|
|
717
|
-
|
|
718
|
-
// Verify persisted to disk
|
|
719
|
-
const onDisk = JSON.parse(fs.readFileSync(path.join(tmpDir, '.ace', 'settings.json'), 'utf-8'));
|
|
720
|
-
assert.strictEqual(onDisk.github_project.enabled, true);
|
|
721
|
-
assert.strictEqual(onDisk.github_project.project_number, 3);
|
|
722
|
-
});
|
|
723
|
-
|
|
724
|
-
test('preserves non-github settings when writing', () => {
|
|
725
|
-
const result = runAceTools('write-github-settings enabled=true', tmpDir);
|
|
726
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
727
|
-
const data = JSON.parse(result.output);
|
|
728
|
-
|
|
729
|
-
assert.strictEqual(data.settings.model_profile, 'balanced');
|
|
730
|
-
assert.strictEqual(data.settings.commit_docs, true);
|
|
731
|
-
});
|
|
732
|
-
|
|
733
|
-
test('handles project_number=null', () => {
|
|
734
|
-
// First set a number
|
|
735
|
-
runAceTools('write-github-settings project_number=5', tmpDir);
|
|
736
|
-
// Then reset to null
|
|
737
|
-
const result = runAceTools('write-github-settings project_number=null', tmpDir);
|
|
738
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
739
|
-
const data = JSON.parse(result.output);
|
|
740
|
-
|
|
741
|
-
assert.strictEqual(data.settings.github_project.project_number, null);
|
|
742
|
-
});
|
|
743
|
-
});
|
|
744
|
-
|
|
745
|
-
// ─── init research-story ─────────────────────────────────────────────────────
|
|
746
|
-
|
|
747
|
-
const SAMPLE_STORY = `# S3: Display OAuth Provider Buttons
|
|
748
|
-
|
|
749
|
-
**Feature**: F3 OAuth2 Login Flow | **Epic**: #45 User Authentication
|
|
750
|
-
**Status**: Refined | **Size**: 3 | **Sprint**: Sprint 2 | **Link**: [#95](https://github.com/owner/repo/issues/95)
|
|
751
|
-
|
|
752
|
-
## User Story
|
|
753
|
-
|
|
754
|
-
> As a returning customer,
|
|
755
|
-
> I want to click a Google or GitHub login button,
|
|
756
|
-
> so that I can authenticate without remembering a site-specific password.
|
|
757
|
-
|
|
758
|
-
## Description
|
|
759
|
-
|
|
760
|
-
This story adds OAuth provider buttons to the login page. It builds on the
|
|
761
|
-
auth service foundation (S1) and enables the token exchange flow (S4).
|
|
762
|
-
|
|
763
|
-
## Acceptance Criteria
|
|
764
|
-
|
|
765
|
-
### Scenario: Successful Google login
|
|
766
|
-
|
|
767
|
-
**Given** the user is on the login page and has a valid Google account
|
|
768
|
-
**When** they click the "Sign in with Google" button and complete Google's OAuth flow
|
|
769
|
-
**Then** they are redirected to the dashboard and see their Google profile name
|
|
770
|
-
|
|
771
|
-
### Scenario: Provider unavailable
|
|
772
|
-
|
|
773
|
-
**Given** the user is on the login page and the Google OAuth service is unreachable
|
|
774
|
-
**When** they click the "Sign in with Google" button
|
|
775
|
-
**Then** they see an error message "Login service temporarily unavailable. Please try again."
|
|
776
|
-
|
|
777
|
-
### Scenario: GitHub login button displayed
|
|
778
|
-
|
|
779
|
-
**Given** the user navigates to the login page
|
|
780
|
-
**When** the page loads
|
|
781
|
-
**Then** they see a "Sign in with GitHub" button alongside the Google button
|
|
782
|
-
|
|
783
|
-
## Out of Scope
|
|
784
|
-
|
|
785
|
-
- Token refresh logic (handled by S4)
|
|
786
|
-
- Account linking (future feature)
|
|
787
|
-
|
|
788
|
-
## Dependencies
|
|
789
|
-
|
|
790
|
-
### Blocked By
|
|
791
|
-
- S1 Auth service foundation
|
|
792
|
-
|
|
793
|
-
### Blocks
|
|
794
|
-
- S4 Token exchange flow
|
|
795
|
-
|
|
796
|
-
### External
|
|
797
|
-
- Google OAuth API — available
|
|
798
|
-
|
|
799
|
-
## Definition of Done
|
|
800
|
-
|
|
801
|
-
- [ ] All acceptance criteria scenarios pass
|
|
802
|
-
- [ ] Code reviewed and approved
|
|
803
|
-
- [ ] Tests written and passing
|
|
804
|
-
- [ ] CI pipeline green
|
|
805
|
-
- [ ] Documentation updated (if applicable)
|
|
806
|
-
- [ ] Product Owner verified
|
|
807
|
-
|
|
808
|
-
## Relevant Wiki
|
|
809
|
-
|
|
810
|
-
### System-Wide
|
|
811
|
-
|
|
812
|
-
- \`.docs/wiki/system-wide/system-structure.md\` — Mandatory system-wide context
|
|
813
|
-
- \`.docs/wiki/system-wide/system-architecture.md\` — Mandatory system-wide context
|
|
814
|
-
- \`.docs/wiki/system-wide/coding-standards.md\` — Mandatory system-wide context
|
|
815
|
-
- \`.docs/wiki/system-wide/testing-framework.md\` — Mandatory system-wide context
|
|
816
|
-
|
|
817
|
-
### Systems
|
|
818
|
-
- \`.docs/wiki/subsystems/auth/systems/oauth-provider.md\` — Implements the provider abstraction this story extends
|
|
819
|
-
|
|
820
|
-
### Patterns
|
|
821
|
-
- \`.docs/wiki/subsystems/auth/patterns/strategy-pattern.md\` — Each OAuth provider is a strategy; new provider must follow this
|
|
822
|
-
|
|
823
|
-
### Decisions
|
|
824
|
-
- \`.docs/wiki/subsystems/auth/decisions/adr-003-jwt-over-sessions.md\` — Constrains token format to JWT
|
|
825
|
-
`;
|
|
826
|
-
|
|
827
|
-
describe('init research-story command', () => {
|
|
828
|
-
let tmpDir;
|
|
829
|
-
|
|
830
|
-
beforeEach(() => {
|
|
831
|
-
tmpDir = createTempProject();
|
|
832
|
-
fs.mkdirSync(path.join(tmpDir, '.ace'), { recursive: true });
|
|
833
|
-
});
|
|
834
|
-
|
|
835
|
-
afterEach(() => {
|
|
836
|
-
cleanup(tmpDir);
|
|
837
|
-
});
|
|
838
|
-
|
|
839
|
-
test('parses story from file and extracts metadata', () => {
|
|
840
|
-
// Create story directory structure
|
|
841
|
-
const storyDir = path.join(tmpDir, '.ace', 'artifacts', 'product', '45-user-authentication', 'f3-oauth2-login-flow', 's3-display-oauth-provider-buttons');
|
|
842
|
-
fs.mkdirSync(storyDir, { recursive: true });
|
|
843
|
-
const storyFile = path.join(storyDir, 's3-display-oauth-provider-buttons.md');
|
|
844
|
-
fs.writeFileSync(storyFile, SAMPLE_STORY);
|
|
845
|
-
|
|
846
|
-
const relPath = path.relative(tmpDir, storyFile).replace(/\\/g, '/');
|
|
847
|
-
const result = runAceTools(`init research-story ${relPath}`, tmpDir);
|
|
848
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
849
|
-
const data = JSON.parse(result.output);
|
|
850
|
-
|
|
851
|
-
assert.strictEqual(data.story_valid, true);
|
|
852
|
-
assert.strictEqual(data.story_error, null);
|
|
853
|
-
assert.strictEqual(data.story_source, 'file');
|
|
854
|
-
assert.strictEqual(data.story.id, 'S3');
|
|
855
|
-
assert.strictEqual(data.story.title, 'Display OAuth Provider Buttons');
|
|
856
|
-
assert.strictEqual(data.story.status, 'Refined');
|
|
857
|
-
assert.strictEqual(data.story.size, '3');
|
|
858
|
-
assert.strictEqual(data.feature.id, 'F3');
|
|
859
|
-
assert.strictEqual(data.feature.title, 'OAuth2 Login Flow');
|
|
860
|
-
assert.strictEqual(data.epic.id, '#45');
|
|
861
|
-
assert.strictEqual(data.epic.title, 'User Authentication');
|
|
862
|
-
});
|
|
863
|
-
|
|
864
|
-
test('extracts user story and description', () => {
|
|
865
|
-
const storyDir = path.join(tmpDir, '.ace', 'artifacts', 'product', 'epic', 'feat', 'story');
|
|
866
|
-
fs.mkdirSync(storyDir, { recursive: true });
|
|
867
|
-
const storyFile = path.join(storyDir, 'story.md');
|
|
868
|
-
fs.writeFileSync(storyFile, SAMPLE_STORY);
|
|
869
|
-
|
|
870
|
-
const relPath = path.relative(tmpDir, storyFile).replace(/\\/g, '/');
|
|
871
|
-
const result = runAceTools(`init research-story ${relPath}`, tmpDir);
|
|
872
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
873
|
-
const data = JSON.parse(result.output);
|
|
874
|
-
|
|
875
|
-
assert.ok(data.user_story.includes('As a returning customer'), `user_story should contain persona: ${data.user_story}`);
|
|
876
|
-
assert.ok(data.user_story.includes('authenticate without remembering'), `user_story should contain benefit`);
|
|
877
|
-
assert.ok(data.description.includes('OAuth provider buttons'), `description should contain story details: ${data.description}`);
|
|
878
|
-
assert.strictEqual(data.acceptance_criteria_count, 3);
|
|
879
|
-
});
|
|
880
|
-
|
|
881
|
-
test('extracts wiki references with categories', () => {
|
|
882
|
-
const storyDir = path.join(tmpDir, '.ace', 'artifacts', 'product', 'epic', 'feat', 'story');
|
|
883
|
-
fs.mkdirSync(storyDir, { recursive: true });
|
|
884
|
-
const storyFile = path.join(storyDir, 'story.md');
|
|
885
|
-
fs.writeFileSync(storyFile, SAMPLE_STORY);
|
|
886
|
-
|
|
887
|
-
const relPath = path.relative(tmpDir, storyFile).replace(/\\/g, '/');
|
|
888
|
-
const result = runAceTools(`init research-story ${relPath}`, tmpDir);
|
|
889
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
890
|
-
const data = JSON.parse(result.output);
|
|
891
|
-
|
|
892
|
-
assert.strictEqual(data.wiki_references.system_wide.length, 4);
|
|
893
|
-
assert.ok(data.wiki_references.system_wide.includes('.docs/wiki/system-wide/system-structure.md'));
|
|
894
|
-
assert.ok(data.wiki_references.system_wide.includes('.docs/wiki/system-wide/coding-standards.md'));
|
|
895
|
-
|
|
896
|
-
assert.strictEqual(data.wiki_references.subsystem_docs.length, 3);
|
|
897
|
-
const oauthDoc = data.wiki_references.subsystem_docs.find(d => d.path.includes('oauth-provider'));
|
|
898
|
-
assert.ok(oauthDoc, 'Should find oauth-provider doc');
|
|
899
|
-
assert.strictEqual(oauthDoc.category, 'systems');
|
|
900
|
-
|
|
901
|
-
const strategyDoc = data.wiki_references.subsystem_docs.find(d => d.path.includes('strategy-pattern'));
|
|
902
|
-
assert.ok(strategyDoc, 'Should find strategy-pattern doc');
|
|
903
|
-
assert.strictEqual(strategyDoc.category, 'patterns');
|
|
904
|
-
|
|
905
|
-
const adrDoc = data.wiki_references.subsystem_docs.find(d => d.path.includes('adr-003'));
|
|
906
|
-
assert.ok(adrDoc, 'Should find ADR doc');
|
|
907
|
-
assert.strictEqual(adrDoc.category, 'decisions');
|
|
908
|
-
|
|
909
|
-
assert.strictEqual(data.wiki_references.total_count, 7);
|
|
910
|
-
});
|
|
911
|
-
|
|
912
|
-
test('computes paths from file location', () => {
|
|
913
|
-
const storyDir = path.join(tmpDir, '.ace', 'artifacts', 'product', 'e1-auth', 'f3-oauth', 's3-buttons');
|
|
914
|
-
fs.mkdirSync(storyDir, { recursive: true });
|
|
915
|
-
const storyFile = path.join(storyDir, 's3-buttons.md');
|
|
916
|
-
fs.writeFileSync(storyFile, SAMPLE_STORY);
|
|
917
|
-
|
|
918
|
-
const relPath = path.relative(tmpDir, storyFile).replace(/\\/g, '/');
|
|
919
|
-
const result = runAceTools(`init research-story ${relPath}`, tmpDir);
|
|
920
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
921
|
-
const data = JSON.parse(result.output);
|
|
922
|
-
|
|
923
|
-
assert.ok(data.paths, 'paths should be present');
|
|
924
|
-
assert.ok(data.paths.story_dir.includes('s3-buttons'), `story_dir should contain story slug: ${data.paths.story_dir}`);
|
|
925
|
-
assert.ok(data.paths.external_analysis_file.endsWith('external-analysis.md'), `external_analysis_file: ${data.paths.external_analysis_file}`);
|
|
926
|
-
assert.ok(data.paths.integration_analysis_file.endsWith('integration-analysis.md'));
|
|
927
|
-
assert.ok(data.paths.feature_file.endsWith('f3-oauth.md'), `feature_file: ${data.paths.feature_file}`);
|
|
928
|
-
});
|
|
929
|
-
|
|
930
|
-
test('detects existing artifacts', () => {
|
|
931
|
-
const storyDir = path.join(tmpDir, '.ace', 'artifacts', 'product', 'epic', 'feat', 'story');
|
|
932
|
-
fs.mkdirSync(storyDir, { recursive: true });
|
|
933
|
-
const storyFile = path.join(storyDir, 'story.md');
|
|
934
|
-
fs.writeFileSync(storyFile, SAMPLE_STORY);
|
|
935
|
-
// Create external analysis
|
|
936
|
-
fs.writeFileSync(path.join(storyDir, 'external-analysis.md'), '# External Analysis');
|
|
937
|
-
|
|
938
|
-
const relPath = path.relative(tmpDir, storyFile).replace(/\\/g, '/');
|
|
939
|
-
const result = runAceTools(`init research-story ${relPath}`, tmpDir);
|
|
940
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
941
|
-
const data = JSON.parse(result.output);
|
|
942
|
-
|
|
943
|
-
assert.strictEqual(data.has_external_analysis, true);
|
|
944
|
-
assert.strictEqual(data.has_integration_analysis, false);
|
|
945
|
-
});
|
|
946
|
-
|
|
947
|
-
test('verifies wiki doc existence', () => {
|
|
948
|
-
const storyDir = path.join(tmpDir, '.ace', 'artifacts', 'product', 'epic', 'feat', 'story');
|
|
949
|
-
fs.mkdirSync(storyDir, { recursive: true });
|
|
950
|
-
const storyFile = path.join(storyDir, 'story.md');
|
|
951
|
-
fs.writeFileSync(storyFile, SAMPLE_STORY);
|
|
952
|
-
|
|
953
|
-
// Create some wiki docs but not all
|
|
954
|
-
fs.mkdirSync(path.join(tmpDir, '.docs', 'wiki', 'system-wide'), { recursive: true });
|
|
955
|
-
fs.writeFileSync(path.join(tmpDir, '.docs', 'wiki', 'system-wide', 'system-structure.md'), '# Structure');
|
|
956
|
-
fs.writeFileSync(path.join(tmpDir, '.docs', 'wiki', 'system-wide', 'coding-standards.md'), '# Standards');
|
|
957
|
-
|
|
958
|
-
const relPath = path.relative(tmpDir, storyFile).replace(/\\/g, '/');
|
|
959
|
-
const result = runAceTools(`init research-story ${relPath}`, tmpDir);
|
|
960
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
961
|
-
const data = JSON.parse(result.output);
|
|
962
|
-
|
|
963
|
-
assert.ok(data.wiki_docs_exist.existing.length >= 2, `Should find at least 2 existing: ${JSON.stringify(data.wiki_docs_exist.existing)}`);
|
|
964
|
-
assert.ok(data.wiki_docs_exist.missing.length >= 2, `Should find at least 2 missing: ${JSON.stringify(data.wiki_docs_exist.missing)}`);
|
|
965
|
-
assert.ok(data.wiki_docs_exist.existing.includes('.docs/wiki/system-wide/system-structure.md'));
|
|
966
|
-
});
|
|
967
|
-
|
|
968
|
-
test('returns error for non-existent file', () => {
|
|
969
|
-
const result = runAceTools('init research-story nonexistent.md', tmpDir);
|
|
970
|
-
assert.ok(result.success, `Command should still succeed with error in JSON: ${result.error}`);
|
|
971
|
-
const data = JSON.parse(result.output);
|
|
972
|
-
|
|
973
|
-
assert.strictEqual(data.story_valid, false);
|
|
974
|
-
assert.ok(data.story_error.includes('not found'), `story_error: ${data.story_error}`);
|
|
975
|
-
});
|
|
976
|
-
|
|
977
|
-
test('returns error for no parameter', () => {
|
|
978
|
-
const result = runAceTools('init research-story', tmpDir);
|
|
979
|
-
assert.ok(result.success, `Command should still succeed with error in JSON: ${result.error}`);
|
|
980
|
-
const data = JSON.parse(result.output);
|
|
981
|
-
|
|
982
|
-
assert.strictEqual(data.story_valid, false);
|
|
983
|
-
assert.ok(data.story_error !== null, 'Should have a story_error');
|
|
984
|
-
});
|
|
985
|
-
|
|
986
|
-
test('includes model and environment fields', () => {
|
|
987
|
-
const storyDir = path.join(tmpDir, '.ace', 'artifacts', 'product', 'epic', 'feat', 'story');
|
|
988
|
-
fs.mkdirSync(storyDir, { recursive: true });
|
|
989
|
-
const storyFile = path.join(storyDir, 'story.md');
|
|
990
|
-
fs.writeFileSync(storyFile, SAMPLE_STORY);
|
|
991
|
-
|
|
992
|
-
const relPath = path.relative(tmpDir, storyFile).replace(/\\/g, '/');
|
|
993
|
-
const result = runAceTools(`init research-story ${relPath}`, tmpDir);
|
|
994
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
995
|
-
const data = JSON.parse(result.output);
|
|
996
|
-
|
|
997
|
-
assert.ok(typeof data.analyst_model === 'string', 'analyst_model should be a string');
|
|
998
|
-
assert.ok(typeof data.mapper_model === 'string', 'mapper_model should be a string');
|
|
999
|
-
assert.ok(typeof data.has_git === 'boolean', 'has_git should be boolean');
|
|
1000
|
-
assert.ok(typeof data.has_gh_cli === 'boolean', 'has_gh_cli should be boolean');
|
|
1001
|
-
assert.ok(typeof data.commit_docs === 'boolean', 'commit_docs should be boolean');
|
|
1002
|
-
assert.ok(data.github_project !== undefined, 'github_project should be present');
|
|
1003
|
-
});
|
|
1004
|
-
|
|
1005
|
-
test('handles story without Relevant Wiki section', () => {
|
|
1006
|
-
const minimalStory = `# S1: Basic Story
|
|
1007
|
-
|
|
1008
|
-
**Feature**: F1 Auth | **Epic**: E1 Security
|
|
1009
|
-
**Status**: Todo | **Size**: 2
|
|
1010
|
-
|
|
1011
|
-
## User Story
|
|
1012
|
-
|
|
1013
|
-
> As a user,
|
|
1014
|
-
> I want to log in,
|
|
1015
|
-
> so that I can access my account.
|
|
1016
|
-
|
|
1017
|
-
## Description
|
|
1018
|
-
|
|
1019
|
-
Basic login functionality.
|
|
1020
|
-
|
|
1021
|
-
## Acceptance Criteria
|
|
1022
|
-
|
|
1023
|
-
### Scenario: Successful login
|
|
1024
|
-
|
|
1025
|
-
**Given** valid credentials
|
|
1026
|
-
**When** user submits login form
|
|
1027
|
-
**Then** user is redirected to dashboard
|
|
1028
|
-
|
|
1029
|
-
## Definition of Done
|
|
1030
|
-
|
|
1031
|
-
- [ ] All AC pass
|
|
1032
|
-
`;
|
|
1033
|
-
const storyDir = path.join(tmpDir, '.ace', 'artifacts', 'product', 'epic', 'feat', 'story');
|
|
1034
|
-
fs.mkdirSync(storyDir, { recursive: true });
|
|
1035
|
-
const storyFile = path.join(storyDir, 'story.md');
|
|
1036
|
-
fs.writeFileSync(storyFile, minimalStory);
|
|
1037
|
-
|
|
1038
|
-
const relPath = path.relative(tmpDir, storyFile).replace(/\\/g, '/');
|
|
1039
|
-
const result = runAceTools(`init research-story ${relPath}`, tmpDir);
|
|
1040
|
-
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
1041
|
-
const data = JSON.parse(result.output);
|
|
1042
|
-
|
|
1043
|
-
assert.strictEqual(data.story_valid, true);
|
|
1044
|
-
assert.strictEqual(data.wiki_references.total_count, 0);
|
|
1045
|
-
assert.strictEqual(data.acceptance_criteria_count, 1);
|
|
1046
|
-
assert.strictEqual(data.story.id, 'S1');
|
|
1047
|
-
});
|
|
1048
|
-
|
|
1049
|
-
test('classifies GitHub URL correctly', () => {
|
|
1050
|
-
// We can't actually fetch from GitHub in tests, but we can verify it tries
|
|
1051
|
-
const result = runAceTools('init research-story https://github.com/owner/repo/issues/123', tmpDir);
|
|
1052
|
-
assert.ok(result.success, `Command should succeed: ${result.error}`);
|
|
1053
|
-
const data = JSON.parse(result.output);
|
|
1054
|
-
|
|
1055
|
-
assert.strictEqual(data.story_source, 'github');
|
|
1056
|
-
// It will either fail due to no gh cli or fail to fetch — both are valid
|
|
1057
|
-
// The point is it classified correctly
|
|
1058
|
-
});
|
|
1059
|
-
|
|
1060
|
-
test('classifies issue number correctly', () => {
|
|
1061
|
-
const result = runAceTools('init research-story 42', tmpDir);
|
|
1062
|
-
assert.ok(result.success, `Command should succeed: ${result.error}`);
|
|
1063
|
-
const data = JSON.parse(result.output);
|
|
1064
|
-
|
|
1065
|
-
assert.strictEqual(data.story_source, 'github');
|
|
1066
|
-
});
|
|
1067
|
-
});
|
|
1068
|
-
|
|
1069
|
-
// ─── CLI error handling ───────────────────────────────────────────────────────
|
|
1070
|
-
|
|
1071
|
-
describe('CLI error handling', () => {
|
|
1072
|
-
test('errors on unknown command', () => {
|
|
1073
|
-
const result = runAceTools('nonexistent');
|
|
1074
|
-
assert.strictEqual(result.success, false);
|
|
1075
|
-
assert.ok(result.error.includes('Unknown command'), `Expected unknown command error, got: ${result.error}`);
|
|
1076
|
-
});
|
|
1077
|
-
|
|
1078
|
-
test('errors when no command provided', () => {
|
|
1079
|
-
const result = runAceTools('');
|
|
1080
|
-
assert.strictEqual(result.success, false);
|
|
1081
|
-
assert.ok(result.error.includes('Usage'), `Expected usage message, got: ${result.error}`);
|
|
1082
|
-
});
|
|
1083
|
-
|
|
1084
|
-
test('errors on unknown init subcommand', () => {
|
|
1085
|
-
const result = runAceTools('init nonexistent');
|
|
1086
|
-
assert.strictEqual(result.success, false);
|
|
1087
|
-
assert.ok(result.error.includes('Unknown init subcommand'), `Expected subcommand error, got: ${result.error}`);
|
|
1088
|
-
});
|
|
1089
|
-
});
|