mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-12 05:00:36 +08:00
Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57216a7824 | ||
|
|
6e210cf084 | ||
|
|
5376030421 | ||
|
|
7d735eb2d8 | ||
|
|
4d55e9ac6d | ||
|
|
9d674b22a3 | ||
|
|
96458ced1f | ||
|
|
3d8f2a5974 | ||
|
|
006676c973 | ||
|
|
63f45c0fcf | ||
|
|
522126a6ee | ||
|
|
23b8030494 | ||
|
|
f70df96656 | ||
|
|
df12368f11 | ||
|
|
acd1ca28f3 | ||
|
|
aedf4a34af | ||
|
|
f0c52ac7e8 | ||
|
|
f933e9b144 | ||
|
|
24b4866426 | ||
|
|
b7899602b4 | ||
|
|
b66d914198 | ||
|
|
9926103505 | ||
|
|
873e45a996 |
@@ -0,0 +1,6 @@
|
||||
This directory is managed by Changesets.
|
||||
|
||||
- Add a changeset locally with `pnpm changeset`.
|
||||
- The CI "Release (prepare)" workflow opens/updates a Version Packages PR.
|
||||
- Publishing happens from a GitHub Release via the "Publish to npm" workflow.
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build project
|
||||
run: pnpm run build
|
||||
|
||||
- name: Run tests
|
||||
run: pnpm test
|
||||
|
||||
- name: Upload test coverage
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-report
|
||||
path: coverage/
|
||||
retention-days: 7
|
||||
|
||||
lint:
|
||||
name: Lint & Type Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build project
|
||||
run: pnpm run build
|
||||
|
||||
- name: Type check
|
||||
run: pnpm exec tsc --noEmit
|
||||
|
||||
- name: Check for build artifacts
|
||||
run: |
|
||||
if [ ! -d "dist" ]; then
|
||||
echo "Error: dist directory not found after build"
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "dist/cli/index.js" ]; then
|
||||
echo "Error: CLI entry point not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
validate-changesets:
|
||||
name: Validate Changesets
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Validate changesets
|
||||
run: |
|
||||
if command -v changeset &> /dev/null; then
|
||||
pnpm exec changeset status --since=origin/main
|
||||
else
|
||||
echo "Changesets not configured, skipping validation"
|
||||
fi
|
||||
|
||||
required-checks:
|
||||
name: All checks passed
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test, lint]
|
||||
if: always()
|
||||
steps:
|
||||
- name: Verify all checks passed
|
||||
run: |
|
||||
if [[ "${{ needs.test.result }}" != "success" ]]; then
|
||||
echo "Test job failed"
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${{ needs.lint.result }}" != "success" ]]; then
|
||||
echo "Lint job failed"
|
||||
exit 1
|
||||
fi
|
||||
echo "All required checks passed!"
|
||||
@@ -0,0 +1,37 @@
|
||||
name: Release (prepare)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
# Opens/updates the Version Packages PR; no publishing here
|
||||
- name: Create/Update Version PR
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
title: 'chore(release): version packages'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
name: Publish to npm
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch: {}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: publish-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
scope: '@fission-ai'
|
||||
always-auth: true
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build project
|
||||
run: pnpm run build
|
||||
|
||||
- name: Ensure running from a tag
|
||||
run: |
|
||||
if [[ "$GITHUB_REF" != refs/tags/* ]]; then
|
||||
echo "This workflow must run from a tag (got: $GITHUB_REF)";
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
- name: Verify release tag matches package.json
|
||||
run: |
|
||||
TAG="${GITHUB_REF_NAME#v}"
|
||||
PKG_VERSION=$(node -p "require('./package.json').version")
|
||||
if [ "$TAG" != "$PKG_VERSION" ]; then
|
||||
echo "Tag v$TAG does not match package.json $PKG_VERSION"; exit 1
|
||||
fi
|
||||
|
||||
- name: Debug npm auth and context
|
||||
run: |
|
||||
test -n "$NODE_AUTH_TOKEN" || (echo "NODE_AUTH_TOKEN is missing" && exit 1)
|
||||
echo "NODE_AUTH_TOKEN present"
|
||||
npm --version
|
||||
pnpm --version
|
||||
node --version
|
||||
npm config get registry
|
||||
npm whoami
|
||||
npm ping
|
||||
|
||||
- run: pnpm test
|
||||
|
||||
- name: Publish
|
||||
run: pnpm publish --access public --provenance --no-git-checks --tag next
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 24b4866: Initial release
|
||||
@@ -0,0 +1,7 @@
|
||||
# @fission-ai/openspec
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 24b4866: Initial release
|
||||
@@ -1,57 +0,0 @@
|
||||
# Delta: CLI List Command
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Command Execution
|
||||
The command SHALL scan and analyze either active changes or specs based on the selected mode.
|
||||
|
||||
#### Scenario: Scanning for changes (default)
|
||||
- **WHEN** `openspec list` is executed without flags
|
||||
- **THEN** scan the `openspec/changes/` directory for change directories
|
||||
- **AND** exclude the `archive/` subdirectory from results
|
||||
- **AND** parse each change's `tasks.md` file to count task completion
|
||||
|
||||
#### Scenario: Scanning for specs
|
||||
- **WHEN** `openspec list --specs` is executed
|
||||
- **THEN** scan the `openspec/specs/` directory for capabilities
|
||||
- **AND** read each capability's `spec.md`
|
||||
- **AND** parse requirements to compute requirement counts
|
||||
|
||||
### Requirement: Output Format
|
||||
The command SHALL display items in a clear, readable table format with mode-appropriate progress or counts.
|
||||
|
||||
#### Scenario: Displaying change list (default)
|
||||
- **WHEN** displaying the list of changes
|
||||
- **THEN** show a table with columns:
|
||||
- Change name (directory name)
|
||||
- Task progress (e.g., "3/5 tasks" or "✓ Complete")
|
||||
|
||||
#### Scenario: Displaying spec list
|
||||
- **WHEN** displaying the list of specs
|
||||
- **THEN** show a table with columns:
|
||||
- Spec id (directory name)
|
||||
- Requirement count (e.g., "requirements 12")
|
||||
|
||||
### Requirement: Empty State
|
||||
The command SHALL provide clear feedback when no items are present for the selected mode.
|
||||
|
||||
#### Scenario: Handling empty state (changes)
|
||||
- **WHEN** no active changes exist (only archive/ or empty changes/)
|
||||
- **THEN** display: "No active changes found."
|
||||
|
||||
#### Scenario: Handling empty state (specs)
|
||||
- **WHEN** no specs directory exists or contains no capabilities
|
||||
- **THEN** display: "No specs found."
|
||||
|
||||
### Requirement: Flags
|
||||
The command SHALL accept flags to select the noun being listed.
|
||||
|
||||
#### Scenario: Selecting specs
|
||||
- **WHEN** `--specs` is provided
|
||||
- **THEN** list specs instead of changes
|
||||
|
||||
#### Scenario: Selecting changes
|
||||
- **WHEN** `--changes` is provided
|
||||
- **THEN** list changes explicitly (same as default behavior)
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
# Change: Improve Deterministic Tests (Isolate From Repo State)
|
||||
|
||||
## Problem
|
||||
|
||||
Some unit tests (e.g., ChangeCommand.show/validate) read the live repository
|
||||
state via `process.cwd()` and `openspec/changes`. This makes outcomes depend on
|
||||
whatever directories happen to exist and the order returned by `fs.readdir`,
|
||||
causing flaky success/failure across environments.
|
||||
|
||||
Symptoms observed:
|
||||
- Tests sometimes select a partial or unrelated change folder.
|
||||
- Failures like missing `proposal.md` when a stray change directory is picked.
|
||||
- Environment/sandbox differences alter `readdir` ordering and worker behavior.
|
||||
|
||||
## Goals
|
||||
|
||||
- Make tests deterministic and hermetic.
|
||||
- Remove dependence on real repo contents and directory ordering.
|
||||
- Keep runtime behavior unchanged for end users.
|
||||
|
||||
## Non‑Goals
|
||||
|
||||
- Introduce heavy frameworks or test harness complexity.
|
||||
- Redesign CLI behavior or change default paths for users.
|
||||
|
||||
## Approach
|
||||
|
||||
1) Test-local fixture root
|
||||
- Each suite that touches filesystem discovery creates a temporary directory:
|
||||
- `openspec/changes/sample-change/proposal.md`
|
||||
- `openspec/changes/sample-change/specs/sample/spec.md`
|
||||
- `beforeAll`: `process.chdir(tmpRoot)`; `afterAll`: restore original cwd.
|
||||
- Use a constant `changeName = 'sample-change'`; remove reliance on
|
||||
`readdir` order.
|
||||
|
||||
2) Optional thin DI for commands (minimal, if needed)
|
||||
- Allow `ChangeCommand` (and similar) to accept an optional `root` path
|
||||
(default `process.cwd()`), used for path resolution.
|
||||
- Tests pass the temp root explicitly; production code remains unchanged.
|
||||
|
||||
3) Harden discovery helpers (safe enhancement)
|
||||
- Update `getActiveChangeIds()`/`getActiveChanges()` to include only
|
||||
directories containing `proposal.md` (and optionally at least one
|
||||
`specs/*/spec.md`).
|
||||
- Prevents incomplete/stray change folders from being treated as active.
|
||||
|
||||
## Rationale
|
||||
|
||||
- Small, focused changes eliminate flakiness without altering user workflows.
|
||||
- Temporary fixtures are a well-understood testing pattern and keep tests fast.
|
||||
- Optional constructor root param is a minimal DI surface that avoids global
|
||||
stubbing and keeps code simple.
|
||||
|
||||
## Risks & Mitigations
|
||||
|
||||
- Risk: Tests forget to restore `process.cwd()`.
|
||||
- Mitigation: Add `afterAll` guard restoring cwd; reset `process.exitCode` in
|
||||
`afterEach` where modified.
|
||||
- Risk: Behavior divergence if DI root is misused.
|
||||
- Mitigation: Default to `process.cwd()`; only tests pass custom roots.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Tests that previously depended on repo state now:
|
||||
- Create and use a temp fixture root.
|
||||
- Do not read real `openspec/changes` during execution.
|
||||
- Pass consistently regardless of directory order or stray folders.
|
||||
- No change to CLI behavior for end users (paths still default to cwd).
|
||||
|
||||
## Rollout
|
||||
|
||||
- Phase 1: Convert the suites that hit `ChangeCommand.show/validate` to
|
||||
isolated fixtures; verify stability locally and in CI.
|
||||
- Phase 2: Apply the same pattern to any remaining suites that touch file
|
||||
discovery (`list`, `show`, `validate`, `diff`).
|
||||
- Phase 3 (optional): Introduce the constructor `root` param and discovery
|
||||
hardening, if Phase 1 alone isn’t sufficient.
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. Test Isolation
|
||||
- [x] 1.1 Create temp fixture roots per suite (openspec/changes, openspec/specs)
|
||||
- [x] 1.2 Use process.chdir to temp root within tests
|
||||
- [x] 1.3 Restore original cwd and clean temp dirs after each
|
||||
|
||||
## 2. Deterministic Discovery
|
||||
- [x] 2.1 Implement getActiveChangeIds(root?) to only include dirs with proposal.md
|
||||
- [x] 2.2 Implement getSpecIds(root?) to only include dirs with spec.md
|
||||
- [x] 2.3 Return sorted results to avoid fs.readdir ordering variance
|
||||
|
||||
## 3. Command Integration
|
||||
- [x] 3.1 Ensure change/show/validate rely on cwd and discovery helpers
|
||||
- [x] 3.2 Keep runtime behavior unchanged for end users
|
||||
|
||||
## 4. Validation
|
||||
- [x] 4.1 Convert affected command tests (show, spec, validate, change) to isolated fixtures
|
||||
- [x] 4.2 Verify tests pass consistently across environments
|
||||
- [x] 4.3 Confirm no reads from real repo state during tests
|
||||
|
||||
## 5. Optional (Not Needed Now)
|
||||
- [x] 5.1 Add optional root param to discovery helpers (default process.cwd())
|
||||
- [ ] 5.2 Consider threading root through command constructors if ever required
|
||||
|
||||
+8
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openspec",
|
||||
"version": "0.1.0-beta.0",
|
||||
"name": "@fission-ai/openspec",
|
||||
"version": "0.1.0",
|
||||
"description": "AI-native system for spec-driven development",
|
||||
"keywords": [
|
||||
"openspec",
|
||||
@@ -9,15 +9,16 @@
|
||||
"ai",
|
||||
"development"
|
||||
],
|
||||
"homepage": "https://github.com/tabdilsaidixit/openspec",
|
||||
"homepage": "https://github.com/Fission-AI/OpenSpec",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tabdilsaidixit/openspec.git"
|
||||
"url": "https://github.com/Fission-AI/OpenSpec"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": "OpenSpec Contributors",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"tag": "next"
|
||||
},
|
||||
"exports": {
|
||||
@@ -45,12 +46,14 @@
|
||||
"test:ui": "vitest --ui",
|
||||
"test:coverage": "vitest --coverage",
|
||||
"prepare": "pnpm run build",
|
||||
"prepublishOnly": "pnpm run build"
|
||||
"prepublishOnly": "pnpm run build",
|
||||
"changeset": "changeset"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.27.7",
|
||||
"@types/node": "^24.2.0",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"typescript": "^5.9.2",
|
||||
|
||||
Generated
+757
File diff suppressed because it is too large
Load Diff
+13
-5
@@ -241,10 +241,18 @@ export class ChangeCommand {
|
||||
private async getActiveChanges(changesPath: string): Promise<string[]> {
|
||||
try {
|
||||
const entries = await fs.readdir(changesPath, { withFileTypes: true });
|
||||
return entries
|
||||
.filter(entry => entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== ARCHIVE_DIR)
|
||||
.map(entry => entry.name)
|
||||
.sort();
|
||||
const result: string[] = [];
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === ARCHIVE_DIR) continue;
|
||||
const proposalPath = path.join(changesPath, entry.name, 'proposal.md');
|
||||
try {
|
||||
await fs.access(proposalPath);
|
||||
result.push(entry.name);
|
||||
} catch {
|
||||
// skip directories without proposal.md
|
||||
}
|
||||
}
|
||||
return result.sort();
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
@@ -280,4 +288,4 @@ export class ChangeCommand {
|
||||
console.error('Next steps:');
|
||||
bullets.forEach(b => console.error(` ${b}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,18 @@ export async function getActiveChangeIds(root: string = process.cwd()): Promise<
|
||||
const changesPath = path.join(root, 'openspec', 'changes');
|
||||
try {
|
||||
const entries = await fs.readdir(changesPath, { withFileTypes: true });
|
||||
return entries
|
||||
.filter(entry => entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'archive')
|
||||
.map(entry => entry.name)
|
||||
.sort();
|
||||
const result: string[] = [];
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'archive') continue;
|
||||
const proposalPath = path.join(changesPath, entry.name, 'proposal.md');
|
||||
try {
|
||||
await fs.access(proposalPath);
|
||||
result.push(entry.name);
|
||||
} catch {
|
||||
// skip directories without proposal.md
|
||||
}
|
||||
}
|
||||
return result.sort();
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
@@ -35,4 +43,3 @@ export async function getSpecIds(root: string = process.cwd()): Promise<string[]
|
||||
return result.sort();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
@@ -9,9 +9,6 @@ describe('change show (interactive behavior)', () => {
|
||||
const changesDir = path.join(testDir, 'openspec', 'changes');
|
||||
const bin = path.join(projectRoot, 'bin', 'openspec.js');
|
||||
|
||||
beforeAll(() => {
|
||||
execSync('pnpm -s build', { stdio: 'pipe' });
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await fs.mkdir(changesDir, { recursive: true });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
@@ -12,9 +12,6 @@ describe('change validate (interactive behavior)', () => {
|
||||
const changesDir = path.join(testDir, 'openspec', 'changes');
|
||||
const bin = path.join(projectRoot, 'bin', 'openspec.js');
|
||||
|
||||
beforeAll(() => {
|
||||
execSync('pnpm -s build', { stdio: 'pipe' });
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await fs.mkdir(changesDir, { recursive: true });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, beforeAll } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
@@ -10,9 +10,6 @@ describe('top-level show command', () => {
|
||||
const specsDir = path.join(testDir, 'openspec', 'specs');
|
||||
const openspecBin = path.join(projectRoot, 'bin', 'openspec.js');
|
||||
|
||||
beforeAll(() => {
|
||||
execSync('pnpm -s build', { stdio: 'pipe' });
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await fs.mkdir(changesDir, { recursive: true });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
@@ -9,9 +9,6 @@ describe('spec show (interactive behavior)', () => {
|
||||
const specsDir = path.join(testDir, 'openspec', 'specs');
|
||||
const bin = path.join(projectRoot, 'bin', 'openspec.js');
|
||||
|
||||
beforeAll(() => {
|
||||
execSync('pnpm -s build', { stdio: 'pipe' });
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await fs.mkdir(specsDir, { recursive: true });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
@@ -9,9 +9,6 @@ describe('spec validate (interactive behavior)', () => {
|
||||
const specsDir = path.join(testDir, 'openspec', 'specs');
|
||||
const bin = path.join(projectRoot, 'bin', 'openspec.js');
|
||||
|
||||
beforeAll(() => {
|
||||
execSync('pnpm -s build', { stdio: 'pipe' });
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await fs.mkdir(specsDir, { recursive: true });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, beforeAll } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
@@ -9,10 +9,6 @@ describe('spec command', () => {
|
||||
const specsDir = path.join(testDir, 'openspec', 'specs');
|
||||
const openspecBin = path.join(projectRoot, 'bin', 'openspec.js');
|
||||
|
||||
beforeAll(() => {
|
||||
// Ensure CLI is built so bin/openspec.js loads latest logic from dist/
|
||||
execSync('pnpm -s build', { stdio: 'pipe' });
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await fs.mkdir(specsDir, { recursive: true });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
@@ -9,10 +9,6 @@ describe('validate command enriched human output', () => {
|
||||
const changesDir = path.join(testDir, 'openspec', 'changes');
|
||||
const bin = path.join(projectRoot, 'bin', 'openspec.js');
|
||||
|
||||
beforeAll(() => {
|
||||
// Build once so the bin can resolve dist
|
||||
try { execSync('pnpm -s build', { stdio: 'pipe' }); } catch {}
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await fs.mkdir(changesDir, { recursive: true });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
@@ -10,9 +10,6 @@ describe('top-level validate command', () => {
|
||||
const specsDir = path.join(testDir, 'openspec', 'specs');
|
||||
const bin = path.join(projectRoot, 'bin', 'openspec.js');
|
||||
|
||||
beforeAll(() => {
|
||||
execSync('pnpm -s build', { stdio: 'pipe' });
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await fs.mkdir(changesDir, { recursive: true });
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { ChangeCommand } from '../../../src/commands/change.js';
|
||||
|
||||
// These tests assume the repository's own openspec/changes directory exists
|
||||
// and contains at least one active change (e.g., add-change-commands)
|
||||
import path from 'path';
|
||||
import { promises as fs } from 'fs';
|
||||
import os from 'os';
|
||||
|
||||
describe('ChangeCommand.list', () => {
|
||||
let cmd: ChangeCommand;
|
||||
let tempRoot: string;
|
||||
let originalCwd: string;
|
||||
|
||||
beforeAll(() => {
|
||||
beforeAll(async () => {
|
||||
cmd = new ChangeCommand();
|
||||
originalCwd = process.cwd();
|
||||
tempRoot = path.join(os.tmpdir(), `openspec-change-command-list-${Date.now()}`);
|
||||
const changeDir = path.join(tempRoot, 'openspec', 'changes', 'demo');
|
||||
await fs.mkdir(changeDir, { recursive: true });
|
||||
const proposal = `# Change: Demo\n\n## Why\nTest list.\n\n## What Changes\n- **auth:** Add requirement`;
|
||||
await fs.writeFile(path.join(changeDir, 'proposal.md'), proposal, 'utf-8');
|
||||
await fs.writeFile(path.join(changeDir, 'tasks.md'), '- [x] Task 1\n- [ ] Task 2\n', 'utf-8');
|
||||
process.chdir(tempRoot);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
process.chdir(originalCwd);
|
||||
await fs.rm(tempRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('returns JSON with expected shape', async () => {
|
||||
|
||||
@@ -1,34 +1,33 @@
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { ChangeCommand } from '../../../src/commands/change.js';
|
||||
import path from 'path';
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
async function findSingleActiveChange(root: string): Promise<string | undefined> {
|
||||
const changesDir = path.join(root, 'openspec', 'changes');
|
||||
try {
|
||||
const entries = await fs.readdir(changesDir, { withFileTypes: true });
|
||||
const names = entries
|
||||
.filter((e) => e.isDirectory() && e.name !== 'archive')
|
||||
.map((e) => e.name);
|
||||
if (names.length === 1) return names[0];
|
||||
return names.includes('add-change-commands') ? 'add-change-commands' : names[0];
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
import os from 'os';
|
||||
|
||||
describe('ChangeCommand.show/validate', () => {
|
||||
let cmd: ChangeCommand;
|
||||
let changeName: string | undefined;
|
||||
let changeName: string;
|
||||
let tempRoot: string;
|
||||
let originalCwd: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
cmd = new ChangeCommand();
|
||||
changeName = await findSingleActiveChange(process.cwd());
|
||||
originalCwd = process.cwd();
|
||||
tempRoot = path.join(os.tmpdir(), `openspec-change-command-${Date.now()}`);
|
||||
const changesDir = path.join(tempRoot, 'openspec', 'changes', 'sample-change');
|
||||
await fs.mkdir(changesDir, { recursive: true });
|
||||
const proposal = `# Change: Sample Change\n\n## Why\nConsistency in tests.\n\n## What Changes\n- **auth:** Add requirement`;
|
||||
await fs.writeFile(path.join(changesDir, 'proposal.md'), proposal, 'utf-8');
|
||||
process.chdir(tempRoot);
|
||||
changeName = 'sample-change';
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
process.chdir(originalCwd);
|
||||
await fs.rm(tempRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('show --json prints JSON including deltas', async () => {
|
||||
if (!changeName) return; // skip if no changes present
|
||||
|
||||
const logs: string[] = [];
|
||||
const origLog = console.log;
|
||||
try {
|
||||
@@ -67,8 +66,6 @@ describe('ChangeCommand.show/validate', () => {
|
||||
});
|
||||
|
||||
it('show --json --requirements-only returns minimal object with deltas (deprecated alias)', async () => {
|
||||
if (!changeName) return; // skip if no changes present
|
||||
|
||||
const logs: string[] = [];
|
||||
const origLog = console.log;
|
||||
try {
|
||||
@@ -93,8 +90,6 @@ describe('ChangeCommand.show/validate', () => {
|
||||
});
|
||||
|
||||
it('validate --strict --json returns a report with valid boolean', async () => {
|
||||
if (!changeName) return; // skip if no changes present
|
||||
|
||||
const logs: string[] = [];
|
||||
const origLog = console.log;
|
||||
try {
|
||||
|
||||
+4
-1
@@ -4,6 +4,9 @@ export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
globalSetup: './vitest.setup.ts',
|
||||
// Keep default pool settings; some tests rely on process.chdir,
|
||||
// which is not supported in worker threads
|
||||
include: ['test/**/*.test.ts'],
|
||||
coverage: {
|
||||
reporter: ['text', 'json', 'html'],
|
||||
@@ -19,4 +22,4 @@ export default defineConfig({
|
||||
testTimeout: 10000,
|
||||
hookTimeout: 10000
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { execSync } from 'child_process';
|
||||
import { existsSync } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
// Run once before all tests
|
||||
export async function setup() {
|
||||
const distPath = path.join(process.cwd(), 'dist', 'cli', 'index.js');
|
||||
|
||||
if (!existsSync(distPath)) {
|
||||
console.log('Building project before tests...');
|
||||
try {
|
||||
execSync('pnpm run build', {
|
||||
stdio: 'inherit',
|
||||
cwd: process.cwd()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to build project:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user