@epoint-testtech/ep-stage-skill 0.0.3-alpha.2 → 0.0.4-alpha.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.
Files changed (109) hide show
  1. package/SKILL.md +2 -2
  2. package/codex-skill/ep-stage/glue-create-project/SKILL.md +186 -0
  3. package/codex-skill/ep-stage/glue-generate-testcase/SKILL.md +199 -0
  4. package/codex-skill/ep-stage/glue-generate-testcase/references/testcase-schema.md +112 -0
  5. package/codex-skill/ep-stage/glue-run-test/SKILL.md +249 -0
  6. package/codex-skill/ep-stage/glue-run-test/references/crud-pipeline.md +145 -0
  7. package/codex-skill/ep-stage/{glue-test → glue-run-test}/scripts/generate-crud-spec.mjs +3 -3
  8. package/codex-skill/ep-stage/recording-to-glue/SKILL.md +1 -0
  9. package/codex-skill/ep-stage/scripts/validate-skill.mjs +29 -7
  10. package/dist/src/cli/dev/extract-contract.d.ts +14 -0
  11. package/dist/src/cli/dev/extract-contract.d.ts.map +1 -0
  12. package/dist/src/cli/dev/extract-contract.js +114 -0
  13. package/dist/src/cli/generate-crud-contract.js +7 -77
  14. package/dist/src/cli/generate-playwright-tests.d.ts +0 -28
  15. package/dist/src/cli/generate-playwright-tests.d.ts.map +1 -1
  16. package/dist/src/cli/generate-playwright-tests.js +4 -81
  17. package/dist/src/cli/generate-testcase.d.ts +83 -0
  18. package/dist/src/cli/generate-testcase.d.ts.map +1 -0
  19. package/dist/src/cli/generate-testcase.js +197 -0
  20. package/dist/src/cli/index.d.ts +18 -0
  21. package/dist/src/cli/index.d.ts.map +1 -0
  22. package/dist/src/cli/index.js +55 -0
  23. package/dist/src/cli/probe.d.ts +44 -0
  24. package/dist/src/cli/probe.d.ts.map +1 -0
  25. package/dist/src/cli/probe.js +221 -0
  26. package/dist/src/cli/run-gap-pipeline.js +4 -0
  27. package/dist/src/cli/run.d.ts +63 -0
  28. package/dist/src/cli/run.d.ts.map +1 -0
  29. package/dist/src/cli/run.js +116 -0
  30. package/dist/src/cli/spec.d.ts +45 -0
  31. package/dist/src/cli/spec.d.ts.map +1 -0
  32. package/dist/src/cli/spec.js +74 -0
  33. package/dist/src/context/stage-context.d.ts +72 -8
  34. package/dist/src/context/stage-context.d.ts.map +1 -1
  35. package/dist/src/context/stage-context.js +61 -15
  36. package/dist/src/index.d.ts +2 -2
  37. package/dist/src/index.d.ts.map +1 -1
  38. package/dist/src/index.js +1 -1
  39. package/dist/src/testcase/testcase-generator.d.ts.map +1 -1
  40. package/dist/src/testcase/testcase-generator.js +4 -0
  41. package/dist/src/testcase/testcase-v2.d.ts +50 -0
  42. package/dist/src/testcase/testcase-v2.d.ts.map +1 -0
  43. package/dist/src/testcase/testcase-v2.js +1 -0
  44. package/dist/src/util/credentials.d.ts +12 -0
  45. package/dist/src/util/credentials.d.ts.map +1 -0
  46. package/dist/src/util/credentials.js +19 -0
  47. package/dist/src/util/i18n-testcase.d.ts +8 -0
  48. package/dist/src/util/i18n-testcase.d.ts.map +1 -0
  49. package/dist/src/util/i18n-testcase.js +55 -0
  50. package/dist/src/util/softlink.d.ts +33 -0
  51. package/dist/src/util/softlink.d.ts.map +1 -0
  52. package/dist/src/util/softlink.js +43 -0
  53. package/dist/src/validation/credentials.d.ts +19 -0
  54. package/dist/src/validation/credentials.d.ts.map +1 -0
  55. package/dist/src/validation/credentials.js +38 -0
  56. package/dist/src/validation/index.d.ts +5 -0
  57. package/dist/src/validation/index.d.ts.map +1 -0
  58. package/dist/src/validation/index.js +3 -0
  59. package/dist/src/validation/projects-index.d.ts +13 -0
  60. package/dist/src/validation/projects-index.d.ts.map +1 -0
  61. package/dist/src/validation/projects-index.js +37 -0
  62. package/dist/src/validation/testcase.d.ts +13 -0
  63. package/dist/src/validation/testcase.d.ts.map +1 -0
  64. package/dist/src/validation/testcase.js +53 -0
  65. package/dist/test/cli/extract-contract.test.d.ts +2 -0
  66. package/dist/test/cli/extract-contract.test.d.ts.map +1 -0
  67. package/dist/test/cli/extract-contract.test.js +32 -0
  68. package/dist/test/cli/generate-testcase.test.d.ts +2 -0
  69. package/dist/test/cli/generate-testcase.test.d.ts.map +1 -0
  70. package/dist/test/cli/generate-testcase.test.js +130 -0
  71. package/dist/test/cli/index.test.d.ts +2 -0
  72. package/dist/test/cli/index.test.d.ts.map +1 -0
  73. package/dist/test/cli/index.test.js +93 -0
  74. package/dist/test/cli/run.test.d.ts +2 -0
  75. package/dist/test/cli/run.test.d.ts.map +1 -0
  76. package/dist/test/cli/run.test.js +149 -0
  77. package/dist/test/cli/spec.test.d.ts +2 -0
  78. package/dist/test/cli/spec.test.d.ts.map +1 -0
  79. package/dist/test/cli/spec.test.js +196 -0
  80. package/dist/test/stage-context.test.js +145 -13
  81. package/dist/test/util/credentials.test.d.ts +2 -0
  82. package/dist/test/util/credentials.test.d.ts.map +1 -0
  83. package/dist/test/util/credentials.test.js +64 -0
  84. package/dist/test/util/i18n-testcase.test.d.ts +2 -0
  85. package/dist/test/util/i18n-testcase.test.d.ts.map +1 -0
  86. package/dist/test/util/i18n-testcase.test.js +119 -0
  87. package/dist/test/util/softlink.test.d.ts +2 -0
  88. package/dist/test/util/softlink.test.d.ts.map +1 -0
  89. package/dist/test/util/softlink.test.js +82 -0
  90. package/dist/test/validation/credentials.test.d.ts +2 -0
  91. package/dist/test/validation/credentials.test.d.ts.map +1 -0
  92. package/dist/test/validation/credentials.test.js +72 -0
  93. package/dist/test/validation/projects-index.test.d.ts +2 -0
  94. package/dist/test/validation/projects-index.test.d.ts.map +1 -0
  95. package/dist/test/validation/projects-index.test.js +48 -0
  96. package/dist/test/validation/testcase.test.d.ts +2 -0
  97. package/dist/test/validation/testcase.test.d.ts.map +1 -0
  98. package/dist/test/validation/testcase.test.js +129 -0
  99. package/docs/README.md +6 -6
  100. package/docs/mvp-usage-guide.md +3 -3
  101. package/package.json +9 -4
  102. package/codex-skill/ep-stage/create-project/SKILL.md +0 -59
  103. package/codex-skill/ep-stage/glue-test/SKILL.md +0 -258
  104. package/codex-skill/ep-stage/glue-test/references/crud-pipeline.md +0 -139
  105. package/codex-skill/ep-stage/glue-testcase/SKILL.md +0 -31
  106. package/codex-skill/ep-stage/glue-testcase/references/testcase-schema.md +0 -67
  107. /package/codex-skill/ep-stage/{glue-testcase → glue-generate-testcase}/examples/observable-testcase.json +0 -0
  108. /package/codex-skill/ep-stage/{glue-test → glue-run-test}/references/gap-review-protocol.md +0 -0
  109. /package/codex-skill/ep-stage/{glue-test → glue-run-test}/references/harness-principles.md +0 -0
@@ -0,0 +1,196 @@
1
+ /**
2
+ * spec.ts 单元测试(REQ-SPEC-01)。
3
+ *
4
+ * 覆盖:
5
+ * - parseSpecArgs 缺参 / 非法 / -- 分隔符
6
+ * - runSpec:
7
+ * · 缺 contract.json 时报错(引导用户先跑 testcase 子命令);
8
+ * · 正确装配 v1 .spec.ts(含 SKELETON_MARKER_LINE 头部标记 + skeleton 主体);
9
+ * · testcase.json5 不通过 validateTestcase 时抛错(透传 v2 schema 校验)。
10
+ */
11
+ import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
12
+ import os from 'node:os';
13
+ import path from 'node:path';
14
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
15
+ import { parseSpecArgs, runSpec } from '../../src/cli/spec.js';
16
+ let tmpRoot;
17
+ beforeEach(() => {
18
+ tmpRoot = mkdtempSync(path.join(os.tmpdir(), 'ep-stage-spec-'));
19
+ });
20
+ afterEach(() => {
21
+ // 留 tmpdir 给宿主清理,避免删 fixture 时误命中其他测试。
22
+ });
23
+ function createContract() {
24
+ return {
25
+ contractVersion: 'crud-business-module/v1',
26
+ module: { id: 'zwplace', label: '场所窗口信息管理' },
27
+ pages: {
28
+ list: {
29
+ role: 'list',
30
+ pageId: 'gxhzwplacelist',
31
+ title: '场所窗口信息列表',
32
+ iframeSrcKeyword: 'gxhzwplacelist',
33
+ fields: [
34
+ { name: 'windowname', label: '窗口名称', controlType: 'minitext', required: false, availability: 'auto_extractable', sources: [] },
35
+ ],
36
+ buttons: [],
37
+ grids: [],
38
+ dialogs: [],
39
+ sources: [],
40
+ },
41
+ add: {
42
+ role: 'add',
43
+ pageId: 'gxhzwplaceadd',
44
+ title: '新增场所窗口',
45
+ iframeSrcKeyword: 'gxhzwplaceadd',
46
+ fields: [
47
+ { name: 'windowname', label: '窗口名称', controlType: 'minitext', required: true, availability: 'auto_extractable', sources: [] },
48
+ ],
49
+ buttons: [],
50
+ grids: [],
51
+ dialogs: [],
52
+ sources: [],
53
+ },
54
+ edit: {
55
+ role: 'edit',
56
+ pageId: 'gxhzwplaceedit',
57
+ title: '修改场所窗口',
58
+ iframeSrcKeyword: 'gxhzwplaceedit',
59
+ fields: [
60
+ { name: 'windowname', label: '窗口名称', controlType: 'minitext', required: true, availability: 'auto_extractable', sources: [] },
61
+ ],
62
+ buttons: [],
63
+ grids: [],
64
+ dialogs: [],
65
+ sources: [],
66
+ },
67
+ },
68
+ dataKey: {
69
+ field: 'windowname',
70
+ label: '窗口名称',
71
+ availability: 'derivable',
72
+ generationStrategy: 'timestamp_prefix',
73
+ sources: [],
74
+ },
75
+ searchConditions: [
76
+ {
77
+ field: 'windowname',
78
+ label: '窗口名称',
79
+ component: 'input',
80
+ seedValue: '自动化测试窗口',
81
+ generationStrategy: 'timestamp_prefix',
82
+ sources: [],
83
+ },
84
+ ],
85
+ flows: {
86
+ create: {
87
+ entryButton: { pageRole: 'list', label: '新增', sources: [] },
88
+ targetPageId: 'gxhzwplaceadd',
89
+ saveButton: { pageRole: 'add', label: '保存并关闭', sources: [] },
90
+ overrideFields: [],
91
+ },
92
+ search: {
93
+ searchField: { pageRole: 'list', field: 'windowname', label: '窗口名称', sources: [] },
94
+ submitControl: { pageRole: 'list', label: '查询', sources: [] },
95
+ resultGrid: 'datagrid',
96
+ },
97
+ update: {
98
+ entryAction: { pageRole: 'list', label: '修改', sources: [] },
99
+ targetPageId: 'gxhzwplaceedit',
100
+ updateField: { pageRole: 'edit', field: 'windowname', label: '窗口名称', sources: [] },
101
+ saveButton: { pageRole: 'edit', label: '保存并关闭', sources: [] },
102
+ },
103
+ delete: {
104
+ entryButton: { pageRole: 'list', label: '删除', sources: [] },
105
+ expectedOutcome: 'success_delete',
106
+ confirmControl: { pageRole: 'list', label: '确定', sources: [] },
107
+ },
108
+ },
109
+ assertions: [],
110
+ businessRules: [],
111
+ unresolvedSlots: [],
112
+ };
113
+ }
114
+ function createTestcase() {
115
+ return {
116
+ schemaVersion: 'v2',
117
+ scenario: 'crud.single-page',
118
+ moduleId: 'zwplace',
119
+ moduleName: '场所窗口信息管理',
120
+ menuPath: '业务管理 > 场所窗口',
121
+ requiredSystems: [{ url: '' }],
122
+ requiredRoles: [{ role: 'admin' }],
123
+ coveredActions: [
124
+ { actionId: 'create', kind: '新增', evidence: ['contract=crud-business-module/v1'] },
125
+ ],
126
+ uncoveredCandidates: [],
127
+ cases: [
128
+ {
129
+ caseId: 'zwplace.create',
130
+ title: '新增',
131
+ scenario: 'crud.single-page',
132
+ reviewStatus: 'needs_review',
133
+ requiredRole: 'admin',
134
+ evidence: ['contract=crud-business-module/v1'],
135
+ steps: [],
136
+ assertions: [],
137
+ unresolved: [],
138
+ },
139
+ ],
140
+ reasoningSummary: {
141
+ conclusion: '',
142
+ evidenceChain: [],
143
+ alternatives: [],
144
+ confidence: 'medium',
145
+ risks: [],
146
+ needsHumanReview: true,
147
+ },
148
+ };
149
+ }
150
+ function writeFixtures() {
151
+ const testsDir = path.join(tmpRoot, 'requirement-X');
152
+ mkdirSync(path.join(testsDir, '.ep-stage'), { recursive: true });
153
+ const testcasePath = path.join(testsDir, 'testcase.json5');
154
+ const contractPath = path.join(testsDir, '.ep-stage', 'contract.json');
155
+ writeFileSync(testcasePath, JSON.stringify(createTestcase()), 'utf8');
156
+ writeFileSync(contractPath, JSON.stringify(createContract()), 'utf8');
157
+ return { testcasePath, specOut: path.join(testsDir, 'crud.spec.ts') };
158
+ }
159
+ describe('parseSpecArgs', () => {
160
+ it('缺 --testcase 时抛错', () => {
161
+ expect(() => parseSpecArgs(['--out', '/x'])).toThrow(/缺少 --testcase/);
162
+ });
163
+ it('缺 --out 时抛错', () => {
164
+ expect(() => parseSpecArgs(['--testcase', '/x'])).toThrow(/缺少 --out/);
165
+ });
166
+ it('参数对非法时抛错', () => {
167
+ expect(() => parseSpecArgs(['--testcase'])).toThrow(/无效参数对/);
168
+ });
169
+ it('-- 分隔符被忽略', () => {
170
+ const args = parseSpecArgs(['--', '--testcase', '/a', '--out', '/b']);
171
+ expect(args).toEqual({ testcase: '/a', out: '/b' });
172
+ });
173
+ });
174
+ describe('runSpec', () => {
175
+ it('正常装配 v1 .spec.ts(含 SKELETON_MARKER_LINE)', () => {
176
+ const { testcasePath, specOut } = writeFixtures();
177
+ const result = runSpec({ testcase: testcasePath, out: specOut });
178
+ expect(result).toEqual({ specPath: specOut, version: 'v1' });
179
+ const body = readFileSync(specOut, 'utf8');
180
+ expect(body).toMatch(/^\/\/ === 胶水模式生成(基于 contract \+ coveredActions)===/);
181
+ // skeleton 主体(来自 generateStageSkeletonCrudSpec)应含 Playwright import 或 test 入口
182
+ expect(body.length).toBeGreaterThan(100);
183
+ });
184
+ it('缺 .ep-stage/contract.json 时抛错引导', () => {
185
+ const testsDir = path.join(tmpRoot, 'requirement-Y');
186
+ mkdirSync(testsDir, { recursive: true });
187
+ const testcasePath = path.join(testsDir, 'testcase.json5');
188
+ writeFileSync(testcasePath, JSON.stringify(createTestcase()), 'utf8');
189
+ expect(() => runSpec({ testcase: testcasePath, out: path.join(testsDir, 'out.spec.ts') })).toThrow(/未找到.*contract\.json/);
190
+ });
191
+ it('testcase.json5 schemaVersion 不是 v2 时由 validateTestcase 抛错', () => {
192
+ const { testcasePath, specOut } = writeFixtures();
193
+ writeFileSync(testcasePath, JSON.stringify({ schemaVersion: 'v1' }), 'utf8');
194
+ expect(() => runSpec({ testcase: testcasePath, out: specOut })).toThrow(/schemaVersion 不是 v2/);
195
+ });
196
+ });
@@ -3,7 +3,7 @@ import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import JSON5 from 'json5';
5
5
  import { describe, expect, it } from 'vitest';
6
- import { readStageContext, resolveStageContext, writeProjectIndex, } from '../src/index.js';
6
+ import { lookupProjectByRequirement, readStageContext, resolveStageContext, writeProjectIndex, } from '../src/index.js';
7
7
  /**
8
8
  * 创建隔离的临时目录,避免测试读写真实用户 HOME 或项目文件。
9
9
  *
@@ -44,7 +44,8 @@ describe('stage context resolution', () => {
44
44
  codeListPaths: [path.resolve(projectDir, '../knowledge-project/_docs/code_list.md')],
45
45
  });
46
46
  });
47
- it('resolves current project from user index by code list path when local context is absent', () => {
47
+ it('resolves current project from legacy user index by code list path when local context is absent', () => {
48
+ // 旧链路:legacy projects.index.json5 含 envPath/knowledgeRoot/codeListPaths,仍需可读(REQ-DATA-01 graceful read)。
48
49
  const homeDir = createTempDir('ep-stage-home');
49
50
  const projectDir = createTempDir('ep-stage-project');
50
51
  const cwd = createTempDir('ep-stage-outside-cwd');
@@ -55,19 +56,25 @@ describe('stage context resolution', () => {
55
56
  mkdirSync(path.dirname(codeListPath), { recursive: true });
56
57
  writeFileSync(path.join(projectDir, '.env'), 'LOGIN_SYSTEM_URL=http://example.test/login\n');
57
58
  writeFileSync(codeListPath, '# code list\n');
58
- writeProjectIndex({
59
- homeDir,
59
+ // 直接写 legacy schema(含 envPath/knowledgeRoot/codeListPaths),模拟旧版数据残留。
60
+ const indexDir = path.join(homeDir, '.ep-stage');
61
+ mkdirSync(indexDir, { recursive: true });
62
+ writeFileSync(path.join(indexDir, 'projects.index.json5'), JSON5.stringify({
60
63
  projects: [
61
64
  {
62
65
  projectName: 'indexed-project',
63
66
  projectDir,
64
67
  mode: 'glue',
68
+ stageCreateVersion: '0.0.4-alpha.0',
69
+ requirements: [],
70
+ // legacy 字段
65
71
  envPath: '.env',
66
72
  knowledgeRoot,
67
73
  codeListPaths: [codeListPath],
68
74
  },
69
75
  ],
70
- });
76
+ systems: [],
77
+ }, null, 2));
71
78
  const result = resolveStageContext({
72
79
  cwd,
73
80
  codeListPath,
@@ -91,7 +98,8 @@ describe('stage context resolution', () => {
91
98
  },
92
99
  });
93
100
  });
94
- it('uses stage-context.md as source of truth when user index has stageContextPath', () => {
101
+ it('uses stage-context.md as source of truth when legacy user index has stageContextPath', () => {
102
+ // 旧链路:legacy projects.index.json5 含 stageContextPath 时,回到 stage-context.md 作为真相源(graceful read)。
95
103
  const homeDir = createTempDir('ep-stage-home-source');
96
104
  const projectDir = createTempDir('ep-stage-source-project');
97
105
  const oldKnowledgeRoot = path.join(projectDir, 'old knowledge');
@@ -114,20 +122,25 @@ describe('stage context resolution', () => {
114
122
  '',
115
123
  '# context-project',
116
124
  ].join('\n'));
117
- writeProjectIndex({
118
- homeDir,
125
+ const indexDir = path.join(homeDir, '.ep-stage');
126
+ mkdirSync(indexDir, { recursive: true });
127
+ writeFileSync(path.join(indexDir, 'projects.index.json5'), JSON5.stringify({
119
128
  projects: [
120
129
  {
121
130
  projectName: 'stale-index-project',
122
131
  projectDir,
123
132
  mode: 'glue',
133
+ stageCreateVersion: '0.0.4-alpha.0',
134
+ requirements: [],
135
+ // legacy 字段
124
136
  envPath: 'old.env',
125
137
  knowledgeRoot: oldKnowledgeRoot,
126
138
  codeListPaths: [codeListPath],
127
139
  stageContextPath,
128
140
  },
129
141
  ],
130
- });
142
+ systems: [],
143
+ }, null, 2));
131
144
  const result = resolveStageContext({
132
145
  cwd: createTempDir('ep-stage-source-outside'),
133
146
  codeListPath,
@@ -167,12 +180,16 @@ describe('stage context resolution', () => {
167
180
  projectName: 'json5-project',
168
181
  projectDir: '${projectDir}',
169
182
  mode: 'glue',
183
+ stageCreateVersion: '0.0.4-alpha.0',
184
+ requirements: [],
185
+ // legacy 字段(graceful read,向后兼容)
170
186
  envPath: '.env',
171
187
  codeListPaths: [
172
188
  '${codeListPath}',
173
189
  ],
174
190
  },
175
191
  ],
192
+ systems: [],
176
193
  }`);
177
194
  const result = resolveStageContext({
178
195
  projectDir,
@@ -213,7 +230,7 @@ describe('stage context resolution', () => {
213
230
  },
214
231
  });
215
232
  });
216
- it('writes v1 project index and keeps reading legacy JSON5 index files', () => {
233
+ it('writes new schema project index (no version field, with systems[]) and keeps reading legacy JSON5', () => {
217
234
  const homeDir = createTempDir('ep-stage-home-version');
218
235
  const projectDir = createTempDir('ep-stage-version-project');
219
236
  mkdirSync(projectDir, { recursive: true });
@@ -224,20 +241,31 @@ describe('stage context resolution', () => {
224
241
  projectName: 'versioned-project',
225
242
  projectDir,
226
243
  mode: 'glue',
227
- envPath: '.env',
244
+ stageCreateVersion: '0.0.4-alpha.0',
245
+ requirements: [],
228
246
  },
229
247
  ],
230
248
  });
231
249
  const parsedIndex = JSON5.parse(readFileSync(indexPath, 'utf8'));
250
+ // 新 schema:顶层无 version、含 systems[]、projects[] 不含 legacy 字段。
232
251
  expect(parsedIndex).toMatchObject({
233
- version: 1,
234
252
  projects: [
235
253
  {
236
254
  projectName: 'versioned-project',
237
255
  projectDir,
256
+ mode: 'glue',
257
+ stageCreateVersion: '0.0.4-alpha.0',
258
+ requirements: [],
238
259
  },
239
260
  ],
261
+ systems: [],
240
262
  });
263
+ expect(parsedIndex.version).toBeUndefined();
264
+ expect(parsedIndex.projects[0].envPath).toBeUndefined();
265
+ expect(parsedIndex.projects[0].knowledgeRoot).toBeUndefined();
266
+ expect(parsedIndex.projects[0].codeListPaths).toBeUndefined();
267
+ expect(parsedIndex.projects[0].stageContextPath).toBeUndefined();
268
+ // legacy 数据:旧版 index 不带 systems 字段,仍要可读(graceful read)。
241
269
  const legacyHomeDir = createTempDir('ep-stage-legacy-version');
242
270
  const legacyProjectDir = createTempDir('ep-stage-legacy-project');
243
271
  const legacyIndexDir = path.join(legacyHomeDir, '.ep-stage');
@@ -245,7 +273,7 @@ describe('stage context resolution', () => {
245
273
  mkdirSync(legacyIndexDir, { recursive: true });
246
274
  writeFileSync(path.join(legacyProjectDir, '.env'), 'LOGIN_USERNAME=demo-user\n');
247
275
  writeFileSync(path.join(legacyIndexDir, 'projects.index.json5'), `{
248
- // legacy index without version
276
+ // legacy index without version / systems
249
277
  projects: [
250
278
  {
251
279
  projectName: 'legacy-project',
@@ -261,3 +289,107 @@ describe('stage context resolution', () => {
261
289
  }).context.projectName).toBe('legacy-project');
262
290
  });
263
291
  });
292
+ describe('lookupProjectByRequirement (REQ-DATA-01)', () => {
293
+ it('returns null when projects.index.json5 is missing', () => {
294
+ const homeDir = createTempDir('ep-stage-lookup-missing');
295
+ // 不创建 ~/.ep-stage 目录
296
+ const result = lookupProjectByRequirement({
297
+ requirementPath: path.join(homeDir, 'no-such-requirement'),
298
+ homeDir,
299
+ });
300
+ expect(result).toBeNull();
301
+ });
302
+ it('matches when requirementPath exactly equals a requirement requirementDir', () => {
303
+ const homeDir = createTempDir('ep-stage-lookup-req');
304
+ const projectDir = createTempDir('ep-stage-lookup-projdir');
305
+ const requirementDir = path.join(projectDir, 'requirements', 'req-001');
306
+ const testsDir = path.join(projectDir, 'tests', 'req-001');
307
+ writeProjectIndex({
308
+ homeDir,
309
+ projects: [
310
+ {
311
+ projectName: 'lookup-project',
312
+ projectDir,
313
+ mode: 'glue',
314
+ stageCreateVersion: '0.0.4-alpha.0',
315
+ requirements: [
316
+ {
317
+ name: 'req-001',
318
+ requirementDir,
319
+ testsDir,
320
+ },
321
+ ],
322
+ },
323
+ ],
324
+ });
325
+ const result = lookupProjectByRequirement({ requirementPath: requirementDir, homeDir });
326
+ expect(result).not.toBeNull();
327
+ expect(result?.projectName).toBe('lookup-project');
328
+ expect(result?.requirements[0]?.name).toBe('req-001');
329
+ });
330
+ it('matches when requirementPath exactly equals a requirement testsDir', () => {
331
+ const homeDir = createTempDir('ep-stage-lookup-tests');
332
+ const projectDir = createTempDir('ep-stage-lookup-projdir');
333
+ const requirementDir = path.join(projectDir, 'requirements', 'req-002');
334
+ const testsDir = path.join(projectDir, 'tests', 'req-002');
335
+ writeProjectIndex({
336
+ homeDir,
337
+ projects: [
338
+ {
339
+ projectName: 'lookup-project',
340
+ projectDir,
341
+ mode: 'glue',
342
+ stageCreateVersion: '0.0.4-alpha.0',
343
+ requirements: [
344
+ {
345
+ name: 'req-002',
346
+ requirementDir,
347
+ testsDir,
348
+ },
349
+ ],
350
+ },
351
+ ],
352
+ });
353
+ const result = lookupProjectByRequirement({ requirementPath: testsDir, homeDir });
354
+ expect(result).not.toBeNull();
355
+ expect(result?.projectName).toBe('lookup-project');
356
+ });
357
+ it('matches when requirementPath exactly equals a project projectDir', () => {
358
+ const homeDir = createTempDir('ep-stage-lookup-pd');
359
+ const projectDir = createTempDir('ep-stage-lookup-projdir-only');
360
+ writeProjectIndex({
361
+ homeDir,
362
+ projects: [
363
+ {
364
+ projectName: 'projdir-only',
365
+ projectDir,
366
+ mode: 'glue',
367
+ stageCreateVersion: '0.0.4-alpha.0',
368
+ requirements: [],
369
+ },
370
+ ],
371
+ });
372
+ const result = lookupProjectByRequirement({ requirementPath: projectDir, homeDir });
373
+ expect(result).not.toBeNull();
374
+ expect(result?.projectName).toBe('projdir-only');
375
+ });
376
+ it('returns null when no project matches', () => {
377
+ const homeDir = createTempDir('ep-stage-lookup-nomatch');
378
+ const projectDir = createTempDir('ep-stage-lookup-projdir');
379
+ writeProjectIndex({
380
+ homeDir,
381
+ projects: [
382
+ {
383
+ projectName: 'unrelated-project',
384
+ projectDir,
385
+ mode: 'glue',
386
+ stageCreateVersion: '0.0.4-alpha.0',
387
+ requirements: [],
388
+ },
389
+ ],
390
+ });
391
+ const otherDir = createTempDir('ep-stage-lookup-other');
392
+ const result = lookupProjectByRequirement({ requirementPath: otherDir, homeDir });
393
+ expect(result).toBeNull();
394
+ });
395
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=credentials.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials.test.d.ts","sourceRoot":"","sources":["../../../test/util/credentials.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,64 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { loadCredentials } from '../../src/util/credentials.js';
6
+ /**
7
+ * REQ-ENV-02 / REQ-ENV-03:从 <需求名>/credentials.json5 加载数组形态凭据。
8
+ *
9
+ * 不重复实现 schema 校验(已由 validateCredentials 覆盖),本测试关注 IO 行为:
10
+ * 文件读取、JSON5 解析、对校验器的组合调用,以及缺失场景的友好错误。
11
+ */
12
+ describe('loadCredentials', () => {
13
+ let tmpDir;
14
+ let credPath;
15
+ beforeEach(() => {
16
+ tmpDir = mkdtempSync(path.join(os.tmpdir(), 'credentials-test-'));
17
+ credPath = path.join(tmpDir, 'credentials.json5');
18
+ });
19
+ afterEach(() => {
20
+ rmSync(tmpDir, { recursive: true, force: true });
21
+ });
22
+ it('读合法数组形态凭据返回 Credential[]', () => {
23
+ writeFileSync(credPath, `[
24
+ // 普通账号
25
+ { url: 'https://demo.example.com', username: 'alice', password: 'p1', role: 'admin', systemName: '后台' },
26
+ { url: 'https://demo.example.com', username: 'bob', password: 'p2', role: 'user' },
27
+ ]`);
28
+ const list = loadCredentials(credPath);
29
+ expect(list).toHaveLength(2);
30
+ expect(list[0]).toMatchObject({
31
+ url: 'https://demo.example.com',
32
+ username: 'alice',
33
+ password: 'p1',
34
+ role: 'admin',
35
+ systemName: '后台',
36
+ });
37
+ expect(list[1]).toMatchObject({ username: 'bob', role: 'user' });
38
+ expect(list[1].systemName).toBeUndefined();
39
+ });
40
+ it('单条凭据无可选字段也能解析', () => {
41
+ writeFileSync(credPath, `[{ url: 'https://x.test', username: 'u', password: 'p' }]`);
42
+ const list = loadCredentials(credPath);
43
+ expect(list).toHaveLength(1);
44
+ expect(list[0].role).toBeUndefined();
45
+ expect(list[0].systemName).toBeUndefined();
46
+ });
47
+ it('缺必填字段 url 时抛 schema 错误(来自 validateCredentials)', () => {
48
+ writeFileSync(credPath, `[{ username: 'u', password: 'p' }]`);
49
+ expect(() => loadCredentials(credPath)).toThrow(/缺必填字段 url/);
50
+ });
51
+ it('顶层不是数组时抛 schema 错误', () => {
52
+ writeFileSync(credPath, `{ url: 'x', username: 'u', password: 'p' }`);
53
+ expect(() => loadCredentials(credPath)).toThrow(/顶层必须是数组/);
54
+ });
55
+ it('文件不存在时抛带路径的友好错误', () => {
56
+ const missing = path.join(tmpDir, 'not-here.json5');
57
+ expect(() => loadCredentials(missing)).toThrow(/credentials\.json5 不存在/);
58
+ expect(() => loadCredentials(missing)).toThrow(new RegExp(missing.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
59
+ });
60
+ it('JSON5 语法错误时抛解析错误', () => {
61
+ writeFileSync(credPath, `[ { url: 'x', username: 'u', password: 'p' `); // 缺右括号
62
+ expect(() => loadCredentials(credPath)).toThrow();
63
+ });
64
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=i18n-testcase.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i18n-testcase.test.d.ts","sourceRoot":"","sources":["../../../test/util/i18n-testcase.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,119 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { i18nTestcase } from '../../src/util/i18n-testcase.js';
3
+ /**
4
+ * REQ-UX-03:testcase.json5 英文字段名 / 状态值 → testcase.md 中文显示文案。
5
+ * 映射表见 spec §4.6。CLI 写 testcase.md 时调用本 util;testcase.json5 内部字段名永远英文。
6
+ *
7
+ * 说明:`unresolved` 在 spec 表中同时出现于「字段名」(未确认项) 和「状态值」(未解决),
8
+ * 这是已知冲突;当前实现以扁平 lookup 表为准(后者覆盖前者),见 i18nTestcase 注释。
9
+ */
10
+ describe('i18nTestcase', () => {
11
+ describe('审阅状态值', () => {
12
+ it('confirmed → 已确认', () => {
13
+ expect(i18nTestcase('confirmed')).toBe('已确认');
14
+ });
15
+ it('needs_review → 待审阅', () => {
16
+ expect(i18nTestcase('needs_review')).toBe('待审阅');
17
+ });
18
+ it('blocked → 已阻断', () => {
19
+ expect(i18nTestcase('blocked')).toBe('已阻断');
20
+ });
21
+ it('planned → 已计划', () => {
22
+ expect(i18nTestcase('planned')).toBe('已计划');
23
+ });
24
+ it('candidate → 候选', () => {
25
+ expect(i18nTestcase('candidate')).toBe('候选');
26
+ });
27
+ it('resolved → 已解决', () => {
28
+ expect(i18nTestcase('resolved')).toBe('已解决');
29
+ });
30
+ });
31
+ describe('推荐动作值', () => {
32
+ it('infer → 推理', () => {
33
+ expect(i18nTestcase('infer')).toBe('推理');
34
+ });
35
+ it('supplement-hints → 补充 hints', () => {
36
+ expect(i18nTestcase('supplement-hints')).toBe('补充 hints');
37
+ });
38
+ it('skip → 跳过', () => {
39
+ expect(i18nTestcase('skip')).toBe('跳过');
40
+ });
41
+ });
42
+ describe('场景值', () => {
43
+ it('crud.single-page → 单页 CRUD', () => {
44
+ expect(i18nTestcase('crud.single-page')).toBe('单页 CRUD');
45
+ });
46
+ it('crud.nested → 嵌套 CRUD', () => {
47
+ expect(i18nTestcase('crud.nested')).toBe('嵌套 CRUD');
48
+ });
49
+ it('statistic → 统计视图', () => {
50
+ expect(i18nTestcase('statistic')).toBe('统计视图');
51
+ });
52
+ it('approval → 审批流', () => {
53
+ expect(i18nTestcase('approval')).toBe('审批流');
54
+ });
55
+ });
56
+ describe('字段名', () => {
57
+ it('caseId → 用例 ID', () => {
58
+ expect(i18nTestcase('caseId')).toBe('用例 ID');
59
+ });
60
+ it('actionId → 动作 ID', () => {
61
+ expect(i18nTestcase('actionId')).toBe('动作 ID');
62
+ });
63
+ it('scenario → 场景类型', () => {
64
+ expect(i18nTestcase('scenario')).toBe('场景类型');
65
+ });
66
+ it('menuPath → 菜单路径', () => {
67
+ expect(i18nTestcase('menuPath')).toBe('菜单路径');
68
+ });
69
+ it('requiredRole → 操作角色', () => {
70
+ expect(i18nTestcase('requiredRole')).toBe('操作角色');
71
+ });
72
+ it('requiredSystems → 需求系统集', () => {
73
+ expect(i18nTestcase('requiredSystems')).toBe('需求系统集');
74
+ });
75
+ it('requiredRoles → 需求角色集', () => {
76
+ expect(i18nTestcase('requiredRoles')).toBe('需求角色集');
77
+ });
78
+ it('coveredActions → 骨架已命中动作', () => {
79
+ expect(i18nTestcase('coveredActions')).toBe('骨架已命中动作');
80
+ });
81
+ it('uncoveredCandidates → 骨架未命中候选', () => {
82
+ expect(i18nTestcase('uncoveredCandidates')).toBe('骨架未命中候选');
83
+ });
84
+ it('businessIntent → 业务意图', () => {
85
+ expect(i18nTestcase('businessIntent')).toBe('业务意图');
86
+ });
87
+ it('assertionExpectation → 预期断言', () => {
88
+ expect(i18nTestcase('assertionExpectation')).toBe('预期断言');
89
+ });
90
+ it('recommendedAction → 推荐动作', () => {
91
+ expect(i18nTestcase('recommendedAction')).toBe('推荐动作');
92
+ });
93
+ it('reviewStatus → 审阅状态', () => {
94
+ expect(i18nTestcase('reviewStatus')).toBe('审阅状态');
95
+ });
96
+ it('evidence → 证据', () => {
97
+ expect(i18nTestcase('evidence')).toBe('证据');
98
+ });
99
+ });
100
+ describe('未知值兜底', () => {
101
+ it('未知值原样返回', () => {
102
+ expect(i18nTestcase('some-unknown-value')).toBe('some-unknown-value');
103
+ });
104
+ it('空字符串原样返回', () => {
105
+ expect(i18nTestcase('')).toBe('');
106
+ });
107
+ it('英文字段名未在表中时原样返回', () => {
108
+ expect(i18nTestcase('createdAt')).toBe('createdAt');
109
+ });
110
+ });
111
+ describe('unresolved 冲突(已知 trade-off)', () => {
112
+ // spec §4.6 中 `unresolved` 同时是字段名(未确认项)和状态值(未解决)。
113
+ // 扁平 lookup 表无法同时持有两个映射,后定义的状态值覆盖字段名。
114
+ // 字段名场景下 CLI 可在调用前对 key 做硬编码标签替换;此 util 保留单值兜底。
115
+ it('unresolved → 未解决(状态值优先)', () => {
116
+ expect(i18nTestcase('unresolved')).toBe('未解决');
117
+ });
118
+ });
119
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=softlink.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"softlink.test.d.ts","sourceRoot":"","sources":["../../../test/util/softlink.test.ts"],"names":[],"mappings":""}