@analizza-ai/testspec 0.1.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/CHANGELOG.md +24 -0
- package/LICENSE +21 -0
- package/README.md +189 -0
- package/bin/cli.js +42 -0
- package/package.json +69 -0
- package/src/adapters/agents/claude.js +88 -0
- package/src/adapters/agents/copilot.js +39 -0
- package/src/adapters/agents/index.js +22 -0
- package/src/adapters/sdd/index.js +23 -0
- package/src/adapters/sdd/openspec.js +58 -0
- package/src/adapters/sdd/speckit.js +19 -0
- package/src/commands/generate.js +66 -0
- package/src/commands/init.js +112 -0
- package/src/commands/report.js +60 -0
- package/src/commands/validate.js +68 -0
- package/src/core/reporter.js +44 -0
- package/src/core/spec-parser.js +141 -0
- package/src/core/stub-generator.js +92 -0
- package/src/core/testcontainers.js +39 -0
- package/src/core/tests-builder.js +120 -0
- package/src/index.js +10 -0
- package/src/utils/config.js +29 -0
- package/src/utils/logger.js +13 -0
- package/src/utils/sdd-detector.js +23 -0
- package/templates/agent-instructions/AGENTS.md +39 -0
- package/templates/agent-instructions/CLAUDE.md +48 -0
- package/templates/agent-instructions/copilot.md +52 -0
- package/templates/agent-instructions/skills/testspec-apply-qa.md +424 -0
- package/templates/agent-instructions/skills/testspec-generate.md +138 -0
- package/templates/agent-instructions/skills/testspec-run-qa.md +338 -0
- package/templates/agent-instructions/skills/testspec-specify-qa.md +535 -0
- package/templates/stubs/jest/unit.template.js +17 -0
- package/templates/stubs/junit/unit.template.java +27 -0
- package/templates/stubs/pytest/unit.template.py +18 -0
- package/templates/stubs/testcontainers/node-pg-kafka.template.js +38 -0
- package/templates/stubs/testcontainers/node-pg.template.js +32 -0
- package/templates/stubs/testcontainers/spring-pg-kafka.template.java +41 -0
- package/templates/stubs/vitest/unit.template.js +19 -0
- package/templates/tests-md/default.md +43 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-05-25
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `testspec init` — setup wizard for SDD framework + AI agent detection
|
|
14
|
+
- `testspec generate` — reads OpenSpec artifacts → writes `tests.md` + stubs
|
|
15
|
+
- `testspec validate` — maps test run results back to CT-01..N pass/fail
|
|
16
|
+
- `testspec report` — CT coverage/gap report
|
|
17
|
+
- OpenSpec adapter (full implementation)
|
|
18
|
+
- SpecKit adapter (stub, not yet implemented)
|
|
19
|
+
- Claude Code agent adapter (print-to-chat + `--api` mode)
|
|
20
|
+
- GitHub Copilot agent adapter (print-to-chat)
|
|
21
|
+
- Unit test stub templates: Vitest, Jest, Pytest, JUnit
|
|
22
|
+
- Integration test stub templates: Node.js + PostgreSQL, Node.js + PostgreSQL + Kafka, Spring Boot + PostgreSQL + Kafka
|
|
23
|
+
- `tests.md` canonical structure with CT tables, load profile hints, chaos scenarios
|
|
24
|
+
- Agent instruction templates: CLAUDE.md, copilot.md, AGENTS.md
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Diego Lirio
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# @analizza-ai/testspec
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@analizza-ai/testspec)
|
|
4
|
+
[](https://github.com/analizza-ai/testspec/actions/workflows/ci.yml)
|
|
5
|
+
[](./LICENSE)
|
|
6
|
+
[](./package.json)
|
|
7
|
+
|
|
8
|
+
**Spec Driven Test — the test layer for SDD**
|
|
9
|
+
|
|
10
|
+
testspec (SDT) inverts traditional test generation. Instead of writing code first and covering it with tests, SDT drives the entire test lifecycle from your spec artifacts. Specs are the single source of truth.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Test pyramid
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
┌─────────────────────────────────────────────────┐
|
|
18
|
+
│ CHAOS ENGINEERING │ ← /testspec-run-qa
|
|
19
|
+
│ (resilience · DR · fault injection) │
|
|
20
|
+
├─────────────────────────────────────────────────┤
|
|
21
|
+
│ QA LAYER │ ← /testspec-apply-qa
|
|
22
|
+
│ end-to-end tests · load tests (k6/Gatling) │
|
|
23
|
+
├─────────────────────────────────────────────────┤
|
|
24
|
+
│ DEVELOPER LAYER │ ← testspec generate
|
|
25
|
+
│ unit tests · integration tests (Testcontainers│
|
|
26
|
+
│ PostgreSQL · Kafka · etc.) │
|
|
27
|
+
└─────────────────────────────────────────────────┘
|
|
28
|
+
All layers driven by: tests.md
|
|
29
|
+
tests.md driven by: spec.md + proposal.md + design.md
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## What is SDT
|
|
35
|
+
|
|
36
|
+
testspec reads the spec artifacts produced by your SDD framework (OpenSpec, SpecKit, etc.) and generates:
|
|
37
|
+
|
|
38
|
+
1. **`tests.md`** — a technology-agnostic test document with numbered CT-01..N test cases
|
|
39
|
+
2. **Unit test stubs** — Jest / Vitest / Pytest / JUnit skeletons named after CTs
|
|
40
|
+
3. **Integration test stubs** — Testcontainers-based, pre-configured for your stack
|
|
41
|
+
|
|
42
|
+
The QA layer then consumes `tests.md` to generate k6/Gatling scripts without ambiguity.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Quick start
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# install globally
|
|
50
|
+
npm install -g @analizza-ai/testspec
|
|
51
|
+
|
|
52
|
+
# in your project root (must have openspec/ or similar)
|
|
53
|
+
testspec init # detects SDD framework, selects AI agent, writes config
|
|
54
|
+
testspec generate # reads specs → writes tests.md + stubs
|
|
55
|
+
testspec validate --results test-results.json
|
|
56
|
+
testspec report
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## How it works
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
Spec artifacts (proposal.md · design.md · specs/**/*.md · tasks.md)
|
|
65
|
+
↓
|
|
66
|
+
testspec generate
|
|
67
|
+
↓
|
|
68
|
+
SpecContext (scenarios, rules, contracts, dbAssertions)
|
|
69
|
+
↓
|
|
70
|
+
Agent prompt (printed to chat or sent via --api)
|
|
71
|
+
↓
|
|
72
|
+
tests.md (CT-01..N)
|
|
73
|
+
↓
|
|
74
|
+
Unit stubs + Integration stubs (Testcontainers)
|
|
75
|
+
↓
|
|
76
|
+
QA repo reads tests.md via GitHub MCP
|
|
77
|
+
↓
|
|
78
|
+
k6 / Gatling scripts · chaos scripts
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Supported SDD frameworks
|
|
84
|
+
|
|
85
|
+
| Framework | Status |
|
|
86
|
+
|-----------|--------|
|
|
87
|
+
| OpenSpec (`@fission-ai/openspec`) | ✅ v1 |
|
|
88
|
+
| SpecKit | planned |
|
|
89
|
+
| BMAD | planned |
|
|
90
|
+
| Kiro (AWS) | planned |
|
|
91
|
+
| Custom | planned |
|
|
92
|
+
|
|
93
|
+
## Supported AI agents
|
|
94
|
+
|
|
95
|
+
| Agent | Status |
|
|
96
|
+
|-------|--------|
|
|
97
|
+
| Claude Code | ✅ print-to-chat + `--api` |
|
|
98
|
+
| GitHub Copilot | ✅ print-to-chat |
|
|
99
|
+
|
|
100
|
+
## Supported unit test frameworks
|
|
101
|
+
|
|
102
|
+
| Framework | Status |
|
|
103
|
+
|-----------|--------|
|
|
104
|
+
| Vitest | ✅ |
|
|
105
|
+
| Jest | ✅ |
|
|
106
|
+
| Pytest | ✅ |
|
|
107
|
+
| JUnit | ✅ |
|
|
108
|
+
|
|
109
|
+
## Supported integration runtimes
|
|
110
|
+
|
|
111
|
+
| Runtime | Status |
|
|
112
|
+
|---------|--------|
|
|
113
|
+
| Testcontainers + PostgreSQL | ✅ |
|
|
114
|
+
| Testcontainers + PostgreSQL + Kafka | ✅ |
|
|
115
|
+
| Testcontainers + Spring Boot + Kafka | ✅ |
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## tests.md format
|
|
120
|
+
|
|
121
|
+
```yaml
|
|
122
|
+
---
|
|
123
|
+
feature: Item Creation
|
|
124
|
+
change: item-creation
|
|
125
|
+
generated: 2026-05-25T10:00:00.000Z
|
|
126
|
+
sdd: openspec
|
|
127
|
+
sdt: 0.1.0
|
|
128
|
+
stack: { lang: node, db: postgresql }
|
|
129
|
+
qa-repo: analizza-ai/qa
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
# Tests — Item Creation
|
|
133
|
+
|
|
134
|
+
## Scope
|
|
135
|
+
## Out of scope
|
|
136
|
+
|
|
137
|
+
## Test cases
|
|
138
|
+
|
|
139
|
+
### CT-01 — Create item with valid payload
|
|
140
|
+
|
|
141
|
+
| Field | Value |
|
|
142
|
+
|---------------------|------------------------------|
|
|
143
|
+
| Type | integration |
|
|
144
|
+
| Layer | developer |
|
|
145
|
+
| Precondition | User is authenticated |
|
|
146
|
+
| Input | POST /api/items { name: "x" }|
|
|
147
|
+
| Expected output | 201 Created { id: 1 } |
|
|
148
|
+
| DB validation | SELECT id FROM items WHERE id = 1 |
|
|
149
|
+
| Acceptance criteria | · item persisted · id returned |
|
|
150
|
+
|
|
151
|
+
## Load profile hints
|
|
152
|
+
## Chaos scenarios
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Configuration
|
|
158
|
+
|
|
159
|
+
`testspec.config.json` in your project root:
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"sdd": "openspec",
|
|
164
|
+
"agent": "claude",
|
|
165
|
+
"unitFramework": "vitest",
|
|
166
|
+
"stubs": { "unit": true, "integration": true },
|
|
167
|
+
"loadHints": true,
|
|
168
|
+
"chaosHints": true,
|
|
169
|
+
"qaRepo": "your-org/qa-repo"
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Adapter extension guide
|
|
176
|
+
|
|
177
|
+
See [docs/adapters-sdd.md](docs/adapters-sdd.md) to add a custom SDD framework adapter.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Contributing
|
|
182
|
+
|
|
183
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). Issues and PRs welcome.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## License
|
|
188
|
+
|
|
189
|
+
MIT © Diego Lirio
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* bin/cli.js
|
|
4
|
+
* Entry point for the testspec CLI.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { program } from 'commander';
|
|
8
|
+
import { readFileSync } from 'fs';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { dirname, join } from 'path';
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
|
|
14
|
+
|
|
15
|
+
program
|
|
16
|
+
.name('testspec')
|
|
17
|
+
.description('Spec Driven Test — generate tests.md and stubs from SDD specs')
|
|
18
|
+
.version(pkg.version);
|
|
19
|
+
|
|
20
|
+
const { initCommand } = await import('../src/commands/init.js');
|
|
21
|
+
const { generateCommand } = await import('../src/commands/generate.js');
|
|
22
|
+
const { validateCommand } = await import('../src/commands/validate.js');
|
|
23
|
+
const { reportCommand } = await import('../src/commands/report.js');
|
|
24
|
+
|
|
25
|
+
program.addCommand(initCommand);
|
|
26
|
+
program.addCommand(generateCommand);
|
|
27
|
+
program.addCommand(validateCommand);
|
|
28
|
+
program.addCommand(reportCommand);
|
|
29
|
+
|
|
30
|
+
// alias: testspec-generate → generate
|
|
31
|
+
program
|
|
32
|
+
.command('testspec-generate')
|
|
33
|
+
.description('Alias for "generate" — reads specs → writes tests.md + stubs')
|
|
34
|
+
.option('-c, --change <name>', 'target a specific change folder')
|
|
35
|
+
.option('--api', 'call Claude API instead of printing prompt to chat')
|
|
36
|
+
.option('--no-stubs', 'skip stub generation, write tests.md only')
|
|
37
|
+
.action(async (opts) => {
|
|
38
|
+
const { runGenerate } = await import('../src/commands/generate.js');
|
|
39
|
+
await runGenerate(opts);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@analizza-ai/testspec",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Spec Driven Test — the test layer for SDD",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"testspec": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./src/index.js",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/index.js"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20.19.0"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"test:watch": "vitest",
|
|
19
|
+
"test:coverage": "vitest run --coverage",
|
|
20
|
+
"lint": "eslint src bin tests",
|
|
21
|
+
"lint:fix": "eslint src bin tests --fix"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"sdd",
|
|
25
|
+
"spec-driven",
|
|
26
|
+
"test-generation",
|
|
27
|
+
"testspec",
|
|
28
|
+
"openspec",
|
|
29
|
+
"claude",
|
|
30
|
+
"ai-testing"
|
|
31
|
+
],
|
|
32
|
+
"author": "Diego Lirio <diegolirio.dl@gmail.com>",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/analizza-ai/testspec.git"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/analizza-ai/testspec/issues"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/analizza-ai/testspec#readme",
|
|
42
|
+
"files": [
|
|
43
|
+
"bin/",
|
|
44
|
+
"src/",
|
|
45
|
+
"templates/",
|
|
46
|
+
"README.md",
|
|
47
|
+
"LICENSE",
|
|
48
|
+
"CHANGELOG.md"
|
|
49
|
+
],
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public",
|
|
52
|
+
"registry": "https://registry.npmjs.org/"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"commander": "^12.0.0",
|
|
56
|
+
"gray-matter": "^4.0.3",
|
|
57
|
+
"chalk": "^5.3.0",
|
|
58
|
+
"glob": "^11.0.0",
|
|
59
|
+
"js-yaml": "^4.1.0"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"vitest": "^1.6.0",
|
|
63
|
+
"eslint": "^9.0.0",
|
|
64
|
+
"@anthropic-ai/sdk": "^0.30.0"
|
|
65
|
+
},
|
|
66
|
+
"optionalDependencies": {
|
|
67
|
+
"@anthropic-ai/sdk": "^0.30.0"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/adapters/agents/claude.js
|
|
3
|
+
* Claude adapter. Builds the SpecContext → tests.md prompt.
|
|
4
|
+
* Default: prints prompt to stdout (agent reads it in chat).
|
|
5
|
+
* With --api flag: calls Claude API (requires ANTHROPIC_API_KEY).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { buildPrompt } from '../../core/tests-builder.js';
|
|
9
|
+
import { log } from '../../utils/logger.js';
|
|
10
|
+
|
|
11
|
+
export class ClaudeAdapter {
|
|
12
|
+
/**
|
|
13
|
+
* @param {object} specContext
|
|
14
|
+
* @param {object} config
|
|
15
|
+
* @param {{ useApi: boolean }} opts
|
|
16
|
+
* @returns {Promise<string>} tests.md content
|
|
17
|
+
*/
|
|
18
|
+
async generateTests(specContext, config, opts = {}) {
|
|
19
|
+
const prompt = buildPrompt(specContext, config);
|
|
20
|
+
|
|
21
|
+
if (!opts.useApi) {
|
|
22
|
+
log.info('\n─── Prompt for Claude Code (/testspec-generate) ────────\n');
|
|
23
|
+
console.log(prompt);
|
|
24
|
+
log.info('\n────────────────────────────────────────────────────────\n');
|
|
25
|
+
log.warn('Paste the output above into your Claude Code chat to get tests.md.');
|
|
26
|
+
log.warn('Re-run with --api to call Claude API directly.');
|
|
27
|
+
|
|
28
|
+
// Return a placeholder tests.md so the CLI can write a file
|
|
29
|
+
return buildPlaceholder(specContext);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return this.#callApi(prompt, specContext, config);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async #callApi(prompt, _specContext, _config) {
|
|
36
|
+
let Anthropic;
|
|
37
|
+
try {
|
|
38
|
+
({ default: Anthropic } = await import('@anthropic-ai/sdk'));
|
|
39
|
+
} catch {
|
|
40
|
+
log.error('@anthropic-ai/sdk not installed. Run: npm install @anthropic-ai/sdk');
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
45
|
+
if (!apiKey) {
|
|
46
|
+
log.error('ANTHROPIC_API_KEY env var is required for --api mode.');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const client = new Anthropic({ apiKey });
|
|
51
|
+
log.info('Calling Claude API…');
|
|
52
|
+
|
|
53
|
+
const message = await client.messages.create({
|
|
54
|
+
model: 'claude-sonnet-4-6',
|
|
55
|
+
max_tokens: 8192,
|
|
56
|
+
messages: [{ role: 'user', content: prompt }],
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return message.content[0].text;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildPlaceholder(specContext) {
|
|
64
|
+
const now = new Date().toISOString();
|
|
65
|
+
return `---
|
|
66
|
+
feature: ${specContext.feature}
|
|
67
|
+
change: ${specContext.changeName}
|
|
68
|
+
generated: ${now}
|
|
69
|
+
sdd: ${specContext.sdd}
|
|
70
|
+
sdt: 0.1.0
|
|
71
|
+
status: placeholder — run with --api or paste the prompt into Claude Code
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
# Tests — ${specContext.feature}
|
|
75
|
+
|
|
76
|
+
> This file was generated as a placeholder. Paste the printed prompt into your Claude Code chat
|
|
77
|
+
> and replace this file with the output, or re-run \`testspec generate --api\`.
|
|
78
|
+
|
|
79
|
+
## Scope
|
|
80
|
+
_To be filled by agent_
|
|
81
|
+
|
|
82
|
+
## Out of scope
|
|
83
|
+
_To be filled by agent_
|
|
84
|
+
|
|
85
|
+
## Test cases
|
|
86
|
+
_To be filled by agent_
|
|
87
|
+
`;
|
|
88
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/adapters/agents/copilot.js
|
|
3
|
+
* GitHub Copilot adapter. Prints the prompt so Copilot can read it via copilot-instructions.md context.
|
|
4
|
+
* Does not call an API — Copilot reads the prompt in the editor chat.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { buildPrompt } from '../../core/tests-builder.js';
|
|
8
|
+
import { log } from '../../utils/logger.js';
|
|
9
|
+
|
|
10
|
+
export class CopilotAdapter {
|
|
11
|
+
/**
|
|
12
|
+
* @param {object} specContext
|
|
13
|
+
* @param {object} config
|
|
14
|
+
* @returns {Promise<string>} placeholder tests.md
|
|
15
|
+
*/
|
|
16
|
+
async generateTests(specContext, config) {
|
|
17
|
+
const prompt = buildPrompt(specContext, config);
|
|
18
|
+
|
|
19
|
+
log.info('\n─── Prompt for GitHub Copilot ───────────────────────────\n');
|
|
20
|
+
console.log(prompt);
|
|
21
|
+
log.info('\n────────────────────────────────────────────────────────\n');
|
|
22
|
+
log.warn('Paste the above into GitHub Copilot Chat to generate tests.md.');
|
|
23
|
+
|
|
24
|
+
const now = new Date().toISOString();
|
|
25
|
+
return `---
|
|
26
|
+
feature: ${specContext.feature}
|
|
27
|
+
change: ${specContext.changeName}
|
|
28
|
+
generated: ${now}
|
|
29
|
+
sdd: ${specContext.sdd}
|
|
30
|
+
sdt: 0.1.0
|
|
31
|
+
status: placeholder — paste the prompt into GitHub Copilot Chat
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
# Tests — ${specContext.feature}
|
|
35
|
+
|
|
36
|
+
> Paste the printed prompt into GitHub Copilot Chat and replace this file with the output.
|
|
37
|
+
`;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/adapters/agents/index.js
|
|
3
|
+
* Registry mapping AI agent names to their adapter implementations.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ClaudeAdapter } from './claude.js';
|
|
7
|
+
import { CopilotAdapter } from './copilot.js';
|
|
8
|
+
|
|
9
|
+
const registry = {
|
|
10
|
+
claude: ClaudeAdapter,
|
|
11
|
+
copilot: CopilotAdapter,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Returns an instantiated adapter for the given agent name.
|
|
16
|
+
* @param {string} name
|
|
17
|
+
*/
|
|
18
|
+
export function getAdapter(name = 'claude') {
|
|
19
|
+
const Adapter = registry[name.toLowerCase()];
|
|
20
|
+
if (!Adapter) throw new Error(`Unknown agent adapter: "${name}". Supported: ${Object.keys(registry).join(', ')}`);
|
|
21
|
+
return new Adapter();
|
|
22
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/adapters/sdd/index.js
|
|
3
|
+
* Registry mapping SDD framework names to their adapter implementations.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { OpenSpecAdapter } from './openspec.js';
|
|
7
|
+
import { SpecKitAdapter } from './speckit.js';
|
|
8
|
+
|
|
9
|
+
const registry = {
|
|
10
|
+
openspec: OpenSpecAdapter,
|
|
11
|
+
speckit: SpecKitAdapter,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Returns an instantiated adapter for the given SDD framework name.
|
|
16
|
+
* @param {string} name
|
|
17
|
+
* @returns {OpenSpecAdapter | SpecKitAdapter}
|
|
18
|
+
*/
|
|
19
|
+
export function getAdapter(name = 'openspec') {
|
|
20
|
+
const Adapter = registry[name.toLowerCase()];
|
|
21
|
+
if (!Adapter) throw new Error(`Unknown SDD adapter: "${name}". Supported: ${Object.keys(registry).join(', ')}`);
|
|
22
|
+
return new Adapter();
|
|
23
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/adapters/sdd/openspec.js
|
|
3
|
+
* Full OpenSpec adapter. Reads change folders under openspec/changes/{name}/
|
|
4
|
+
* and loads proposal.md, design.md, specs/**\/*.md, tasks.md, config.yaml.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { glob } from 'glob';
|
|
10
|
+
import yaml from 'js-yaml';
|
|
11
|
+
|
|
12
|
+
export class OpenSpecAdapter {
|
|
13
|
+
/** @returns {string[]} list of change names (non-archived) */
|
|
14
|
+
discoverChanges(root) {
|
|
15
|
+
const changesDir = join(root, 'openspec', 'changes');
|
|
16
|
+
if (!existsSync(changesDir)) return [];
|
|
17
|
+
return readdirSync(changesDir, { withFileTypes: true })
|
|
18
|
+
.filter((e) => e.isDirectory() && e.name !== 'archive')
|
|
19
|
+
.map((e) => e.name)
|
|
20
|
+
.sort();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Loads all spec artifacts for a change.
|
|
25
|
+
* @returns {{ proposal: string, design: string, specs: string[], tasks: string, config: object }}
|
|
26
|
+
*/
|
|
27
|
+
loadArtifacts(root, changeName) {
|
|
28
|
+
const base = join(root, 'openspec', 'changes', changeName);
|
|
29
|
+
|
|
30
|
+
const read = (file) => {
|
|
31
|
+
const p = join(base, file);
|
|
32
|
+
return existsSync(p) ? readFileSync(p, 'utf-8') : '';
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const specFiles = glob.sync('specs/**/*.md', { cwd: base });
|
|
36
|
+
const specs = specFiles.map((f) => readFileSync(join(base, f), 'utf-8'));
|
|
37
|
+
|
|
38
|
+
const configPath = join(root, 'openspec', 'config.yaml');
|
|
39
|
+
const config = existsSync(configPath)
|
|
40
|
+
? yaml.load(readFileSync(configPath, 'utf-8'))
|
|
41
|
+
: {};
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
proposal: read('proposal.md'),
|
|
45
|
+
design: read('design.md'),
|
|
46
|
+
specs,
|
|
47
|
+
specFiles,
|
|
48
|
+
tasks: read('tasks.md'),
|
|
49
|
+
config,
|
|
50
|
+
changeName,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** @returns {string} absolute path where tests.md should be written */
|
|
55
|
+
getOutputPath(root, changeName) {
|
|
56
|
+
return join(root, 'openspec', 'changes', changeName, 'tests.md');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/adapters/sdd/speckit.js
|
|
3
|
+
* SpecKit adapter stub. Interface only — not yet implemented.
|
|
4
|
+
* Implement discoverChanges / loadArtifacts / getOutputPath when SpecKit is supported.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export class SpecKitAdapter {
|
|
8
|
+
discoverChanges(_root) {
|
|
9
|
+
throw new Error('SpecKit adapter is not yet implemented.');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
loadArtifacts(_root, _changeName) {
|
|
13
|
+
throw new Error('SpecKit adapter is not yet implemented.');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getOutputPath(_root, _changeName) {
|
|
17
|
+
throw new Error('SpecKit adapter is not yet implemented.');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/commands/generate.js
|
|
3
|
+
* Core command: reads SDD spec artifacts → builds tests.md + optional stubs.
|
|
4
|
+
* Also registered as the "testspec-generate" alias in bin/cli.js.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Command } from 'commander';
|
|
8
|
+
import { log } from '../utils/logger.js';
|
|
9
|
+
import { loadConfig } from '../utils/config.js';
|
|
10
|
+
import { getAdapter as getSddAdapter } from '../adapters/sdd/index.js';
|
|
11
|
+
import { getAdapter as getAgentAdapter } from '../adapters/agents/index.js';
|
|
12
|
+
import { parseSpecs } from '../core/spec-parser.js';
|
|
13
|
+
import { generateStubs } from '../core/stub-generator.js';
|
|
14
|
+
import { writeFileSync, mkdirSync } from 'fs';
|
|
15
|
+
import { dirname } from 'path';
|
|
16
|
+
|
|
17
|
+
export const generateCommand = new Command('generate')
|
|
18
|
+
.description('Read specs → write tests.md + optional unit/integration stubs')
|
|
19
|
+
.option('-c, --change <name>', 'target a specific change folder')
|
|
20
|
+
.option('--api', 'call Claude API instead of printing prompt to chat')
|
|
21
|
+
.option('--no-stubs', 'skip stub generation, write tests.md only')
|
|
22
|
+
.action(runGenerate);
|
|
23
|
+
|
|
24
|
+
export async function runGenerate(opts = {}) {
|
|
25
|
+
const config = loadConfig(process.cwd());
|
|
26
|
+
const sddAdapter = getSddAdapter(config.sdd);
|
|
27
|
+
const agentAdapter = getAgentAdapter(config.agent);
|
|
28
|
+
|
|
29
|
+
// 1. Discover and load artifacts
|
|
30
|
+
const changes = sddAdapter.discoverChanges(process.cwd());
|
|
31
|
+
if (changes.length === 0) {
|
|
32
|
+
log.error('No changes found. Run from a project root with SDD artifacts.');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const changeName = opts.change || changes[changes.length - 1];
|
|
37
|
+
log.info(`Processing change: ${changeName}`);
|
|
38
|
+
|
|
39
|
+
const artifacts = sddAdapter.loadArtifacts(process.cwd(), changeName);
|
|
40
|
+
const outputPath = sddAdapter.getOutputPath(process.cwd(), changeName);
|
|
41
|
+
|
|
42
|
+
// 2. Parse into SpecContext
|
|
43
|
+
const specContext = parseSpecs(artifacts, config);
|
|
44
|
+
log.info(`Parsed ${specContext.specs.length} spec file(s)`);
|
|
45
|
+
|
|
46
|
+
// 3. Generate tests.md via agent adapter
|
|
47
|
+
const testsContent = await agentAdapter.generateTests(specContext, config, {
|
|
48
|
+
useApi: opts.api ?? false,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// 4. Write tests.md
|
|
52
|
+
mkdirSync(dirname(outputPath), { recursive: true });
|
|
53
|
+
writeFileSync(outputPath, testsContent);
|
|
54
|
+
log.success(`tests.md written → ${outputPath}`);
|
|
55
|
+
|
|
56
|
+
// 5. Optional stubs
|
|
57
|
+
const stubsEnabled = opts.stubs !== false && config.stubs?.unit !== false;
|
|
58
|
+
if (stubsEnabled) {
|
|
59
|
+
const stubResults = generateStubs(specContext, config, dirname(outputPath));
|
|
60
|
+
log.success(`Unit stubs: ${stubResults.unit.length} file(s)`);
|
|
61
|
+
log.success(`Integration stubs: ${stubResults.integration.length} file(s)`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const ctCount = (testsContent.match(/^### CT-\d+/gm) || []).length;
|
|
65
|
+
log.info(`\nSummary: ${specContext.specs.length} spec(s) → ${ctCount} CT(s) → tests.md written`);
|
|
66
|
+
}
|