@lumenflow/cli 2.11.0 → 2.13.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/README.md +6 -17
- package/dist/__tests__/commands.test.js +198 -2
- package/dist/__tests__/init-docs-structure.test.js +33 -0
- package/dist/__tests__/initiative-add-wu.test.js +71 -1
- package/dist/__tests__/no-beacon-references-docs.test.js +30 -0
- package/dist/__tests__/no-beacon-references.test.js +39 -0
- package/dist/__tests__/wu-create.test.js +50 -1
- package/dist/__tests__/wu-done-docs-only-policy.test.js +20 -0
- package/dist/__tests__/wu-prep-default-exec.test.js +35 -0
- package/dist/__tests__/wu-prep.test.js +32 -0
- package/dist/__tests__/wu-validate.test.js +36 -0
- package/dist/agent-issues-query.js +1 -0
- package/dist/agent-issues-query.js.map +1 -0
- package/dist/agent-log-issue.js +1 -0
- package/dist/agent-log-issue.js.map +1 -0
- package/dist/agent-session-end.js +1 -0
- package/dist/agent-session-end.js.map +1 -0
- package/dist/agent-session.js +1 -0
- package/dist/agent-session.js.map +1 -0
- package/dist/backlog-prune.js +2 -0
- package/dist/backlog-prune.js.map +1 -0
- package/dist/cli-entry-point.js +1 -0
- package/dist/cli-entry-point.js.map +1 -0
- package/dist/commands/integrate.js +1 -0
- package/dist/commands/integrate.js.map +1 -0
- package/dist/commands.js +56 -77
- package/dist/commands.js.map +1 -0
- package/dist/deps-add.js +1 -0
- package/dist/deps-add.js.map +1 -0
- package/dist/deps-remove.js +1 -0
- package/dist/deps-remove.js.map +1 -0
- package/dist/docs-sync.js +1 -0
- package/dist/docs-sync.js.map +1 -0
- package/dist/doctor.js +1 -0
- package/dist/doctor.js.map +1 -0
- package/dist/file-delete.js +1 -0
- package/dist/file-delete.js.map +1 -0
- package/dist/file-edit.js +1 -0
- package/dist/file-edit.js.map +1 -0
- package/dist/file-read.js +1 -0
- package/dist/file-read.js.map +1 -0
- package/dist/file-write.js +1 -0
- package/dist/file-write.js.map +1 -0
- package/dist/flow-bottlenecks.js +1 -0
- package/dist/flow-bottlenecks.js.map +1 -0
- package/dist/flow-report.js +1 -0
- package/dist/flow-report.js.map +1 -0
- package/dist/gates.js +26 -12
- package/dist/gates.js.map +1 -0
- package/dist/git-branch.js +1 -0
- package/dist/git-branch.js.map +1 -0
- package/dist/git-diff.js +1 -0
- package/dist/git-diff.js.map +1 -0
- package/dist/git-log.js +1 -0
- package/dist/git-log.js.map +1 -0
- package/dist/git-status.js +1 -0
- package/dist/git-status.js.map +1 -0
- package/dist/guard-locked.js +1 -0
- package/dist/guard-locked.js.map +1 -0
- package/dist/guard-main-branch.js +1 -0
- package/dist/guard-main-branch.js.map +1 -0
- package/dist/guard-worktree-commit.js +1 -0
- package/dist/guard-worktree-commit.js.map +1 -0
- package/dist/hooks/auto-checkpoint-utils.js +52 -0
- package/dist/hooks/auto-checkpoint-utils.js.map +1 -0
- package/dist/hooks/enforcement-checks.js +1 -0
- package/dist/hooks/enforcement-checks.js.map +1 -0
- package/dist/hooks/enforcement-generator.js +185 -1
- package/dist/hooks/enforcement-generator.js.map +1 -0
- package/dist/hooks/enforcement-sync.js +91 -1
- package/dist/hooks/enforcement-sync.js.map +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -0
- package/dist/init.js +70 -36
- package/dist/init.js.map +1 -0
- package/dist/initiative-add-wu.js +180 -59
- package/dist/initiative-add-wu.js.map +1 -0
- package/dist/initiative-bulk-assign-wus.js +3 -1
- package/dist/initiative-bulk-assign-wus.js.map +1 -0
- package/dist/initiative-create.js +1 -0
- package/dist/initiative-create.js.map +1 -0
- package/dist/initiative-edit.js +70 -34
- package/dist/initiative-edit.js.map +1 -0
- package/dist/initiative-list.js +1 -0
- package/dist/initiative-list.js.map +1 -0
- package/dist/initiative-plan.js +1 -0
- package/dist/initiative-plan.js.map +1 -0
- package/dist/initiative-remove-wu.js +1 -0
- package/dist/initiative-remove-wu.js.map +1 -0
- package/dist/initiative-status.js +1 -0
- package/dist/initiative-status.js.map +1 -0
- package/dist/lane-health.js +1 -0
- package/dist/lane-health.js.map +1 -0
- package/dist/lane-suggest.js +1 -0
- package/dist/lane-suggest.js.map +1 -0
- package/dist/lumenflow-upgrade.js +1 -0
- package/dist/lumenflow-upgrade.js.map +1 -0
- package/dist/mem-checkpoint.js +1 -0
- package/dist/mem-checkpoint.js.map +1 -0
- package/dist/mem-cleanup.js +114 -1
- package/dist/mem-cleanup.js.map +1 -0
- package/dist/mem-context.js +1 -0
- package/dist/mem-context.js.map +1 -0
- package/dist/mem-create.js +1 -0
- package/dist/mem-create.js.map +1 -0
- package/dist/mem-delete.js +1 -0
- package/dist/mem-delete.js.map +1 -0
- package/dist/mem-export.js +1 -0
- package/dist/mem-export.js.map +1 -0
- package/dist/mem-inbox.js +1 -0
- package/dist/mem-inbox.js.map +1 -0
- package/dist/mem-index.js +1 -0
- package/dist/mem-index.js.map +1 -0
- package/dist/mem-init.js +1 -0
- package/dist/mem-init.js.map +1 -0
- package/dist/mem-profile.js +1 -0
- package/dist/mem-profile.js.map +1 -0
- package/dist/mem-promote.js +1 -0
- package/dist/mem-promote.js.map +1 -0
- package/dist/mem-ready.js +1 -0
- package/dist/mem-ready.js.map +1 -0
- package/dist/mem-recover.js +1 -0
- package/dist/mem-recover.js.map +1 -0
- package/dist/mem-signal.js +12 -1
- package/dist/mem-signal.js.map +1 -0
- package/dist/mem-start.js +1 -0
- package/dist/mem-start.js.map +1 -0
- package/dist/mem-summarize.js +1 -0
- package/dist/mem-summarize.js.map +1 -0
- package/dist/mem-triage.js +2 -1
- package/dist/mem-triage.js.map +1 -0
- package/dist/merge-block.js +1 -0
- package/dist/merge-block.js.map +1 -0
- package/dist/metrics-cli.js +1 -0
- package/dist/metrics-cli.js.map +1 -0
- package/dist/metrics-snapshot.js +1 -0
- package/dist/metrics-snapshot.js.map +1 -0
- package/dist/onboarding-smoke-test.js +1 -0
- package/dist/onboarding-smoke-test.js.map +1 -0
- package/dist/orchestrate-init-status.js +1 -0
- package/dist/orchestrate-init-status.js.map +1 -0
- package/dist/orchestrate-initiative.js +1 -0
- package/dist/orchestrate-initiative.js.map +1 -0
- package/dist/orchestrate-monitor.js +1 -0
- package/dist/orchestrate-monitor.js.map +1 -0
- package/dist/plan-create.js +1 -0
- package/dist/plan-create.js.map +1 -0
- package/dist/plan-edit.js +1 -0
- package/dist/plan-edit.js.map +1 -0
- package/dist/plan-link.js +1 -0
- package/dist/plan-link.js.map +1 -0
- package/dist/plan-promote.js +1 -0
- package/dist/plan-promote.js.map +1 -0
- package/dist/public-manifest.js +773 -0
- package/dist/public-manifest.js.map +1 -0
- package/dist/release.js +3 -2
- package/dist/release.js.map +1 -0
- package/dist/rotate-progress.js +2 -1
- package/dist/rotate-progress.js.map +1 -0
- package/dist/session-coordinator.js +1 -0
- package/dist/session-coordinator.js.map +1 -0
- package/dist/shared-validators.js +78 -0
- package/dist/shared-validators.js.map +1 -0
- package/dist/signal-cleanup.js +1 -0
- package/dist/signal-cleanup.js.map +1 -0
- package/dist/spawn-list.js +1 -0
- package/dist/spawn-list.js.map +1 -0
- package/dist/state-bootstrap.js +1 -0
- package/dist/state-bootstrap.js.map +1 -0
- package/dist/state-cleanup.js +1 -0
- package/dist/state-cleanup.js.map +1 -0
- package/dist/state-doctor-fix.js +1 -0
- package/dist/state-doctor-fix.js.map +1 -0
- package/dist/state-doctor.js +1 -0
- package/dist/state-doctor.js.map +1 -0
- package/dist/sync-templates.js +1 -0
- package/dist/sync-templates.js.map +1 -0
- package/dist/trace-gen.js +1 -0
- package/dist/trace-gen.js.map +1 -0
- package/dist/validate-agent-skills.js +1 -0
- package/dist/validate-agent-skills.js.map +1 -0
- package/dist/validate-agent-sync.js +1 -0
- package/dist/validate-agent-sync.js.map +1 -0
- package/dist/validate-backlog-sync.js +1 -0
- package/dist/validate-backlog-sync.js.map +1 -0
- package/dist/validate-skills-spec.js +1 -0
- package/dist/validate-skills-spec.js.map +1 -0
- package/dist/validate.js +1 -0
- package/dist/validate.js.map +1 -0
- package/dist/wu-block.js +1 -0
- package/dist/wu-block.js.map +1 -0
- package/dist/wu-claim-mode.js +80 -0
- package/dist/wu-claim-mode.js.map +1 -0
- package/dist/wu-claim-repair-guidance.js +10 -0
- package/dist/wu-claim-repair-guidance.js.map +1 -0
- package/dist/wu-claim.js +71 -9
- package/dist/wu-claim.js.map +1 -0
- package/dist/wu-cleanup.js +2 -1
- package/dist/wu-cleanup.js.map +1 -0
- package/dist/wu-create.js +58 -14
- package/dist/wu-create.js.map +1 -0
- package/dist/wu-delete.js +3 -2
- package/dist/wu-delete.js.map +1 -0
- package/dist/wu-deps.js +2 -1
- package/dist/wu-deps.js.map +1 -0
- package/dist/wu-done-auto-cleanup.js +1 -0
- package/dist/wu-done-auto-cleanup.js.map +1 -0
- package/dist/wu-done-check.js +1 -0
- package/dist/wu-done-check.js.map +1 -0
- package/dist/wu-done-decay.js +88 -0
- package/dist/wu-done-decay.js.map +1 -0
- package/dist/wu-done.js +92 -21
- package/dist/wu-done.js.map +1 -0
- package/dist/wu-edit.js +7 -5
- package/dist/wu-edit.js.map +1 -0
- package/dist/wu-infer-lane.js +1 -0
- package/dist/wu-infer-lane.js.map +1 -0
- package/dist/wu-preflight.js +1 -0
- package/dist/wu-preflight.js.map +1 -0
- package/dist/wu-prep.js +227 -36
- package/dist/wu-prep.js.map +1 -0
- package/dist/wu-proto.js +12 -9
- package/dist/wu-proto.js.map +1 -0
- package/dist/wu-prune.js +1 -0
- package/dist/wu-prune.js.map +1 -0
- package/dist/wu-recover.js +1 -0
- package/dist/wu-recover.js.map +1 -0
- package/dist/wu-release.js +2 -1
- package/dist/wu-release.js.map +1 -0
- package/dist/wu-repair.js +1 -0
- package/dist/wu-repair.js.map +1 -0
- package/dist/wu-spawn-completion.js +1 -0
- package/dist/wu-spawn-completion.js.map +1 -0
- package/dist/wu-spawn.js +1 -0
- package/dist/wu-spawn.js.map +1 -0
- package/dist/wu-status.js +1 -0
- package/dist/wu-status.js.map +1 -0
- package/dist/wu-unblock.js +1 -0
- package/dist/wu-unblock.js.map +1 -0
- package/dist/wu-unlock-lane.js +1 -0
- package/dist/wu-unlock-lane.js.map +1 -0
- package/dist/wu-validate.js +58 -9
- package/dist/wu-validate.js.map +1 -0
- package/package.json +11 -21
- package/templates/core/.husky/pre-commit.template +5 -5
- package/templates/core/LUMENFLOW.md.template +2 -2
- package/templates/core/ai/onboarding/agent-safety-card.md.template +6 -6
- package/templates/core/ai/onboarding/lumenflow-force-usage.md.template +4 -4
package/README.md
CHANGED
|
@@ -101,7 +101,7 @@ This package provides CLI commands for the LumenFlow workflow framework, includi
|
|
|
101
101
|
|
|
102
102
|
| Command | Description |
|
|
103
103
|
| ---------------------------- | ------------------------------------------------------------------ |
|
|
104
|
-
| `initiative-add-wu` | Link
|
|
104
|
+
| `initiative-add-wu` | Link one or more WUs to an initiative bidirectionally |
|
|
105
105
|
| `initiative-bulk-assign-wus` | Bulk-assign orphaned WUs to initiatives based on lane prefix rules |
|
|
106
106
|
| `initiative-create` | Create a new Initiative with micro-worktree isolation (race-safe) |
|
|
107
107
|
| `initiative-edit` | Edit Initiative YAML files with micro-worktree isolation |
|
|
@@ -131,21 +131,15 @@ This package provides CLI commands for the LumenFlow workflow framework, includi
|
|
|
131
131
|
|
|
132
132
|
### Verification & Gates
|
|
133
133
|
|
|
134
|
-
| Command
|
|
135
|
-
|
|
|
136
|
-
| `gates`
|
|
137
|
-
| `lumenflow-gates` | Alias for `gates` command |
|
|
134
|
+
| Command | Description |
|
|
135
|
+
| ------- | ------------------------------------------------------------------------------------------ |
|
|
136
|
+
| `gates` | Run quality gates with support for docs-only mode, incremental linting, and tiered testing |
|
|
138
137
|
|
|
139
138
|
### System & Setup
|
|
140
139
|
|
|
141
140
|
| Command | Description |
|
|
142
141
|
| -------------------------- | --------------------------------------------------------------------------------- |
|
|
143
142
|
| `backlog-prune` | Backlog Prune Command |
|
|
144
|
-
| `deps-add` | Deps Add CLI Command |
|
|
145
|
-
| `deps-remove` | Deps Remove CLI Command |
|
|
146
|
-
| `guard-locked` | |
|
|
147
|
-
| `guard-main-branch` | Guard Main Branch CLI Tool |
|
|
148
|
-
| `guard-worktree-commit` | |
|
|
149
143
|
| `init-plan` | Link a plan file to an initiative |
|
|
150
144
|
| `lumenflow` | Initialize LumenFlow in a project\n\n |
|
|
151
145
|
| `lumenflow-commands` | List all available LumenFlow CLI commands |
|
|
@@ -162,19 +156,12 @@ This package provides CLI commands for the LumenFlow workflow framework, includi
|
|
|
162
156
|
| `plan-edit` | Edit a section in a plan file |
|
|
163
157
|
| `plan-link` | Link a plan file to a WU or initiative |
|
|
164
158
|
| `plan-promote` | Promote a plan to approved status |
|
|
165
|
-
| `rotate-progress` | Rotate Progress CLI Command |
|
|
166
|
-
| `session-coordinator` | Session Coordinator CLI Command |
|
|
167
159
|
| `signal-cleanup` | Prune old signals based on TTL policy to prevent unbounded growth |
|
|
168
160
|
| `state-bootstrap` | State Bootstrap Command |
|
|
169
161
|
| `state-cleanup` | Orchestrate all state cleanup: signals, memory, events |
|
|
170
162
|
| `state-doctor` | Check state integrity and optionally repair issues |
|
|
171
163
|
| `sync-templates` | Sync internal docs to CLI templates for release-cycle maintenance |
|
|
172
|
-
| `trace-gen` | Trace Generator CLI Command |
|
|
173
164
|
| `validate` | |
|
|
174
|
-
| `validate-agent-skills` | |
|
|
175
|
-
| `validate-agent-sync` | |
|
|
176
|
-
| `validate-backlog-sync` | |
|
|
177
|
-
| `validate-skills-spec` | |
|
|
178
165
|
|
|
179
166
|
### File & Git Operations
|
|
180
167
|
|
|
@@ -292,3 +279,5 @@ For detailed upgrade instructions, migration guides, and troubleshooting, see [U
|
|
|
292
279
|
## License
|
|
293
280
|
|
|
294
281
|
Apache-2.0
|
|
282
|
+
|
|
283
|
+
<!-- MODIFIED -->
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file commands.test.ts
|
|
3
3
|
* Tests for lumenflow commands discovery feature (WU-1378)
|
|
4
|
+
* Extended for public CLI manifest alignment (WU-1432)
|
|
4
5
|
*
|
|
5
|
-
* Tests the
|
|
6
|
-
* grouped by category with brief descriptions.
|
|
6
|
+
* Tests the commands subcommand that lists all available CLI commands
|
|
7
|
+
* grouped by category with brief descriptions. Also verifies alignment
|
|
8
|
+
* between public-manifest.ts, commands.ts, and package.json bin entries.
|
|
7
9
|
*/
|
|
8
10
|
import { describe, it, expect } from 'vitest';
|
|
9
11
|
import { getCommandsRegistry, formatCommandsOutput } from '../commands.js';
|
|
12
|
+
import { getPublicManifest, getPublicCommandNames, getPublicBinNames, isPublicCommand, } from '../public-manifest.js';
|
|
13
|
+
import { readFileSync } from 'node:fs';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { dirname, join } from 'node:path';
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const packageJsonPath = join(__dirname, '../../package.json');
|
|
18
|
+
// Script commands that are NOT CLI binaries (pnpm scripts only)
|
|
19
|
+
// These appear in the registry but should NOT be in the manifest
|
|
20
|
+
const SCRIPT_COMMANDS = new Set(['format', 'lint', 'typecheck', 'test', 'setup']);
|
|
10
21
|
describe('lumenflow commands', () => {
|
|
11
22
|
describe('getCommandsRegistry', () => {
|
|
12
23
|
it('should return command categories', () => {
|
|
@@ -73,3 +84,188 @@ describe('lumenflow commands', () => {
|
|
|
73
84
|
});
|
|
74
85
|
});
|
|
75
86
|
});
|
|
87
|
+
// ============================================================================
|
|
88
|
+
// WU-1432: Public CLI Manifest Tests
|
|
89
|
+
// ============================================================================
|
|
90
|
+
describe('public CLI manifest (WU-1432)', () => {
|
|
91
|
+
describe('getPublicManifest', () => {
|
|
92
|
+
it('should return an array of public commands', () => {
|
|
93
|
+
const manifest = getPublicManifest();
|
|
94
|
+
expect(manifest).toBeDefined();
|
|
95
|
+
expect(Array.isArray(manifest)).toBe(true);
|
|
96
|
+
expect(manifest.length).toBeGreaterThan(0);
|
|
97
|
+
});
|
|
98
|
+
it('should have required fields for each command', () => {
|
|
99
|
+
const manifest = getPublicManifest();
|
|
100
|
+
for (const cmd of manifest) {
|
|
101
|
+
expect(cmd.name).toBeDefined();
|
|
102
|
+
expect(typeof cmd.name).toBe('string');
|
|
103
|
+
expect(cmd.name.length).toBeGreaterThan(0);
|
|
104
|
+
expect(cmd.binName).toBeDefined();
|
|
105
|
+
expect(typeof cmd.binName).toBe('string');
|
|
106
|
+
expect(cmd.binName.length).toBeGreaterThan(0);
|
|
107
|
+
expect(cmd.description).toBeDefined();
|
|
108
|
+
expect(typeof cmd.description).toBe('string');
|
|
109
|
+
expect(cmd.description.length).toBeGreaterThan(0);
|
|
110
|
+
expect(cmd.category).toBeDefined();
|
|
111
|
+
expect(typeof cmd.category).toBe('string');
|
|
112
|
+
expect(cmd.category.length).toBeGreaterThan(0);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
it('should include core public commands', () => {
|
|
116
|
+
const names = getPublicCommandNames();
|
|
117
|
+
// WU lifecycle - must be public
|
|
118
|
+
expect(names).toContain('wu:create');
|
|
119
|
+
expect(names).toContain('wu:claim');
|
|
120
|
+
expect(names).toContain('wu:done');
|
|
121
|
+
expect(names).toContain('wu:prep');
|
|
122
|
+
expect(names).toContain('wu:status');
|
|
123
|
+
// Gates - must be public
|
|
124
|
+
expect(names).toContain('gates');
|
|
125
|
+
expect(names).toContain('lumenflow');
|
|
126
|
+
// Memory - must be public
|
|
127
|
+
expect(names).toContain('mem:checkpoint');
|
|
128
|
+
expect(names).toContain('mem:inbox');
|
|
129
|
+
// Initiatives - must be public
|
|
130
|
+
expect(names).toContain('initiative:create');
|
|
131
|
+
expect(names).toContain('initiative:status');
|
|
132
|
+
});
|
|
133
|
+
it('should NOT include internal/maintainer commands', () => {
|
|
134
|
+
const names = getPublicCommandNames();
|
|
135
|
+
// Guards are internal
|
|
136
|
+
expect(names).not.toContain('guard-worktree-commit');
|
|
137
|
+
expect(names).not.toContain('guard-locked');
|
|
138
|
+
expect(names).not.toContain('guard-main-branch');
|
|
139
|
+
// Validation internals
|
|
140
|
+
expect(names).not.toContain('validate-agent-skills');
|
|
141
|
+
expect(names).not.toContain('validate-agent-sync');
|
|
142
|
+
expect(names).not.toContain('validate-backlog-sync');
|
|
143
|
+
expect(names).not.toContain('validate-skills-spec');
|
|
144
|
+
// Session internals
|
|
145
|
+
expect(names).not.toContain('session-coordinator');
|
|
146
|
+
expect(names).not.toContain('rotate-progress');
|
|
147
|
+
// Trace/debug internals
|
|
148
|
+
expect(names).not.toContain('trace-gen');
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
describe('isPublicCommand', () => {
|
|
152
|
+
it('should return true for public commands', () => {
|
|
153
|
+
expect(isPublicCommand('wu:create')).toBe(true);
|
|
154
|
+
expect(isPublicCommand('wu:claim')).toBe(true);
|
|
155
|
+
expect(isPublicCommand('gates')).toBe(true);
|
|
156
|
+
expect(isPublicCommand('lumenflow')).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
it('should return false for internal commands', () => {
|
|
159
|
+
expect(isPublicCommand('guard-worktree-commit')).toBe(false);
|
|
160
|
+
expect(isPublicCommand('validate-agent-skills')).toBe(false);
|
|
161
|
+
expect(isPublicCommand('session-coordinator')).toBe(false);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
describe('manifest alignment (WU-1432)', () => {
|
|
166
|
+
describe('commands.ts derives from manifest', () => {
|
|
167
|
+
it('should have all CLI registry commands in the public manifest (excluding script commands)', () => {
|
|
168
|
+
const registry = getCommandsRegistry();
|
|
169
|
+
const publicNames = new Set(getPublicCommandNames());
|
|
170
|
+
const registryNames = [];
|
|
171
|
+
for (const category of registry) {
|
|
172
|
+
for (const cmd of category.commands) {
|
|
173
|
+
// Skip script commands - they're not CLI binaries
|
|
174
|
+
if (!SCRIPT_COMMANDS.has(cmd.name)) {
|
|
175
|
+
registryNames.push(cmd.name);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Every CLI command in the registry must be a public command
|
|
180
|
+
for (const name of registryNames) {
|
|
181
|
+
expect(publicNames.has(name), `Command "${name}" in registry but not in public manifest`).toBe(true);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
it('should have matching descriptions between manifest and registry', () => {
|
|
185
|
+
const manifest = getPublicManifest();
|
|
186
|
+
const registry = getCommandsRegistry();
|
|
187
|
+
// Build a map of registry descriptions
|
|
188
|
+
const registryDescriptions = new Map();
|
|
189
|
+
for (const category of registry) {
|
|
190
|
+
for (const cmd of category.commands) {
|
|
191
|
+
registryDescriptions.set(cmd.name, cmd.description);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// For commands that appear in both, descriptions should match
|
|
195
|
+
for (const cmd of manifest) {
|
|
196
|
+
const registryDesc = registryDescriptions.get(cmd.name);
|
|
197
|
+
if (registryDesc) {
|
|
198
|
+
expect(cmd.description, `Description mismatch for "${cmd.name}"`).toBe(registryDesc);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
it('should have matching categories between manifest and registry', () => {
|
|
203
|
+
const manifest = getPublicManifest();
|
|
204
|
+
const registry = getCommandsRegistry();
|
|
205
|
+
// Build a map of registry categories
|
|
206
|
+
const registryCategories = new Map();
|
|
207
|
+
for (const category of registry) {
|
|
208
|
+
for (const cmd of category.commands) {
|
|
209
|
+
registryCategories.set(cmd.name, category.name);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// For commands that appear in both, categories should match
|
|
213
|
+
for (const cmd of manifest) {
|
|
214
|
+
const registryCategory = registryCategories.get(cmd.name);
|
|
215
|
+
if (registryCategory) {
|
|
216
|
+
expect(cmd.category, `Category mismatch for "${cmd.name}"`).toBe(registryCategory);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
describe('package.json bin alignment', () => {
|
|
222
|
+
it('should only include public commands in bin', () => {
|
|
223
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
224
|
+
const binEntries = Object.keys(packageJson.bin || {});
|
|
225
|
+
const publicBinNames = new Set(getPublicBinNames());
|
|
226
|
+
// Every bin entry should be in the public manifest
|
|
227
|
+
for (const binName of binEntries) {
|
|
228
|
+
expect(publicBinNames.has(binName), `Bin "${binName}" is in package.json but not in public manifest - should it be internal?`).toBe(true);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
it('should have all public commands in bin', () => {
|
|
232
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
233
|
+
const binEntries = new Set(Object.keys(packageJson.bin || {}));
|
|
234
|
+
const publicManifest = getPublicManifest();
|
|
235
|
+
// Every public manifest command should have a bin entry
|
|
236
|
+
for (const cmd of publicManifest) {
|
|
237
|
+
expect(binEntries.has(cmd.binName), `Public command "${cmd.name}" (bin: ${cmd.binName}) missing from package.json bin`).toBe(true);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
it('should have correct file paths in bin', () => {
|
|
241
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
242
|
+
const publicManifest = getPublicManifest();
|
|
243
|
+
for (const cmd of publicManifest) {
|
|
244
|
+
const binPath = packageJson.bin?.[cmd.binName];
|
|
245
|
+
expect(binPath, `Missing bin path for ${cmd.binName}`).toBeDefined();
|
|
246
|
+
expect(binPath, `Bin path for ${cmd.binName} should start with ./dist/`).toMatch(/^\.\/dist\//);
|
|
247
|
+
expect(binPath, `Bin path for ${cmd.binName} should end with .js`).toMatch(/\.js$/);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
it('should not include internal commands in bin', () => {
|
|
251
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
252
|
+
const binEntries = Object.keys(packageJson.bin || {});
|
|
253
|
+
// These internal commands should NOT be in bin
|
|
254
|
+
const internalCommands = [
|
|
255
|
+
'guard-worktree-commit',
|
|
256
|
+
'guard-locked',
|
|
257
|
+
'guard-main-branch',
|
|
258
|
+
'validate-agent-skills',
|
|
259
|
+
'validate-agent-sync',
|
|
260
|
+
'validate-backlog-sync',
|
|
261
|
+
'validate-skills-spec',
|
|
262
|
+
'session-coordinator',
|
|
263
|
+
'rotate-progress',
|
|
264
|
+
'trace-gen',
|
|
265
|
+
];
|
|
266
|
+
for (const internalCmd of internalCommands) {
|
|
267
|
+
expect(binEntries.includes(internalCmd), `Internal command "${internalCmd}" should NOT be in package.json bin`).toBe(false);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
});
|
|
@@ -67,6 +67,24 @@ describe('docs-structure', () => {
|
|
|
67
67
|
expect(fs.existsSync(path.join(tempDir, 'docs', 'tasks'))).toBe(true);
|
|
68
68
|
expect(fs.existsSync(path.join(tempDir, 'docs', DOCS_04_OPERATIONS))).toBe(false);
|
|
69
69
|
});
|
|
70
|
+
it('should scaffold WU template with resilient defaults (simple)', async () => {
|
|
71
|
+
const options = {
|
|
72
|
+
force: false,
|
|
73
|
+
full: true,
|
|
74
|
+
docsStructure: SIMPLE_DOCS_STRUCTURE,
|
|
75
|
+
};
|
|
76
|
+
await scaffoldProject(tempDir, options);
|
|
77
|
+
const templatePath = path.join(tempDir, 'docs', 'tasks', 'templates', 'wu-template.yaml');
|
|
78
|
+
expect(fs.existsSync(templatePath)).toBe(true);
|
|
79
|
+
const content = fs.readFileSync(templatePath, 'utf-8');
|
|
80
|
+
// Feature WUs should reference plan protocol by default (plan-less friendly).
|
|
81
|
+
expect(content).toContain('lumenflow://plans/WU-XXX-plan.md');
|
|
82
|
+
// Ensure non-empty notes to avoid strict spec-linter failures out of the box.
|
|
83
|
+
expect(content).not.toContain("notes: ''");
|
|
84
|
+
expect(content).toContain('notes:');
|
|
85
|
+
// Ensure manual test stub exists to prevent empty tests failures.
|
|
86
|
+
expect(content).toContain('Manual check:');
|
|
87
|
+
});
|
|
70
88
|
it('should scaffold arc42 structure with --docs-structure arc42', async () => {
|
|
71
89
|
const options = {
|
|
72
90
|
force: false,
|
|
@@ -77,6 +95,21 @@ describe('docs-structure', () => {
|
|
|
77
95
|
// Arc42 structure: docs/04-operations/tasks
|
|
78
96
|
expect(fs.existsSync(path.join(tempDir, 'docs', DOCS_04_OPERATIONS, 'tasks'))).toBe(true);
|
|
79
97
|
});
|
|
98
|
+
it('should scaffold WU template with resilient defaults (arc42)', async () => {
|
|
99
|
+
const options = {
|
|
100
|
+
force: false,
|
|
101
|
+
full: true,
|
|
102
|
+
docsStructure: ARC42_DOCS_STRUCTURE,
|
|
103
|
+
};
|
|
104
|
+
await scaffoldProject(tempDir, options);
|
|
105
|
+
const templatePath = path.join(tempDir, 'docs', DOCS_04_OPERATIONS, 'tasks', 'templates', 'wu-template.yaml');
|
|
106
|
+
expect(fs.existsSync(templatePath)).toBe(true);
|
|
107
|
+
const content = fs.readFileSync(templatePath, 'utf-8');
|
|
108
|
+
expect(content).toContain('lumenflow://plans/WU-XXX-plan.md');
|
|
109
|
+
expect(content).not.toContain("notes: ''");
|
|
110
|
+
expect(content).toContain('notes:');
|
|
111
|
+
expect(content).toContain('Manual check:');
|
|
112
|
+
});
|
|
80
113
|
it('should auto-detect arc42 when docs/04-operations exists', async () => {
|
|
81
114
|
// Create existing arc42 structure
|
|
82
115
|
fs.mkdirSync(path.join(tempDir, 'docs', DOCS_04_OPERATIONS), { recursive: true });
|
|
@@ -10,7 +10,8 @@ import { describe, it, expect, vi, beforeEach, afterEach, beforeAll } from 'vite
|
|
|
10
10
|
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
import { tmpdir } from 'node:os';
|
|
13
|
-
import { stringifyYAML } from '@lumenflow/core/dist/wu-yaml.js';
|
|
13
|
+
import { stringifyYAML, readWU } from '@lumenflow/core/dist/wu-yaml.js';
|
|
14
|
+
import { readInitiative } from '@lumenflow/initiatives/dist/initiative-yaml.js';
|
|
14
15
|
// Test constants to avoid lint warnings about duplicate strings
|
|
15
16
|
const TEST_WU_ID = 'WU-123';
|
|
16
17
|
const TEST_INIT_ID = 'INIT-001';
|
|
@@ -22,6 +23,7 @@ const TEST_INIT_TITLE = 'Test Initiative';
|
|
|
22
23
|
const TEST_INIT_STATUS = 'open';
|
|
23
24
|
const TEST_DATE = '2026-01-25';
|
|
24
25
|
const MIN_DESCRIPTION_LENGTH = 50;
|
|
26
|
+
const TEST_WU_ID_2 = 'WU-124';
|
|
25
27
|
// Valid WU document template
|
|
26
28
|
const createValidWUDoc = (overrides = {}) => ({
|
|
27
29
|
id: TEST_WU_ID,
|
|
@@ -324,6 +326,50 @@ describe('initiative:add-wu WU validation (WU-1330)', () => {
|
|
|
324
326
|
expect(result.valid).toBe(true);
|
|
325
327
|
});
|
|
326
328
|
});
|
|
329
|
+
describe('batch linking (WU-1460)', () => {
|
|
330
|
+
it('should normalize repeatable --wu values with dedupe and order preservation', async () => {
|
|
331
|
+
const { normalizeWuIds } = await import('../initiative-add-wu.js');
|
|
332
|
+
expect(normalizeWuIds(TEST_WU_ID)).toEqual([TEST_WU_ID]);
|
|
333
|
+
expect(normalizeWuIds([TEST_WU_ID, TEST_WU_ID_2, TEST_WU_ID])).toEqual([
|
|
334
|
+
TEST_WU_ID,
|
|
335
|
+
TEST_WU_ID_2,
|
|
336
|
+
]);
|
|
337
|
+
});
|
|
338
|
+
it('should update multiple WUs and initiative in one execute call', async () => {
|
|
339
|
+
const { buildAddWuMicroWorktreeOptions } = await import('../initiative-add-wu.js');
|
|
340
|
+
// Setup valid WUs and initiative
|
|
341
|
+
const wuDir = join(tempDir, WU_REL_PATH);
|
|
342
|
+
const initDir = join(tempDir, INIT_REL_PATH);
|
|
343
|
+
mkdirSync(wuDir, { recursive: true });
|
|
344
|
+
mkdirSync(initDir, { recursive: true });
|
|
345
|
+
const wuPath1 = join(wuDir, `${TEST_WU_ID}.yaml`);
|
|
346
|
+
const wuPath2 = join(wuDir, `${TEST_WU_ID_2}.yaml`);
|
|
347
|
+
const initPath = join(initDir, `${TEST_INIT_ID}.yaml`);
|
|
348
|
+
writeFileSync(wuPath1, stringifyYAML(createValidWUDoc({ id: TEST_WU_ID })));
|
|
349
|
+
writeFileSync(wuPath2, stringifyYAML(createValidWUDoc({ id: TEST_WU_ID_2 })));
|
|
350
|
+
writeFileSync(initPath, stringifyYAML(createValidInitDoc()));
|
|
351
|
+
process.chdir(tempDir);
|
|
352
|
+
const options = buildAddWuMicroWorktreeOptions([TEST_WU_ID, TEST_WU_ID_2], TEST_INIT_ID);
|
|
353
|
+
const result = await options.execute({ worktreePath: tempDir });
|
|
354
|
+
expect(result.files).toContain(`${WU_REL_PATH}/${TEST_WU_ID}.yaml`);
|
|
355
|
+
expect(result.files).toContain(`${WU_REL_PATH}/${TEST_WU_ID_2}.yaml`);
|
|
356
|
+
expect(result.files).toContain(`${INIT_REL_PATH}/${TEST_INIT_ID}.yaml`);
|
|
357
|
+
const updatedWu1 = readWU(wuPath1, TEST_WU_ID);
|
|
358
|
+
const updatedWu2 = readWU(wuPath2, TEST_WU_ID_2);
|
|
359
|
+
const updatedInit = readInitiative(initPath, TEST_INIT_ID);
|
|
360
|
+
expect(updatedWu1.initiative).toBe(TEST_INIT_ID);
|
|
361
|
+
expect(updatedWu2.initiative).toBe(TEST_INIT_ID);
|
|
362
|
+
expect(updatedInit.wus).toContain(TEST_WU_ID);
|
|
363
|
+
expect(updatedInit.wus).toContain(TEST_WU_ID_2);
|
|
364
|
+
});
|
|
365
|
+
it('should validate conflicting links across multiple WUs', async () => {
|
|
366
|
+
const { validateNoConflictingLinks } = await import('../initiative-add-wu.js');
|
|
367
|
+
expect(() => validateNoConflictingLinks([
|
|
368
|
+
{ id: TEST_WU_ID, initiative: TEST_INIT_ID },
|
|
369
|
+
{ id: TEST_WU_ID_2, initiative: 'INIT-999' },
|
|
370
|
+
], TEST_INIT_ID)).toThrow();
|
|
371
|
+
});
|
|
372
|
+
});
|
|
327
373
|
describe('error formatting', () => {
|
|
328
374
|
it('should format errors in human-readable format', async () => {
|
|
329
375
|
const { formatValidationErrors } = await import('../initiative-add-wu.js');
|
|
@@ -356,6 +402,24 @@ describe('initiative:add-wu WU validation (WU-1330)', () => {
|
|
|
356
402
|
const mod = await import('../initiative-add-wu.js');
|
|
357
403
|
expect(typeof mod.formatRetryExhaustionError).toBe('function');
|
|
358
404
|
});
|
|
405
|
+
it('should export operation-level push retry override (WU-1459)', async () => {
|
|
406
|
+
const mod = await import('../initiative-add-wu.js');
|
|
407
|
+
expect(mod.INITIATIVE_ADD_WU_PUSH_RETRY_OVERRIDE).toBeDefined();
|
|
408
|
+
expect(mod.INITIATIVE_ADD_WU_PUSH_RETRY_OVERRIDE.retries).toBeGreaterThan(3);
|
|
409
|
+
expect(mod.INITIATIVE_ADD_WU_PUSH_RETRY_OVERRIDE.min_delay_ms).toBeGreaterThan(100);
|
|
410
|
+
});
|
|
411
|
+
it('should export helper to build micro-worktree options (WU-1459)', async () => {
|
|
412
|
+
const mod = await import('../initiative-add-wu.js');
|
|
413
|
+
expect(typeof mod.buildAddWuMicroWorktreeOptions).toBe('function');
|
|
414
|
+
const options = mod.buildAddWuMicroWorktreeOptions(TEST_WU_ID, TEST_INIT_ID);
|
|
415
|
+
expect(options.pushOnly).toBe(true);
|
|
416
|
+
expect(options.pushRetryOverride).toEqual(mod.INITIATIVE_ADD_WU_PUSH_RETRY_OVERRIDE);
|
|
417
|
+
});
|
|
418
|
+
it('should export batch helpers (WU-1460)', async () => {
|
|
419
|
+
const mod = await import('../initiative-add-wu.js');
|
|
420
|
+
expect(typeof mod.normalizeWuIds).toBe('function');
|
|
421
|
+
expect(typeof mod.validateNoConflictingLinks).toBe('function');
|
|
422
|
+
});
|
|
359
423
|
});
|
|
360
424
|
});
|
|
361
425
|
/**
|
|
@@ -416,5 +480,11 @@ describe('initiative:add-wu retry handling (WU-1333)', () => {
|
|
|
416
480
|
// Should mention concurrent agents as possible cause
|
|
417
481
|
expect(formatted).toMatch(/concurrent|agent|traffic/i);
|
|
418
482
|
});
|
|
483
|
+
it('should include git.push_retry tuning guidance', async () => {
|
|
484
|
+
const { formatRetryExhaustionError } = await import('../initiative-add-wu.js');
|
|
485
|
+
const retryError = new Error('Push failed after 3 attempts.');
|
|
486
|
+
const formatted = formatRetryExhaustionError(retryError, TEST_WU_ID, TEST_INIT_ID);
|
|
487
|
+
expect(formatted).toContain('git.push_retry.retries');
|
|
488
|
+
});
|
|
419
489
|
});
|
|
420
490
|
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file no-beacon-references-docs.test.ts
|
|
3
|
+
* Guardrail test: public + onboarding docs must not reference legacy `.beacon` paths (WU-1450).
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
function repoRootFromThisFile() {
|
|
10
|
+
// packages/@lumenflow/cli/src/__tests__/no-beacon-references-docs.test.ts -> repo root
|
|
11
|
+
const thisDir = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
return path.resolve(thisDir, '..', '..', '..', '..', '..');
|
|
13
|
+
}
|
|
14
|
+
describe('no legacy .beacon references in docs (WU-1450)', () => {
|
|
15
|
+
it('should not contain .beacon references in onboarding/public docs', () => {
|
|
16
|
+
const repoRoot = repoRootFromThisFile();
|
|
17
|
+
const files = [
|
|
18
|
+
'docs/04-operations/_frameworks/lumenflow/agent/onboarding/agent-safety-card.md',
|
|
19
|
+
'docs/04-operations/_frameworks/lumenflow/agent/onboarding/lumenflow-force-usage.md',
|
|
20
|
+
'apps/docs/src/content/docs/getting-started/upgrade.mdx',
|
|
21
|
+
'apps/docs/src/content/docs/reference/changelog.mdx',
|
|
22
|
+
'apps/docs/src/content/docs/reference/compatibility.mdx',
|
|
23
|
+
];
|
|
24
|
+
for (const relPath of files) {
|
|
25
|
+
const absPath = path.join(repoRoot, relPath);
|
|
26
|
+
const content = readFileSync(absPath, 'utf-8');
|
|
27
|
+
expect(content, `${relPath} should not reference .beacon`).not.toContain('.beacon');
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file no-beacon-references.test.ts
|
|
3
|
+
* Guardrail test: `.beacon` is legacy and must not be referenced in docs/templates/scripts (WU-1447).
|
|
4
|
+
*
|
|
5
|
+
* This keeps onboarding friction-free by ensuring `.lumenflow/` is the single canonical namespace.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import { readFileSync } from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
function repoRootFromThisFile() {
|
|
12
|
+
// packages/@lumenflow/cli/src/__tests__/no-beacon-references.test.ts -> repo root (../../../../../)
|
|
13
|
+
const thisDir = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
return path.resolve(thisDir, '..', '..', '..', '..', '..');
|
|
15
|
+
}
|
|
16
|
+
describe('no legacy .beacon references (WU-1447)', () => {
|
|
17
|
+
it('should not contain .beacon references in tracked onboarding docs/templates/scripts', () => {
|
|
18
|
+
const repoRoot = repoRootFromThisFile();
|
|
19
|
+
const files = [
|
|
20
|
+
'scripts/safe-git',
|
|
21
|
+
'scripts/hooks/check-lockfile.sh',
|
|
22
|
+
'scripts/hooks/scan-secrets.sh',
|
|
23
|
+
'scripts/hooks/validate-paths.sh',
|
|
24
|
+
'scripts/hooks/validate-worktree-discipline.sh',
|
|
25
|
+
'LUMENFLOW.md',
|
|
26
|
+
'packages/@lumenflow/cli/templates/core/LUMENFLOW.md.template',
|
|
27
|
+
'packages/@lumenflow/cli/templates/core/ai/onboarding/agent-safety-card.md.template',
|
|
28
|
+
'packages/@lumenflow/cli/templates/core/ai/onboarding/lumenflow-force-usage.md.template',
|
|
29
|
+
'apps/docs/src/content/docs/guides/agent-onboarding.mdx',
|
|
30
|
+
'apps/docs/src/content/docs/guides/ai-agents.mdx',
|
|
31
|
+
'packages/@lumenflow/agent/README.md',
|
|
32
|
+
];
|
|
33
|
+
for (const relPath of files) {
|
|
34
|
+
const absPath = path.join(repoRoot, relPath);
|
|
35
|
+
const content = readFileSync(absPath, 'utf-8');
|
|
36
|
+
expect(content, `${relPath} should not reference .beacon`).not.toContain('.beacon');
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Tests for wu:create helpers and warnings (WU-1429)
|
|
4
4
|
*/
|
|
5
5
|
import { describe, it, expect } from 'vitest';
|
|
6
|
-
import { buildWUContent, collectInitiativeWarnings } from '../wu-create.js';
|
|
6
|
+
import { buildWUContent, collectInitiativeWarnings, validateCreateSpec } from '../wu-create.js';
|
|
7
7
|
const BASE_WU = {
|
|
8
8
|
id: 'WU-1429',
|
|
9
9
|
lane: 'Framework: CLI',
|
|
@@ -21,6 +21,19 @@ const BASE_WU = {
|
|
|
21
21
|
},
|
|
22
22
|
};
|
|
23
23
|
describe('wu:create helpers (WU-1429)', () => {
|
|
24
|
+
it('should default notes to non-empty placeholder when not provided', () => {
|
|
25
|
+
const wu = buildWUContent({
|
|
26
|
+
...BASE_WU,
|
|
27
|
+
opts: {
|
|
28
|
+
...BASE_WU.opts,
|
|
29
|
+
// Intentionally omit notes
|
|
30
|
+
notes: undefined,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
expect(typeof wu.notes).toBe('string');
|
|
34
|
+
expect(wu.notes.trim().length).toBeGreaterThan(0);
|
|
35
|
+
expect(wu.notes).toContain('(auto)');
|
|
36
|
+
});
|
|
24
37
|
it('should persist notes when provided', () => {
|
|
25
38
|
const wu = buildWUContent({
|
|
26
39
|
...BASE_WU,
|
|
@@ -31,6 +44,42 @@ describe('wu:create helpers (WU-1429)', () => {
|
|
|
31
44
|
});
|
|
32
45
|
expect(wu.notes).toBe('Implementation notes for test');
|
|
33
46
|
});
|
|
47
|
+
it('should allow creating a plan-first WU without explicit test flags when code_paths are non-code', () => {
|
|
48
|
+
const validation = validateCreateSpec({
|
|
49
|
+
id: 'WU-2000',
|
|
50
|
+
lane: 'Framework: CLI',
|
|
51
|
+
title: 'Plan-only spec creation',
|
|
52
|
+
priority: 'P2',
|
|
53
|
+
type: 'feature',
|
|
54
|
+
opts: {
|
|
55
|
+
description: 'Context: test context.\nProblem: test problem.\nSolution: test solution that exceeds minimum.',
|
|
56
|
+
acceptance: ['Acceptance criterion'],
|
|
57
|
+
exposure: 'backend-only',
|
|
58
|
+
// Non-code file path: manual-only tests are acceptable.
|
|
59
|
+
codePaths: ['docs/README.md'],
|
|
60
|
+
// No testPathsManual/unit/e2e provided - should auto-default manual stub.
|
|
61
|
+
specRefs: ['lumenflow://plans/WU-2000-plan.md'],
|
|
62
|
+
strict: false,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
expect(validation.valid).toBe(true);
|
|
66
|
+
const wu = buildWUContent({
|
|
67
|
+
id: 'WU-2000',
|
|
68
|
+
lane: 'Framework: CLI',
|
|
69
|
+
title: 'Plan-only spec creation',
|
|
70
|
+
priority: 'P2',
|
|
71
|
+
type: 'feature',
|
|
72
|
+
created: '2026-02-05',
|
|
73
|
+
opts: {
|
|
74
|
+
description: 'Context: test context.\nProblem: test problem.\nSolution: test solution that exceeds minimum.',
|
|
75
|
+
acceptance: ['Acceptance criterion'],
|
|
76
|
+
exposure: 'backend-only',
|
|
77
|
+
codePaths: ['docs/README.md'],
|
|
78
|
+
specRefs: ['lumenflow://plans/WU-2000-plan.md'],
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
expect(wu.tests?.manual?.length).toBeGreaterThan(0);
|
|
82
|
+
});
|
|
34
83
|
it('should warn when initiative has phases but no --phase is provided', () => {
|
|
35
84
|
const warnings = collectInitiativeWarnings({
|
|
36
85
|
initiativeId: 'INIT-TEST',
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file wu-done-docs-only-policy.test.ts
|
|
3
|
+
* Guardrail test: docs-only eligibility checks must not use raw type/exposure string literals (WU-1446).
|
|
4
|
+
*
|
|
5
|
+
* This keeps CLI policy logic DRY and aligned with core constants/helpers.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import { readFileSync } from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
describe('wu:done docs-only policy (WU-1446)', () => {
|
|
12
|
+
it('should not use raw documentation string comparisons for type/exposure checks', () => {
|
|
13
|
+
const thisDir = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const filePath = path.join(thisDir, '..', 'wu-done.ts');
|
|
15
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
16
|
+
// These comparisons should use core constants/helpers instead.
|
|
17
|
+
expect(content).not.toContain("exposure === 'documentation'");
|
|
18
|
+
expect(content).not.toContain("type === 'documentation'");
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
vi.mock('node:child_process', async (importOriginal) => {
|
|
3
|
+
const actual = await importOriginal();
|
|
4
|
+
return { ...actual, spawnSync: vi.fn() };
|
|
5
|
+
});
|
|
6
|
+
vi.mock('node:fs', async (importOriginal) => {
|
|
7
|
+
const actual = await importOriginal();
|
|
8
|
+
return { ...actual, existsSync: vi.fn() };
|
|
9
|
+
});
|
|
10
|
+
describe('wu-prep default exec helpers (WU-1441)', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
vi.resetModules();
|
|
13
|
+
vi.resetAllMocks();
|
|
14
|
+
});
|
|
15
|
+
it('uses node + dist wu-validate for JSON comparison when available', async () => {
|
|
16
|
+
const { spawnSync } = await import('node:child_process');
|
|
17
|
+
const { existsSync } = await import('node:fs');
|
|
18
|
+
// Pretend dist sibling exists so defaultExec picks node+dist path (not pnpm on main).
|
|
19
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
20
|
+
// Both worktree and main should report WU-1 invalid.
|
|
21
|
+
vi.mocked(spawnSync).mockReturnValue({
|
|
22
|
+
status: 1,
|
|
23
|
+
stdout: JSON.stringify({ invalid: [{ wuId: 'WU-1' }] }),
|
|
24
|
+
stderr: '',
|
|
25
|
+
});
|
|
26
|
+
const { checkPreExistingFailures } = await import('../wu-prep.js');
|
|
27
|
+
const result = await checkPreExistingFailures({ mainCheckout: '/repo' });
|
|
28
|
+
expect(result.error).toBeUndefined();
|
|
29
|
+
expect(result.hasPreExisting).toBe(true);
|
|
30
|
+
expect(result.hasNewFailures).toBe(false);
|
|
31
|
+
// Default exec should run node directly, not "pnpm wu:validate" from the main checkout.
|
|
32
|
+
expect(vi.mocked(spawnSync).mock.calls.length).toBeGreaterThan(0);
|
|
33
|
+
expect(vi.mocked(spawnSync).mock.calls[0]?.[0]).toBe('node');
|
|
34
|
+
});
|
|
35
|
+
});
|