mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-04 06:18:24 +08:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e395eb4eeb | ||
|
|
76e1ec2a1f | ||
|
|
27eaccc024 | ||
|
|
b288f2fc88 | ||
|
|
22134a603b | ||
|
|
3b5fd11cb9 | ||
|
|
e9417fc147 | ||
|
|
8bcf2c6905 | ||
|
|
9a03ba1853 |
@@ -0,0 +1,20 @@
|
||||
# Add List Command to OpenSpec CLI
|
||||
|
||||
## Why
|
||||
|
||||
Developers need visibility into available changes and their status to understand the project's evolution and pending work.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add `openspec list` command that displays all changes in the changes/ directory
|
||||
- Show each change name with task completion count (e.g., "add-auth: 3/5 tasks")
|
||||
- Display completion status indicator (✓ for fully complete, progress for partial)
|
||||
- Skip the archive/ subdirectory to focus on active changes
|
||||
- Simple table output for easy scanning
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs: New capability `cli-list` will be added
|
||||
- Affected code:
|
||||
- `src/cli/index.ts` - Add list command
|
||||
- `src/core/list.ts` - New file with directory scanning and task parsing (~60 lines)
|
||||
@@ -0,0 +1,69 @@
|
||||
# List Command Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
The `openspec list` command SHALL provide developers with a quick overview of all active changes in the project, showing their names and task completion status.
|
||||
|
||||
## Behavior
|
||||
|
||||
### Command Execution
|
||||
|
||||
WHEN `openspec list` is executed
|
||||
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
|
||||
|
||||
### Task Counting
|
||||
|
||||
WHEN parsing a `tasks.md` file
|
||||
THEN count tasks matching these patterns:
|
||||
- Completed: Lines containing `- [x]`
|
||||
- Incomplete: Lines containing `- [ ]`
|
||||
AND calculate total tasks as the sum of completed and incomplete
|
||||
|
||||
### Output Format
|
||||
|
||||
WHEN displaying the list
|
||||
THEN show a table with columns:
|
||||
- Change name (directory name)
|
||||
- Task progress (e.g., "3/5 tasks" or "✓ Complete")
|
||||
- Status indicator:
|
||||
- `✓` for fully completed changes (all tasks done)
|
||||
- Progress fraction for partial completion
|
||||
|
||||
Example output:
|
||||
```
|
||||
Changes:
|
||||
add-auth-feature 3/5 tasks
|
||||
update-api-docs ✓ Complete
|
||||
fix-validation 0/2 tasks
|
||||
add-list-command 1/4 tasks
|
||||
```
|
||||
|
||||
### Empty State
|
||||
|
||||
WHEN no active changes exist (only archive/ or empty changes/)
|
||||
THEN display: "No active changes found."
|
||||
|
||||
### Error Handling
|
||||
|
||||
IF a change directory has no `tasks.md` file
|
||||
THEN display the change with "No tasks" status
|
||||
|
||||
IF `openspec/changes/` directory doesn't exist
|
||||
THEN display error: "No OpenSpec changes directory found. Run 'openspec init' first."
|
||||
AND exit with code 1
|
||||
|
||||
### Sorting
|
||||
|
||||
Changes SHALL be displayed in alphabetical order by change name for consistency.
|
||||
|
||||
## Why
|
||||
|
||||
Developers need a quick way to:
|
||||
- See what changes are in progress
|
||||
- Identify which changes are ready to archive
|
||||
- Understand the overall project evolution status
|
||||
- Get a bird's-eye view without opening multiple files
|
||||
|
||||
This command provides that visibility with minimal effort, following OpenSpec's philosophy of simplicity and clarity.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. Core Implementation
|
||||
- [ ] 1.1 Create `src/core/list.ts` with list logic
|
||||
- [ ] 1.1.1 Implement directory scanning (exclude archive/)
|
||||
- [ ] 1.1.2 Implement task counting from tasks.md files
|
||||
- [ ] 1.1.3 Format output as simple table
|
||||
- [ ] 1.2 Add list command to CLI in `src/cli/index.ts`
|
||||
- [ ] 1.2.1 Register `openspec list` command
|
||||
- [ ] 1.2.2 Connect to list.ts implementation
|
||||
|
||||
## 2. Error Handling
|
||||
- [ ] 2.1 Handle missing openspec/changes/ directory
|
||||
- [ ] 2.2 Handle changes without tasks.md files
|
||||
- [ ] 2.3 Handle empty changes directory
|
||||
|
||||
## 3. Testing
|
||||
- [ ] 3.1 Add tests for list functionality
|
||||
- [ ] 3.1.1 Test with multiple changes
|
||||
- [ ] 3.1.2 Test with completed changes
|
||||
- [ ] 3.1.3 Test with no changes
|
||||
- [ ] 3.1.4 Test error conditions
|
||||
|
||||
## 4. Documentation
|
||||
- [ ] 4.1 Update CLI help text with list command
|
||||
- [ ] 4.2 Add list command to README if applicable
|
||||
@@ -0,0 +1,15 @@
|
||||
# Add @requirement Markers for Requirement Identification
|
||||
|
||||
## Why
|
||||
Specs contain WHEN/THEN patterns that define system requirements, but extracting these programmatically requires brittle regex parsing that may miss edge cases or break with formatting changes.
|
||||
|
||||
## What Changes
|
||||
- Define @requirement marker convention for identifying key requirements in specs
|
||||
- Each marker includes a brief identifier (e.g., @requirement user-register)
|
||||
- Markers appear directly before their WHEN/THEN blocks
|
||||
- Document convention in openspec-conventions spec
|
||||
|
||||
## Impact
|
||||
- Affected specs: openspec-conventions (new)
|
||||
- Affected code: None initially - enables future tooling
|
||||
- Breaking changes: None - additive convention only
|
||||
@@ -0,0 +1,219 @@
|
||||
# OpenSpec Conventions Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
OpenSpec conventions SHALL define how system capabilities are documented, how changes are proposed and tracked, and how specifications evolve over time. This meta-specification serves as the source of truth for OpenSpec's own conventions.
|
||||
|
||||
## Core Principles
|
||||
|
||||
The system SHALL follow these principles:
|
||||
- Specs reflect what IS currently built and deployed
|
||||
- Changes contain proposals for what SHOULD be changed
|
||||
- AI drives the documentation process
|
||||
- Specs are living documentation kept in sync with deployed code
|
||||
|
||||
## Directory Structure
|
||||
|
||||
WHEN an OpenSpec project is initialized
|
||||
THEN it SHALL have this structure:
|
||||
```
|
||||
openspec/
|
||||
├── project.md # Project-specific context
|
||||
├── README.md # AI assistant instructions
|
||||
├── specs/ # Current deployed capabilities
|
||||
│ └── [capability]/ # Single, focused capability
|
||||
│ ├── spec.md # WHAT and WHY
|
||||
│ └── design.md # HOW (optional, for established patterns)
|
||||
└── changes/ # Proposed changes
|
||||
├── [change-name]/ # Descriptive change identifier
|
||||
│ ├── proposal.md # Why, what, and impact
|
||||
│ ├── tasks.md # Implementation checklist
|
||||
│ ├── design.md # Technical decisions (optional)
|
||||
│ └── specs/ # Complete future state
|
||||
│ └── [capability]/
|
||||
│ └── spec.md # Clean markdown (no diff syntax)
|
||||
└── archive/ # Completed changes
|
||||
└── YYYY-MM-DD-[name]/
|
||||
```
|
||||
|
||||
## Change Storage Convention
|
||||
|
||||
### Future State Storage
|
||||
|
||||
WHEN creating a change proposal
|
||||
THEN store the complete future state of affected specs
|
||||
AND use clean markdown without diff syntax
|
||||
|
||||
The `changes/[name]/specs/` directory SHALL contain:
|
||||
- Complete spec files as they will exist after the change
|
||||
- Clean markdown without `+` or `-` prefixes
|
||||
- All formatting and structure of the final intended state
|
||||
|
||||
### Proposal Format
|
||||
|
||||
WHEN documenting what changes
|
||||
THEN the proposal SHALL explicitly describe each change:
|
||||
|
||||
```markdown
|
||||
**[Section or Behavior Name]**
|
||||
- From: [current state/requirement]
|
||||
- To: [future state/requirement]
|
||||
- Reason: [why this change is needed]
|
||||
- Impact: [breaking/non-breaking, who's affected]
|
||||
```
|
||||
|
||||
This explicit format compensates for not having inline diffs and ensures reviewers understand exactly what will change.
|
||||
|
||||
## Change Lifecycle
|
||||
|
||||
The change process SHALL follow these states:
|
||||
|
||||
1. **Propose**: AI creates change with future state specs and explicit proposal
|
||||
2. **Review**: Humans review proposal and future state
|
||||
3. **Approve**: Change is approved for implementation
|
||||
4. **Implement**: Follow tasks.md checklist (can span multiple PRs)
|
||||
5. **Deploy**: Changes are deployed to production
|
||||
6. **Update**: Specs in `specs/` are updated to match deployed reality
|
||||
7. **Archive**: Change is moved to `archive/YYYY-MM-DD-[name]/`
|
||||
|
||||
## Viewing Changes
|
||||
|
||||
WHEN reviewing proposed changes
|
||||
THEN reviewers can compare using:
|
||||
- GitHub PR diff view when changes are committed
|
||||
- Command line: `diff -u specs/[capability]/spec.md changes/[name]/specs/[capability]/spec.md`
|
||||
- Any visual diff tool comparing current vs future state
|
||||
|
||||
The system relies on tools to generate diffs rather than storing them.
|
||||
|
||||
## Capability Naming
|
||||
|
||||
Capabilities SHALL use:
|
||||
- Verb-noun patterns (e.g., `user-auth`, `payment-capture`)
|
||||
- Hyphenated lowercase names
|
||||
- Singular focus (one responsibility per capability)
|
||||
- No nesting (flat structure under `specs/`)
|
||||
|
||||
## When Changes Require Proposals
|
||||
|
||||
A proposal SHALL be created for:
|
||||
- New features or capabilities
|
||||
- Breaking changes to existing behavior
|
||||
- Architecture or pattern changes
|
||||
- Performance optimizations that change behavior
|
||||
- Security updates affecting access patterns
|
||||
|
||||
A proposal is NOT required for:
|
||||
- Bug fixes restoring intended behavior
|
||||
- Typos or formatting fixes
|
||||
- Non-breaking dependency updates
|
||||
- Adding tests for existing behavior
|
||||
- Documentation clarifications
|
||||
|
||||
## Requirement Markers
|
||||
|
||||
### Marker Syntax
|
||||
|
||||
@requirement marker-syntax
|
||||
WHEN writing a requirement in a spec
|
||||
THEN prefix it with @requirement followed by a brief kebab-case identifier
|
||||
AND place the marker on the line immediately before the WHEN statement
|
||||
|
||||
@requirement marker-identifier
|
||||
WHEN choosing an identifier for @requirement
|
||||
THEN use kebab-case (lowercase with hyphens)
|
||||
AND keep it brief but descriptive (2-4 words)
|
||||
AND ensure it's unique within the spec
|
||||
|
||||
@requirement marker-placement
|
||||
WHEN adding @requirement markers to a spec
|
||||
THEN place them in the ## Behavior or ## Behaviors section
|
||||
AND ensure each WHEN/THEN block has exactly one marker
|
||||
AND maintain a blank line after each THEN block for readability
|
||||
|
||||
### Examples
|
||||
|
||||
@requirement valid-marker-example
|
||||
WHEN a spec includes properly formatted markers
|
||||
THEN tools can extract and identify requirements programmatically
|
||||
AND the spec remains human-readable
|
||||
|
||||
Example of correct usage:
|
||||
```markdown
|
||||
## Behavior
|
||||
|
||||
@requirement user-register
|
||||
WHEN user registers with valid email
|
||||
THEN create account and send confirmation
|
||||
|
||||
@requirement user-login
|
||||
WHEN user logs in with correct credentials
|
||||
THEN return JWT token with user data
|
||||
|
||||
@requirement invalid-credentials
|
||||
WHEN user provides invalid credentials
|
||||
THEN return 401 unauthorized error
|
||||
```
|
||||
|
||||
@requirement invalid-marker-detection
|
||||
WHEN a requirement lacks an @requirement marker
|
||||
THEN tools should gracefully skip it
|
||||
AND optionally warn about unmarked requirements
|
||||
|
||||
### Edge Cases
|
||||
|
||||
@requirement multiline-when-then
|
||||
WHEN a WHEN or THEN clause spans multiple lines
|
||||
THEN the @requirement marker still goes on the line before WHEN
|
||||
AND the entire block is considered part of that requirement
|
||||
|
||||
@requirement multiple-then-clauses
|
||||
WHEN a requirement has multiple THEN clauses using AND
|
||||
THEN treat them as part of the same requirement
|
||||
AND use a single @requirement marker for the entire block
|
||||
|
||||
@requirement nested-conditions
|
||||
WHEN requirements have nested conditions or complex logic
|
||||
THEN keep the @requirement marker simple
|
||||
AND let the WHEN/THEN content contain the complexity
|
||||
|
||||
## Spec Structure
|
||||
|
||||
@requirement spec-file-location
|
||||
WHEN creating a spec file
|
||||
THEN place it in openspec/specs/[capability-name]/spec.md
|
||||
AND use kebab-case for the capability name
|
||||
|
||||
@requirement spec-sections
|
||||
WHEN structuring a spec
|
||||
THEN include these sections in order:
|
||||
- # [Capability Name] Specification
|
||||
- ## Purpose (brief description)
|
||||
- ## Behavior or ## Behaviors (with @requirement markers)
|
||||
- ## Examples (optional, for complex requirements)
|
||||
|
||||
## Benefits of Requirement Markers
|
||||
|
||||
@requirement tooling-extraction
|
||||
WHEN tools need to extract requirements from specs
|
||||
THEN they can parse @requirement markers reliably
|
||||
AND avoid complex regex patterns for WHEN/THEN extraction
|
||||
|
||||
@requirement requirement-counting
|
||||
WHEN displaying change summaries
|
||||
THEN tools can count requirements by counting @requirement markers
|
||||
AND show accurate requirement counts per spec
|
||||
|
||||
@requirement requirement-referencing
|
||||
WHEN documenting or discussing specific requirements
|
||||
THEN use the @requirement identifier for clear reference
|
||||
AND maintain consistency across documentation
|
||||
|
||||
## Why This Approach
|
||||
|
||||
Clean future state storage provides:
|
||||
- **Readability**: No diff syntax pollution
|
||||
- **AI-compatibility**: Standard markdown that AI tools understand
|
||||
- **Simplicity**: No special parsing or processing needed
|
||||
- **Tool-agnostic**: Any diff tool can show changes
|
||||
- **Clear intent**: Explicit proposals document reasoning
|
||||
@@ -0,0 +1,24 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. Define Convention
|
||||
- [ ] 1.1 Document @requirement marker syntax
|
||||
- [ ] 1.2 Define identifier naming guidelines
|
||||
- [ ] 1.3 Specify marker placement rules
|
||||
- [ ] 1.4 Add examples of proper usage
|
||||
|
||||
## 2. Create Specification
|
||||
- [ ] 2.1 Write openspec-conventions spec
|
||||
- [ ] 2.2 Include requirement marker section
|
||||
- [ ] 2.3 Add good and bad examples
|
||||
- [ ] 2.4 Document edge cases
|
||||
|
||||
## 3. Update Existing Specs
|
||||
- [ ] 3.1 Add @requirement markers to cli-init spec
|
||||
- [ ] 3.2 Add @requirement markers to cli-update spec
|
||||
- [ ] 3.3 Add @requirement markers to cli-view spec
|
||||
- [ ] 3.4 Review and update any other existing specs
|
||||
|
||||
## 4. Documentation
|
||||
- [ ] 4.1 Update README with marker convention
|
||||
- [ ] 4.2 Add marker usage to CLAUDE.md
|
||||
- [ ] 4.3 Create examples for AI assistants
|
||||
@@ -5,6 +5,7 @@ import { promises as fs } from 'fs';
|
||||
import { InitCommand } from '../core/init.js';
|
||||
import { UpdateCommand } from '../core/update.js';
|
||||
import { DiffCommand } from '../core/diff.js';
|
||||
import { ListCommand } from '../core/list.js';
|
||||
|
||||
const program = new Command();
|
||||
|
||||
@@ -75,4 +76,18 @@ program
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('list')
|
||||
.description('List all active changes with their task status')
|
||||
.action(async () => {
|
||||
try {
|
||||
const listCommand = new ListCommand();
|
||||
await listCommand.execute();
|
||||
} catch (error) {
|
||||
console.log(); // Empty line for spacing
|
||||
ora().fail(`Error: ${(error as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
program.parse();
|
||||
@@ -0,0 +1,90 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
interface ChangeInfo {
|
||||
name: string;
|
||||
completedTasks: number;
|
||||
totalTasks: number;
|
||||
}
|
||||
|
||||
export class ListCommand {
|
||||
async execute(targetPath: string = '.'): Promise<void> {
|
||||
const changesDir = path.join(targetPath, 'openspec', 'changes');
|
||||
|
||||
// Check if changes directory exists
|
||||
try {
|
||||
await fs.access(changesDir);
|
||||
} catch {
|
||||
throw new Error("No OpenSpec changes directory found. Run 'openspec init' first.");
|
||||
}
|
||||
|
||||
// Get all directories in changes (excluding archive)
|
||||
const entries = await fs.readdir(changesDir, { withFileTypes: true });
|
||||
const changeDirs = entries
|
||||
.filter(entry => entry.isDirectory() && entry.name !== 'archive')
|
||||
.map(entry => entry.name);
|
||||
|
||||
if (changeDirs.length === 0) {
|
||||
console.log('No active changes found.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect information about each change
|
||||
const changes: ChangeInfo[] = [];
|
||||
|
||||
for (const changeDir of changeDirs) {
|
||||
const tasksPath = path.join(changesDir, changeDir, 'tasks.md');
|
||||
let completedTasks = 0;
|
||||
let incompleteTasks = 0;
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(tasksPath, 'utf-8');
|
||||
const lines = content.split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.includes('- [x]')) {
|
||||
completedTasks++;
|
||||
} else if (line.includes('- [ ]')) {
|
||||
incompleteTasks++;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// No tasks.md file
|
||||
changes.push({
|
||||
name: changeDir,
|
||||
completedTasks: 0,
|
||||
totalTasks: 0
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
changes.push({
|
||||
name: changeDir,
|
||||
completedTasks,
|
||||
totalTasks: completedTasks + incompleteTasks
|
||||
});
|
||||
}
|
||||
|
||||
// Sort alphabetically by name
|
||||
changes.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
// Display results
|
||||
console.log('Changes:');
|
||||
for (const change of changes) {
|
||||
const padding = ' ';
|
||||
const nameWidth = Math.max(...changes.map(c => c.name.length));
|
||||
const paddedName = change.name.padEnd(nameWidth);
|
||||
|
||||
let status: string;
|
||||
if (change.totalTasks === 0) {
|
||||
status = 'No tasks';
|
||||
} else if (change.completedTasks === change.totalTasks) {
|
||||
status = '✓ Complete';
|
||||
} else {
|
||||
status = `${change.completedTasks}/${change.totalTasks} tasks`;
|
||||
}
|
||||
|
||||
console.log(`${padding}${paddedName} ${status}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { ListCommand } from '../../src/core/list.js';
|
||||
|
||||
describe('ListCommand', () => {
|
||||
let tempDir: string;
|
||||
let originalLog: typeof console.log;
|
||||
let logOutput: string[] = [];
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create temp directory
|
||||
tempDir = path.join(os.tmpdir(), `openspec-list-test-${Date.now()}`);
|
||||
await fs.mkdir(tempDir, { recursive: true });
|
||||
|
||||
// Mock console.log to capture output
|
||||
originalLog = console.log;
|
||||
console.log = (...args: any[]) => {
|
||||
logOutput.push(args.join(' '));
|
||||
};
|
||||
logOutput = [];
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Restore console.log
|
||||
console.log = originalLog;
|
||||
|
||||
// Clean up temp directory
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should handle missing openspec/changes directory', async () => {
|
||||
const listCommand = new ListCommand();
|
||||
|
||||
await expect(listCommand.execute(tempDir)).rejects.toThrow(
|
||||
"No OpenSpec changes directory found. Run 'openspec init' first."
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty changes directory', async () => {
|
||||
const changesDir = path.join(tempDir, 'openspec', 'changes');
|
||||
await fs.mkdir(changesDir, { recursive: true });
|
||||
|
||||
const listCommand = new ListCommand();
|
||||
await listCommand.execute(tempDir);
|
||||
|
||||
expect(logOutput).toEqual(['No active changes found.']);
|
||||
});
|
||||
|
||||
it('should exclude archive directory', async () => {
|
||||
const changesDir = path.join(tempDir, 'openspec', 'changes');
|
||||
await fs.mkdir(path.join(changesDir, 'archive'), { recursive: true });
|
||||
await fs.mkdir(path.join(changesDir, 'my-change'), { recursive: true });
|
||||
|
||||
// Create tasks.md with some tasks
|
||||
await fs.writeFile(
|
||||
path.join(changesDir, 'my-change', 'tasks.md'),
|
||||
'- [x] Task 1\n- [ ] Task 2\n'
|
||||
);
|
||||
|
||||
const listCommand = new ListCommand();
|
||||
await listCommand.execute(tempDir);
|
||||
|
||||
expect(logOutput).toContain('Changes:');
|
||||
expect(logOutput.some(line => line.includes('my-change'))).toBe(true);
|
||||
expect(logOutput.some(line => line.includes('archive'))).toBe(false);
|
||||
});
|
||||
|
||||
it('should count tasks correctly', async () => {
|
||||
const changesDir = path.join(tempDir, 'openspec', 'changes');
|
||||
await fs.mkdir(path.join(changesDir, 'test-change'), { recursive: true });
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(changesDir, 'test-change', 'tasks.md'),
|
||||
`# Tasks
|
||||
- [x] Completed task 1
|
||||
- [x] Completed task 2
|
||||
- [ ] Incomplete task 1
|
||||
- [ ] Incomplete task 2
|
||||
- [ ] Incomplete task 3
|
||||
Regular text that should be ignored
|
||||
`
|
||||
);
|
||||
|
||||
const listCommand = new ListCommand();
|
||||
await listCommand.execute(tempDir);
|
||||
|
||||
expect(logOutput.some(line => line.includes('2/5 tasks'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should show complete status for fully completed changes', async () => {
|
||||
const changesDir = path.join(tempDir, 'openspec', 'changes');
|
||||
await fs.mkdir(path.join(changesDir, 'completed-change'), { recursive: true });
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(changesDir, 'completed-change', 'tasks.md'),
|
||||
'- [x] Task 1\n- [x] Task 2\n- [x] Task 3\n'
|
||||
);
|
||||
|
||||
const listCommand = new ListCommand();
|
||||
await listCommand.execute(tempDir);
|
||||
|
||||
expect(logOutput.some(line => line.includes('✓ Complete'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle changes without tasks.md', async () => {
|
||||
const changesDir = path.join(tempDir, 'openspec', 'changes');
|
||||
await fs.mkdir(path.join(changesDir, 'no-tasks'), { recursive: true });
|
||||
|
||||
const listCommand = new ListCommand();
|
||||
await listCommand.execute(tempDir);
|
||||
|
||||
expect(logOutput.some(line => line.includes('no-tasks') && line.includes('No tasks'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should sort changes alphabetically', async () => {
|
||||
const changesDir = path.join(tempDir, 'openspec', 'changes');
|
||||
await fs.mkdir(path.join(changesDir, 'zebra'), { recursive: true });
|
||||
await fs.mkdir(path.join(changesDir, 'alpha'), { recursive: true });
|
||||
await fs.mkdir(path.join(changesDir, 'middle'), { recursive: true });
|
||||
|
||||
const listCommand = new ListCommand();
|
||||
await listCommand.execute(tempDir);
|
||||
|
||||
const changeLines = logOutput.filter(line =>
|
||||
line.includes('alpha') || line.includes('middle') || line.includes('zebra')
|
||||
);
|
||||
|
||||
expect(changeLines[0]).toContain('alpha');
|
||||
expect(changeLines[1]).toContain('middle');
|
||||
expect(changeLines[2]).toContain('zebra');
|
||||
});
|
||||
|
||||
it('should handle multiple changes with various states', async () => {
|
||||
const changesDir = path.join(tempDir, 'openspec', 'changes');
|
||||
|
||||
// Complete change
|
||||
await fs.mkdir(path.join(changesDir, 'completed'), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(changesDir, 'completed', 'tasks.md'),
|
||||
'- [x] Task 1\n- [x] Task 2\n'
|
||||
);
|
||||
|
||||
// Partial change
|
||||
await fs.mkdir(path.join(changesDir, 'partial'), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(changesDir, 'partial', 'tasks.md'),
|
||||
'- [x] Done\n- [ ] Not done\n- [ ] Also not done\n'
|
||||
);
|
||||
|
||||
// No tasks
|
||||
await fs.mkdir(path.join(changesDir, 'no-tasks'), { recursive: true });
|
||||
|
||||
const listCommand = new ListCommand();
|
||||
await listCommand.execute(tempDir);
|
||||
|
||||
expect(logOutput).toContain('Changes:');
|
||||
expect(logOutput.some(line => line.includes('completed') && line.includes('✓ Complete'))).toBe(true);
|
||||
expect(logOutput.some(line => line.includes('partial') && line.includes('1/3 tasks'))).toBe(true);
|
||||
expect(logOutput.some(line => line.includes('no-tasks') && line.includes('No tasks'))).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user