@lumenflow/cli 2.10.0 → 2.12.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 +28 -18
- package/dist/__tests__/commands.test.js +198 -2
- package/dist/__tests__/gates-integration-tests.test.js +112 -0
- package/dist/__tests__/init-docs-structure.test.js +33 -0
- package/dist/__tests__/init.test.js +225 -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__/safe-git.test.js +4 -4
- package/dist/__tests__/wu-create-required-fields.test.js +22 -0
- package/dist/__tests__/wu-create.test.js +121 -0
- 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 +32 -20
- 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 +176 -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 +67 -32
- 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 +20 -1
- 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 +37 -1
- package/dist/state-doctor-fix.js.map +1 -0
- package/dist/state-doctor.js +11 -6
- 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-repair-guidance.js +10 -0
- package/dist/wu-claim-repair-guidance.js.map +1 -0
- package/dist/wu-claim.js +40 -0
- 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 +91 -25
- 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 +75 -18
- package/dist/wu-done.js.map +1 -0
- package/dist/wu-edit.js +2 -1
- 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 +105 -9
- 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 +54 -2
- 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 +3 -2
- 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/.mcp.json.template +8 -0
- 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/templates/core/ai/onboarding/vendor-support.md.template +73 -0
- package/templates/core/scripts/safe-git.template +29 -0
|
@@ -740,4 +740,229 @@ describe('lumenflow init', () => {
|
|
|
740
740
|
});
|
|
741
741
|
});
|
|
742
742
|
});
|
|
743
|
+
// WU-1408: safe-git and pre-commit hook scaffolding
|
|
744
|
+
describe('WU-1408: safe-git and pre-commit scaffolding', () => {
|
|
745
|
+
describe('safe-git wrapper', () => {
|
|
746
|
+
it('should scaffold scripts/safe-git', async () => {
|
|
747
|
+
const options = {
|
|
748
|
+
force: false,
|
|
749
|
+
full: true,
|
|
750
|
+
};
|
|
751
|
+
await scaffoldProject(tempDir, options);
|
|
752
|
+
const safeGitPath = path.join(tempDir, 'scripts', 'safe-git');
|
|
753
|
+
expect(fs.existsSync(safeGitPath)).toBe(true);
|
|
754
|
+
});
|
|
755
|
+
it('should make safe-git executable', async () => {
|
|
756
|
+
const options = {
|
|
757
|
+
force: false,
|
|
758
|
+
full: true,
|
|
759
|
+
};
|
|
760
|
+
await scaffoldProject(tempDir, options);
|
|
761
|
+
const safeGitPath = path.join(tempDir, 'scripts', 'safe-git');
|
|
762
|
+
const stats = fs.statSync(safeGitPath);
|
|
763
|
+
// Check for executable bit (owner, group, or other)
|
|
764
|
+
// eslint-disable-next-line no-bitwise
|
|
765
|
+
const isExecutable = (stats.mode & 0o111) !== 0;
|
|
766
|
+
expect(isExecutable).toBe(true);
|
|
767
|
+
});
|
|
768
|
+
it('should include worktree remove block in safe-git', async () => {
|
|
769
|
+
const options = {
|
|
770
|
+
force: false,
|
|
771
|
+
full: true,
|
|
772
|
+
};
|
|
773
|
+
await scaffoldProject(tempDir, options);
|
|
774
|
+
const safeGitPath = path.join(tempDir, 'scripts', 'safe-git');
|
|
775
|
+
const content = fs.readFileSync(safeGitPath, 'utf-8');
|
|
776
|
+
expect(content).toContain('worktree');
|
|
777
|
+
expect(content).toContain('remove');
|
|
778
|
+
expect(content).toContain('BLOCKED');
|
|
779
|
+
});
|
|
780
|
+
it('should scaffold safe-git even in minimal mode', async () => {
|
|
781
|
+
const options = {
|
|
782
|
+
force: false,
|
|
783
|
+
full: false, // minimal mode
|
|
784
|
+
};
|
|
785
|
+
await scaffoldProject(tempDir, options);
|
|
786
|
+
const safeGitPath = path.join(tempDir, 'scripts', 'safe-git');
|
|
787
|
+
expect(fs.existsSync(safeGitPath)).toBe(true);
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
describe('pre-commit hook', () => {
|
|
791
|
+
it('should scaffold .husky/pre-commit', async () => {
|
|
792
|
+
const options = {
|
|
793
|
+
force: false,
|
|
794
|
+
full: true,
|
|
795
|
+
};
|
|
796
|
+
await scaffoldProject(tempDir, options);
|
|
797
|
+
const preCommitPath = path.join(tempDir, '.husky', 'pre-commit');
|
|
798
|
+
expect(fs.existsSync(preCommitPath)).toBe(true);
|
|
799
|
+
});
|
|
800
|
+
it('should make pre-commit executable', async () => {
|
|
801
|
+
const options = {
|
|
802
|
+
force: false,
|
|
803
|
+
full: true,
|
|
804
|
+
};
|
|
805
|
+
await scaffoldProject(tempDir, options);
|
|
806
|
+
const preCommitPath = path.join(tempDir, '.husky', 'pre-commit');
|
|
807
|
+
const stats = fs.statSync(preCommitPath);
|
|
808
|
+
// eslint-disable-next-line no-bitwise
|
|
809
|
+
const isExecutable = (stats.mode & 0o111) !== 0;
|
|
810
|
+
expect(isExecutable).toBe(true);
|
|
811
|
+
});
|
|
812
|
+
it('should NOT run pnpm test in pre-commit hook', async () => {
|
|
813
|
+
const options = {
|
|
814
|
+
force: false,
|
|
815
|
+
full: true,
|
|
816
|
+
};
|
|
817
|
+
await scaffoldProject(tempDir, options);
|
|
818
|
+
const preCommitPath = path.join(tempDir, '.husky', 'pre-commit');
|
|
819
|
+
const content = fs.readFileSync(preCommitPath, 'utf-8');
|
|
820
|
+
// The pre-commit hook should NOT assume pnpm test exists
|
|
821
|
+
expect(content).not.toContain('pnpm test');
|
|
822
|
+
expect(content).not.toContain('npm test');
|
|
823
|
+
});
|
|
824
|
+
it('should block commits to main/master in pre-commit', async () => {
|
|
825
|
+
const options = {
|
|
826
|
+
force: false,
|
|
827
|
+
full: true,
|
|
828
|
+
};
|
|
829
|
+
await scaffoldProject(tempDir, options);
|
|
830
|
+
const preCommitPath = path.join(tempDir, '.husky', 'pre-commit');
|
|
831
|
+
const content = fs.readFileSync(preCommitPath, 'utf-8');
|
|
832
|
+
// Should protect main branch
|
|
833
|
+
expect(content).toContain('main');
|
|
834
|
+
expect(content).toContain('BLOCK');
|
|
835
|
+
});
|
|
836
|
+
it('should scaffold pre-commit even in minimal mode', async () => {
|
|
837
|
+
const options = {
|
|
838
|
+
force: false,
|
|
839
|
+
full: false, // minimal mode
|
|
840
|
+
};
|
|
841
|
+
await scaffoldProject(tempDir, options);
|
|
842
|
+
const preCommitPath = path.join(tempDir, '.husky', 'pre-commit');
|
|
843
|
+
expect(fs.existsSync(preCommitPath)).toBe(true);
|
|
844
|
+
});
|
|
845
|
+
});
|
|
846
|
+
});
|
|
847
|
+
// WU-1413: MCP server configuration scaffolding
|
|
848
|
+
describe('WU-1413: .mcp.json scaffolding', () => {
|
|
849
|
+
const MCP_JSON_FILE = '.mcp.json';
|
|
850
|
+
describe('.mcp.json creation with --client claude', () => {
|
|
851
|
+
it('should scaffold .mcp.json when --client claude is used', async () => {
|
|
852
|
+
const options = {
|
|
853
|
+
force: false,
|
|
854
|
+
full: false,
|
|
855
|
+
client: 'claude',
|
|
856
|
+
};
|
|
857
|
+
await scaffoldProject(tempDir, options);
|
|
858
|
+
const mcpJsonPath = path.join(tempDir, MCP_JSON_FILE);
|
|
859
|
+
expect(fs.existsSync(mcpJsonPath)).toBe(true);
|
|
860
|
+
});
|
|
861
|
+
it('should include lumenflow MCP server configuration', async () => {
|
|
862
|
+
const options = {
|
|
863
|
+
force: false,
|
|
864
|
+
full: false,
|
|
865
|
+
client: 'claude',
|
|
866
|
+
};
|
|
867
|
+
await scaffoldProject(tempDir, options);
|
|
868
|
+
const mcpJsonPath = path.join(tempDir, MCP_JSON_FILE);
|
|
869
|
+
const content = fs.readFileSync(mcpJsonPath, 'utf-8');
|
|
870
|
+
const mcpConfig = JSON.parse(content);
|
|
871
|
+
// Should have mcpServers key
|
|
872
|
+
expect(mcpConfig.mcpServers).toBeDefined();
|
|
873
|
+
// Should have lumenflow server entry
|
|
874
|
+
expect(mcpConfig.mcpServers.lumenflow).toBeDefined();
|
|
875
|
+
// Should use npx command
|
|
876
|
+
expect(mcpConfig.mcpServers.lumenflow.command).toBe('npx');
|
|
877
|
+
// Should reference @lumenflow/mcp package
|
|
878
|
+
expect(mcpConfig.mcpServers.lumenflow.args).toContain('@lumenflow/mcp');
|
|
879
|
+
});
|
|
880
|
+
it('should be valid JSON', async () => {
|
|
881
|
+
const options = {
|
|
882
|
+
force: false,
|
|
883
|
+
full: false,
|
|
884
|
+
client: 'claude',
|
|
885
|
+
};
|
|
886
|
+
await scaffoldProject(tempDir, options);
|
|
887
|
+
const mcpJsonPath = path.join(tempDir, MCP_JSON_FILE);
|
|
888
|
+
const content = fs.readFileSync(mcpJsonPath, 'utf-8');
|
|
889
|
+
// Should parse without error
|
|
890
|
+
expect(() => JSON.parse(content)).not.toThrow();
|
|
891
|
+
});
|
|
892
|
+
});
|
|
893
|
+
describe('.mcp.json creation with --client all', () => {
|
|
894
|
+
it('should scaffold .mcp.json when --client all is used', async () => {
|
|
895
|
+
const options = {
|
|
896
|
+
force: false,
|
|
897
|
+
full: false,
|
|
898
|
+
client: 'all',
|
|
899
|
+
};
|
|
900
|
+
await scaffoldProject(tempDir, options);
|
|
901
|
+
const mcpJsonPath = path.join(tempDir, MCP_JSON_FILE);
|
|
902
|
+
expect(fs.existsSync(mcpJsonPath)).toBe(true);
|
|
903
|
+
});
|
|
904
|
+
});
|
|
905
|
+
describe('.mcp.json NOT created with other clients', () => {
|
|
906
|
+
it('should NOT scaffold .mcp.json when --client none is used', async () => {
|
|
907
|
+
const options = {
|
|
908
|
+
force: false,
|
|
909
|
+
full: false,
|
|
910
|
+
client: 'none',
|
|
911
|
+
};
|
|
912
|
+
await scaffoldProject(tempDir, options);
|
|
913
|
+
const mcpJsonPath = path.join(tempDir, MCP_JSON_FILE);
|
|
914
|
+
expect(fs.existsSync(mcpJsonPath)).toBe(false);
|
|
915
|
+
});
|
|
916
|
+
it('should NOT scaffold .mcp.json when --client cursor is used', async () => {
|
|
917
|
+
const options = {
|
|
918
|
+
force: false,
|
|
919
|
+
full: false,
|
|
920
|
+
client: 'cursor',
|
|
921
|
+
};
|
|
922
|
+
await scaffoldProject(tempDir, options);
|
|
923
|
+
const mcpJsonPath = path.join(tempDir, MCP_JSON_FILE);
|
|
924
|
+
expect(fs.existsSync(mcpJsonPath)).toBe(false);
|
|
925
|
+
});
|
|
926
|
+
it('should NOT scaffold .mcp.json when no client is specified', async () => {
|
|
927
|
+
const options = {
|
|
928
|
+
force: false,
|
|
929
|
+
full: false,
|
|
930
|
+
};
|
|
931
|
+
await scaffoldProject(tempDir, options);
|
|
932
|
+
const mcpJsonPath = path.join(tempDir, MCP_JSON_FILE);
|
|
933
|
+
expect(fs.existsSync(mcpJsonPath)).toBe(false);
|
|
934
|
+
});
|
|
935
|
+
});
|
|
936
|
+
describe('.mcp.json file modes', () => {
|
|
937
|
+
it('should skip .mcp.json if it already exists (skip mode)', async () => {
|
|
938
|
+
// Create existing .mcp.json
|
|
939
|
+
const existingContent = '{"mcpServers":{"custom":{}}}';
|
|
940
|
+
fs.writeFileSync(path.join(tempDir, MCP_JSON_FILE), existingContent);
|
|
941
|
+
const options = {
|
|
942
|
+
force: false,
|
|
943
|
+
full: false,
|
|
944
|
+
client: 'claude',
|
|
945
|
+
};
|
|
946
|
+
const result = await scaffoldProject(tempDir, options);
|
|
947
|
+
expect(result.skipped).toContain(MCP_JSON_FILE);
|
|
948
|
+
// Content should not be changed
|
|
949
|
+
const content = fs.readFileSync(path.join(tempDir, MCP_JSON_FILE), 'utf-8');
|
|
950
|
+
expect(content).toBe(existingContent);
|
|
951
|
+
});
|
|
952
|
+
it('should overwrite .mcp.json in force mode', async () => {
|
|
953
|
+
// Create existing .mcp.json
|
|
954
|
+
fs.writeFileSync(path.join(tempDir, MCP_JSON_FILE), '{"custom":true}');
|
|
955
|
+
const options = {
|
|
956
|
+
force: true,
|
|
957
|
+
full: false,
|
|
958
|
+
client: 'claude',
|
|
959
|
+
};
|
|
960
|
+
const result = await scaffoldProject(tempDir, options);
|
|
961
|
+
expect(result.created).toContain(MCP_JSON_FILE);
|
|
962
|
+
const content = fs.readFileSync(path.join(tempDir, MCP_JSON_FILE), 'utf-8');
|
|
963
|
+
const mcpConfig = JSON.parse(content);
|
|
964
|
+
expect(mcpConfig.mcpServers.lumenflow).toBeDefined();
|
|
965
|
+
});
|
|
966
|
+
});
|
|
967
|
+
});
|
|
743
968
|
});
|
|
@@ -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
|
+
});
|
|
@@ -13,7 +13,7 @@ const USER_NAME_CONFIG = 'user.name';
|
|
|
13
13
|
const TEST_EMAIL = 'test@test.com';
|
|
14
14
|
const TEST_USERNAME = 'Test';
|
|
15
15
|
const FORCE_BYPASSES_LOG = 'force-bypasses.log';
|
|
16
|
-
// Create a temporary directory for testing to avoid polluting the real .
|
|
16
|
+
// Create a temporary directory for testing to avoid polluting the real .lumenflow directory
|
|
17
17
|
const createTempDir = () => {
|
|
18
18
|
return fs.mkdtempSync(path.join(os.tmpdir(), 'safe-git-test-'));
|
|
19
19
|
};
|
|
@@ -128,7 +128,7 @@ describe('safe-git', () => {
|
|
|
128
128
|
env: { ...process.env, LUMENFLOW_FORCE: '1' },
|
|
129
129
|
});
|
|
130
130
|
// Check that the force bypass log exists and contains the entry
|
|
131
|
-
const bypassLog = path.join(testRepo, '.
|
|
131
|
+
const bypassLog = path.join(testRepo, '.lumenflow', FORCE_BYPASSES_LOG);
|
|
132
132
|
expect(fs.existsSync(bypassLog)).toBe(true);
|
|
133
133
|
const logContent = fs.readFileSync(bypassLog, 'utf-8');
|
|
134
134
|
expect(logContent).toContain('reset --hard');
|
|
@@ -155,7 +155,7 @@ describe('safe-git', () => {
|
|
|
155
155
|
encoding: 'utf-8',
|
|
156
156
|
env: { ...process.env, LUMENFLOW_FORCE: '1', LUMENFLOW_FORCE_REASON: testReason },
|
|
157
157
|
});
|
|
158
|
-
const bypassLog = path.join(testRepo, '.
|
|
158
|
+
const bypassLog = path.join(testRepo, '.lumenflow', FORCE_BYPASSES_LOG);
|
|
159
159
|
const logContent = fs.readFileSync(bypassLog, 'utf-8');
|
|
160
160
|
expect(logContent).toContain(testReason);
|
|
161
161
|
});
|
|
@@ -182,7 +182,7 @@ describe('safe-git', () => {
|
|
|
182
182
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
183
183
|
});
|
|
184
184
|
// Check the bypasslog for the NO_REASON marker
|
|
185
|
-
const bypassLog = path.join(testRepo, '.
|
|
185
|
+
const bypassLog = path.join(testRepo, '.lumenflow', FORCE_BYPASSES_LOG);
|
|
186
186
|
const logContent = fs.readFileSync(bypassLog, 'utf-8');
|
|
187
187
|
expect(logContent).toContain('NO_REASON');
|
|
188
188
|
});
|
|
@@ -82,6 +82,28 @@ describe('wu:create required field aggregation (WU-1366)', () => {
|
|
|
82
82
|
expect(result.valid).toBe(false);
|
|
83
83
|
expect(result.errors.some((e) => e.includes('--spec-refs'))).toBe(true);
|
|
84
84
|
});
|
|
85
|
+
it('should treat empty spec-refs array as missing for feature WUs', () => {
|
|
86
|
+
const result = validateCreateSpec({
|
|
87
|
+
id: TEST_WU_ID,
|
|
88
|
+
lane: TEST_LANE,
|
|
89
|
+
title: 'Test WU',
|
|
90
|
+
priority: 'P2',
|
|
91
|
+
type: 'feature',
|
|
92
|
+
opts: {
|
|
93
|
+
description: VALID_DESCRIPTION,
|
|
94
|
+
acceptance: TEST_ACCEPTANCE,
|
|
95
|
+
exposure: 'backend-only',
|
|
96
|
+
codePaths: ['packages/@lumenflow/cli/src/wu-create.ts'],
|
|
97
|
+
testPathsUnit: [
|
|
98
|
+
'packages/@lumenflow/cli/src/__tests__/wu-create-required-fields.test.ts',
|
|
99
|
+
],
|
|
100
|
+
specRefs: [],
|
|
101
|
+
strict: false,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
expect(result.valid).toBe(false);
|
|
105
|
+
expect(result.errors.some((e) => e.includes('--spec-refs'))).toBe(true);
|
|
106
|
+
});
|
|
85
107
|
it('should return all errors at once, not fail on first error', () => {
|
|
86
108
|
const result = validateCreateSpec({
|
|
87
109
|
id: TEST_WU_ID,
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file wu-create.test.ts
|
|
3
|
+
* Tests for wu:create helpers and warnings (WU-1429)
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import { buildWUContent, collectInitiativeWarnings, validateCreateSpec } from '../wu-create.js';
|
|
7
|
+
const BASE_WU = {
|
|
8
|
+
id: 'WU-1429',
|
|
9
|
+
lane: 'Framework: CLI',
|
|
10
|
+
title: 'Test WU',
|
|
11
|
+
priority: 'P2',
|
|
12
|
+
type: 'feature',
|
|
13
|
+
created: '2026-02-04',
|
|
14
|
+
opts: {
|
|
15
|
+
description: 'Context: test context.\nProblem: test problem.\nSolution: test solution that exceeds minimum.',
|
|
16
|
+
acceptance: ['Acceptance criterion'],
|
|
17
|
+
exposure: 'backend-only',
|
|
18
|
+
codePaths: ['packages/@lumenflow/cli/src/wu-create.ts'],
|
|
19
|
+
testPathsUnit: ['packages/@lumenflow/cli/src/__tests__/wu-create.test.ts'],
|
|
20
|
+
specRefs: ['lumenflow://plans/WU-1429-plan.md'],
|
|
21
|
+
},
|
|
22
|
+
};
|
|
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
|
+
});
|
|
37
|
+
it('should persist notes when provided', () => {
|
|
38
|
+
const wu = buildWUContent({
|
|
39
|
+
...BASE_WU,
|
|
40
|
+
opts: {
|
|
41
|
+
...BASE_WU.opts,
|
|
42
|
+
notes: 'Implementation notes for test',
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
expect(wu.notes).toBe('Implementation notes for test');
|
|
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
|
+
});
|
|
83
|
+
it('should warn when initiative has phases but no --phase is provided', () => {
|
|
84
|
+
const warnings = collectInitiativeWarnings({
|
|
85
|
+
initiativeId: 'INIT-TEST',
|
|
86
|
+
initiativeDoc: {
|
|
87
|
+
phases: [{ id: 1, title: 'Phase 1' }],
|
|
88
|
+
},
|
|
89
|
+
phase: undefined,
|
|
90
|
+
specRefs: ['lumenflow://plans/WU-1429-plan.md'],
|
|
91
|
+
});
|
|
92
|
+
expect(warnings).toEqual(expect.arrayContaining([
|
|
93
|
+
'Initiative INIT-TEST has phases defined. Consider adding --phase to link this WU to a phase.',
|
|
94
|
+
]));
|
|
95
|
+
});
|
|
96
|
+
it('should warn when initiative has related_plan but no spec_refs', () => {
|
|
97
|
+
const warnings = collectInitiativeWarnings({
|
|
98
|
+
initiativeId: 'INIT-TEST',
|
|
99
|
+
initiativeDoc: {
|
|
100
|
+
related_plan: 'lumenflow://plans/INIT-TEST-plan.md',
|
|
101
|
+
},
|
|
102
|
+
phase: '1',
|
|
103
|
+
specRefs: [],
|
|
104
|
+
});
|
|
105
|
+
expect(warnings).toEqual(expect.arrayContaining([
|
|
106
|
+
'Initiative INIT-TEST has related_plan (lumenflow://plans/INIT-TEST-plan.md). Consider adding --spec-refs to link this WU to the plan.',
|
|
107
|
+
]));
|
|
108
|
+
});
|
|
109
|
+
it('should not warn when phase and spec_refs are provided', () => {
|
|
110
|
+
const warnings = collectInitiativeWarnings({
|
|
111
|
+
initiativeId: 'INIT-TEST',
|
|
112
|
+
initiativeDoc: {
|
|
113
|
+
phases: [{ id: 1, title: 'Phase 1' }],
|
|
114
|
+
related_plan: 'lumenflow://plans/INIT-TEST-plan.md',
|
|
115
|
+
},
|
|
116
|
+
phase: '1',
|
|
117
|
+
specRefs: ['lumenflow://plans/WU-1429-plan.md'],
|
|
118
|
+
});
|
|
119
|
+
expect(warnings).toEqual([]);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -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
|
+
});
|