@epoint-testtech/ep-stage-skill 0.0.3-alpha.2 → 0.0.4-alpha.1

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 (155) hide show
  1. package/SKILL.md +2 -2
  2. package/codex-skill/ep-stage/.omc/state/sessions/d636ad35-5fc9-4d5e-85b0-2bcad7d86ee5/pre-tool-advisory-throttle.json +10 -0
  3. package/codex-skill/ep-stage/glue-create-project/SKILL.md +206 -0
  4. package/codex-skill/ep-stage/glue-generate-testcase/SKILL.md +274 -0
  5. package/codex-skill/ep-stage/glue-generate-testcase/references/testcase-schema.md +112 -0
  6. package/codex-skill/ep-stage/glue-generate-testcase/references/testcase-tpl.md +72 -0
  7. package/codex-skill/ep-stage/glue-run-test/SKILL.md +289 -0
  8. package/codex-skill/ep-stage/glue-run-test/references/crud-pipeline.md +145 -0
  9. package/codex-skill/ep-stage/{glue-test → glue-run-test}/scripts/generate-crud-spec.mjs +3 -3
  10. package/codex-skill/ep-stage/recording-to-glue/SKILL.md +1 -0
  11. package/codex-skill/ep-stage/scripts/validate-skill.mjs +29 -7
  12. package/dist/src/cli/dev/extract-contract.d.ts +14 -0
  13. package/dist/src/cli/dev/extract-contract.d.ts.map +1 -0
  14. package/dist/src/cli/dev/extract-contract.js +114 -0
  15. package/dist/src/cli/generate-crud-contract.js +7 -77
  16. package/dist/src/cli/generate-playwright-tests.d.ts +0 -28
  17. package/dist/src/cli/generate-playwright-tests.d.ts.map +1 -1
  18. package/dist/src/cli/generate-playwright-tests.js +4 -81
  19. package/dist/src/cli/generate-testcase.d.ts +84 -0
  20. package/dist/src/cli/generate-testcase.d.ts.map +1 -0
  21. package/dist/src/cli/generate-testcase.js +300 -0
  22. package/dist/src/cli/index.d.ts +26 -0
  23. package/dist/src/cli/index.d.ts.map +1 -0
  24. package/dist/src/cli/index.js +75 -0
  25. package/dist/src/cli/probe.d.ts +46 -0
  26. package/dist/src/cli/probe.d.ts.map +1 -0
  27. package/dist/src/cli/probe.js +447 -0
  28. package/dist/src/cli/run-gap-pipeline.js +4 -0
  29. package/dist/src/cli/run.d.ts +63 -0
  30. package/dist/src/cli/run.d.ts.map +1 -0
  31. package/dist/src/cli/run.js +116 -0
  32. package/dist/src/cli/spec.d.ts +48 -0
  33. package/dist/src/cli/spec.d.ts.map +1 -0
  34. package/dist/src/cli/spec.js +102 -0
  35. package/dist/src/context/stage-context.d.ts +72 -8
  36. package/dist/src/context/stage-context.d.ts.map +1 -1
  37. package/dist/src/context/stage-context.js +61 -15
  38. package/dist/src/contracts/inference-trace.d.ts +223 -0
  39. package/dist/src/contracts/inference-trace.d.ts.map +1 -0
  40. package/dist/src/contracts/inference-trace.js +12 -0
  41. package/dist/src/extractors/code-list.d.ts +3 -3
  42. package/dist/src/extractors/code-list.d.ts.map +1 -1
  43. package/dist/src/extractors/code-list.js +46 -11
  44. package/dist/src/generators/stage-skeleton-script.d.ts.map +1 -1
  45. package/dist/src/generators/stage-skeleton-script.js +9 -0
  46. package/dist/src/index.d.ts +7 -2
  47. package/dist/src/index.d.ts.map +1 -1
  48. package/dist/src/index.js +3 -1
  49. package/dist/src/inference/module-hints-inference.d.ts +30 -0
  50. package/dist/src/inference/module-hints-inference.d.ts.map +1 -0
  51. package/dist/src/inference/module-hints-inference.js +363 -0
  52. package/dist/src/inference/source-analysis.d.ts +26 -0
  53. package/dist/src/inference/source-analysis.d.ts.map +1 -0
  54. package/dist/src/inference/source-analysis.js +258 -0
  55. package/dist/src/testcase/testcase-generator.d.ts.map +1 -1
  56. package/dist/src/testcase/testcase-generator.js +4 -0
  57. package/dist/src/testcase/testcase-v2.d.ts +50 -0
  58. package/dist/src/testcase/testcase-v2.d.ts.map +1 -0
  59. package/dist/src/testcase/testcase-v2.js +1 -0
  60. package/dist/src/util/credentials.d.ts +12 -0
  61. package/dist/src/util/credentials.d.ts.map +1 -0
  62. package/dist/src/util/credentials.js +19 -0
  63. package/dist/src/util/i18n-testcase.d.ts +8 -0
  64. package/dist/src/util/i18n-testcase.d.ts.map +1 -0
  65. package/dist/src/util/i18n-testcase.js +55 -0
  66. package/dist/src/util/softlink.d.ts +33 -0
  67. package/dist/src/util/softlink.d.ts.map +1 -0
  68. package/dist/src/util/softlink.js +43 -0
  69. package/dist/src/validation/credentials.d.ts +19 -0
  70. package/dist/src/validation/credentials.d.ts.map +1 -0
  71. package/dist/src/validation/credentials.js +38 -0
  72. package/dist/src/validation/index.d.ts +6 -0
  73. package/dist/src/validation/index.d.ts.map +1 -0
  74. package/dist/src/validation/index.js +4 -0
  75. package/dist/src/validation/inference-trace.d.ts +13 -0
  76. package/dist/src/validation/inference-trace.d.ts.map +1 -0
  77. package/dist/src/validation/inference-trace.js +31 -0
  78. package/dist/src/validation/projects-index.d.ts +13 -0
  79. package/dist/src/validation/projects-index.d.ts.map +1 -0
  80. package/dist/src/validation/projects-index.js +37 -0
  81. package/dist/src/validation/testcase.d.ts +13 -0
  82. package/dist/src/validation/testcase.d.ts.map +1 -0
  83. package/dist/src/validation/testcase.js +53 -0
  84. package/dist/test/cli/extract-contract.test.d.ts +2 -0
  85. package/dist/test/cli/extract-contract.test.d.ts.map +1 -0
  86. package/dist/test/cli/extract-contract.test.js +32 -0
  87. package/dist/test/cli/generate-testcase-inference.test.d.ts +2 -0
  88. package/dist/test/cli/generate-testcase-inference.test.d.ts.map +1 -0
  89. package/dist/test/cli/generate-testcase-inference.test.js +63 -0
  90. package/dist/test/cli/generate-testcase-paths.test.d.ts +2 -0
  91. package/dist/test/cli/generate-testcase-paths.test.d.ts.map +1 -0
  92. package/dist/test/cli/generate-testcase-paths.test.js +165 -0
  93. package/dist/test/cli/generate-testcase-requirements.test.d.ts +2 -0
  94. package/dist/test/cli/generate-testcase-requirements.test.d.ts.map +1 -0
  95. package/dist/test/cli/generate-testcase-requirements.test.js +229 -0
  96. package/dist/test/cli/generate-testcase.test.d.ts +2 -0
  97. package/dist/test/cli/generate-testcase.test.d.ts.map +1 -0
  98. package/dist/test/cli/generate-testcase.test.js +130 -0
  99. package/dist/test/cli/index.test.d.ts +2 -0
  100. package/dist/test/cli/index.test.d.ts.map +1 -0
  101. package/dist/test/cli/index.test.js +113 -0
  102. package/dist/test/cli/probe.test.d.ts +2 -0
  103. package/dist/test/cli/probe.test.d.ts.map +1 -0
  104. package/dist/test/cli/probe.test.js +31 -0
  105. package/dist/test/cli/run.test.d.ts +2 -0
  106. package/dist/test/cli/run.test.d.ts.map +1 -0
  107. package/dist/test/cli/run.test.js +149 -0
  108. package/dist/test/cli/spec-boundary.test.d.ts +2 -0
  109. package/dist/test/cli/spec-boundary.test.d.ts.map +1 -0
  110. package/dist/test/cli/spec-boundary.test.js +198 -0
  111. package/dist/test/cli/spec.test.d.ts +2 -0
  112. package/dist/test/cli/spec.test.d.ts.map +1 -0
  113. package/dist/test/cli/spec.test.js +196 -0
  114. package/dist/test/code-list.test.d.ts +2 -0
  115. package/dist/test/code-list.test.d.ts.map +1 -0
  116. package/dist/test/code-list.test.js +39 -0
  117. package/dist/test/crud-contract.test.js +12 -32
  118. package/dist/test/inference/module-hints-inference.test.d.ts +2 -0
  119. package/dist/test/inference/module-hints-inference.test.d.ts.map +1 -0
  120. package/dist/test/inference/module-hints-inference.test.js +25 -0
  121. package/dist/test/inference/source-analysis.test.d.ts +2 -0
  122. package/dist/test/inference/source-analysis.test.d.ts.map +1 -0
  123. package/dist/test/inference/source-analysis.test.js +23 -0
  124. package/dist/test/material-inventory.test.js +13 -0
  125. package/dist/test/observable-pipeline.test.js +12 -3
  126. package/dist/test/stage-context.test.js +145 -13
  127. package/dist/test/util/credentials.test.d.ts +2 -0
  128. package/dist/test/util/credentials.test.d.ts.map +1 -0
  129. package/dist/test/util/credentials.test.js +64 -0
  130. package/dist/test/util/i18n-testcase.test.d.ts +2 -0
  131. package/dist/test/util/i18n-testcase.test.d.ts.map +1 -0
  132. package/dist/test/util/i18n-testcase.test.js +119 -0
  133. package/dist/test/util/softlink.test.d.ts +2 -0
  134. package/dist/test/util/softlink.test.d.ts.map +1 -0
  135. package/dist/test/util/softlink.test.js +82 -0
  136. package/dist/test/validation/credentials.test.d.ts +2 -0
  137. package/dist/test/validation/credentials.test.d.ts.map +1 -0
  138. package/dist/test/validation/credentials.test.js +72 -0
  139. package/dist/test/validation/projects-index.test.d.ts +2 -0
  140. package/dist/test/validation/projects-index.test.d.ts.map +1 -0
  141. package/dist/test/validation/projects-index.test.js +48 -0
  142. package/dist/test/validation/testcase.test.d.ts +2 -0
  143. package/dist/test/validation/testcase.test.d.ts.map +1 -0
  144. package/dist/test/validation/testcase.test.js +129 -0
  145. package/docs/README.md +6 -6
  146. package/docs/mvp-usage-guide.md +3 -3
  147. package/package.json +9 -4
  148. package/codex-skill/ep-stage/create-project/SKILL.md +0 -59
  149. package/codex-skill/ep-stage/glue-test/SKILL.md +0 -258
  150. package/codex-skill/ep-stage/glue-test/references/crud-pipeline.md +0 -139
  151. package/codex-skill/ep-stage/glue-testcase/SKILL.md +0 -31
  152. package/codex-skill/ep-stage/glue-testcase/references/testcase-schema.md +0 -67
  153. /package/codex-skill/ep-stage/{glue-testcase → glue-generate-testcase}/examples/observable-testcase.json +0 -0
  154. /package/codex-skill/ep-stage/{glue-test → glue-run-test}/references/gap-review-protocol.md +0 -0
  155. /package/codex-skill/ep-stage/{glue-test → glue-run-test}/references/harness-principles.md +0 -0
@@ -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":""}
@@ -0,0 +1,82 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { mkdtempSync, rmSync, writeFileSync, existsSync, readFileSync, lstatSync, mkdirSync, symlinkSync } from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { createSoftlinkWithFallback, softlinkFs } from '../../src/util/softlink.js';
6
+ /**
7
+ * REQ-FILE-02:跨平台软链 fallback util
8
+ * 三档 fallback:symlink(macOS/Linux)→ hardlink → copy + 时间戳警示(Windows 无管理员权限)。
9
+ *
10
+ * 注:fallback 路径通过 softlinkFs.* spy 模拟失败;ESM namespace 不可写,
11
+ * 因此 softlink.ts 显式暴露 softlinkFs 对象供测试替换。
12
+ */
13
+ describe('createSoftlinkWithFallback', () => {
14
+ let tmpDir;
15
+ let target;
16
+ let linkPath;
17
+ beforeEach(() => {
18
+ tmpDir = mkdtempSync(path.join(os.tmpdir(), 'softlink-test-'));
19
+ target = path.join(tmpDir, 'code_list.md');
20
+ linkPath = path.join(tmpDir, 'requirement', 'code_list.md');
21
+ writeFileSync(target, '# upstream code list\n');
22
+ mkdirSync(path.dirname(linkPath), { recursive: true });
23
+ });
24
+ afterEach(() => {
25
+ vi.restoreAllMocks();
26
+ rmSync(tmpDir, { recursive: true, force: true });
27
+ });
28
+ it('symlink 成功时返回 strategy=symlink,且链接指向 target', () => {
29
+ const result = createSoftlinkWithFallback(target, linkPath);
30
+ expect(result.strategy).toBe('symlink');
31
+ expect(result.warned).toBeUndefined();
32
+ expect(existsSync(linkPath)).toBe(true);
33
+ expect(lstatSync(linkPath).isSymbolicLink()).toBe(true);
34
+ // 通过链接读到的内容应该和源文件一致
35
+ expect(readFileSync(linkPath, 'utf8')).toBe('# upstream code list\n');
36
+ });
37
+ it('目标已存在时先 unlink 再创建(覆盖语义)', () => {
38
+ // 先放一个老文件占位
39
+ writeFileSync(linkPath, '# stale content\n');
40
+ const result = createSoftlinkWithFallback(target, linkPath);
41
+ expect(result.strategy).toBe('symlink');
42
+ expect(lstatSync(linkPath).isSymbolicLink()).toBe(true);
43
+ expect(readFileSync(linkPath, 'utf8')).toBe('# upstream code list\n');
44
+ });
45
+ it('目标已存在且为旧 symlink 时也能覆盖', () => {
46
+ // 先建一个指向别处的 symlink
47
+ const altTarget = path.join(tmpDir, 'alt.md');
48
+ writeFileSync(altTarget, '# alt\n');
49
+ symlinkSync(altTarget, linkPath, 'file');
50
+ expect(lstatSync(linkPath).isSymbolicLink()).toBe(true);
51
+ const result = createSoftlinkWithFallback(target, linkPath);
52
+ expect(result.strategy).toBe('symlink');
53
+ expect(readFileSync(linkPath, 'utf8')).toBe('# upstream code list\n');
54
+ });
55
+ it('symlink 失败时回退到 hardlink', () => {
56
+ // mock softlinkFs.symlinkSync 抛错,模拟 Windows 无权限
57
+ vi.spyOn(softlinkFs, 'symlinkSync').mockImplementation(() => {
58
+ throw new Error('EPERM: operation not permitted');
59
+ });
60
+ const result = createSoftlinkWithFallback(target, linkPath);
61
+ expect(result.strategy).toBe('hardlink');
62
+ expect(result.warned).toBeUndefined();
63
+ expect(existsSync(linkPath)).toBe(true);
64
+ expect(lstatSync(linkPath).isSymbolicLink()).toBe(false);
65
+ expect(readFileSync(linkPath, 'utf8')).toBe('# upstream code list\n');
66
+ });
67
+ it('symlink 与 hardlink 均失败时回退到 copy,并返回 warned=true', () => {
68
+ vi.spyOn(softlinkFs, 'symlinkSync').mockImplementation(() => {
69
+ throw new Error('EPERM: symlink not permitted');
70
+ });
71
+ vi.spyOn(softlinkFs, 'linkSync').mockImplementation(() => {
72
+ throw new Error('EXDEV: cross-device link not permitted');
73
+ });
74
+ const result = createSoftlinkWithFallback(target, linkPath);
75
+ expect(result.strategy).toBe('copy');
76
+ expect(result.warned).toBe(true);
77
+ expect(existsSync(linkPath)).toBe(true);
78
+ expect(lstatSync(linkPath).isSymbolicLink()).toBe(false);
79
+ // copy 的文件内容等于源文件
80
+ expect(readFileSync(linkPath, 'utf8')).toBe('# upstream code list\n');
81
+ });
82
+ });
@@ -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/validation/credentials.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,72 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { validateCredentials } from '../../src/validation/credentials.js';
3
+ describe('validateCredentials', () => {
4
+ it('合法数组(≥1 条)通过', () => {
5
+ const result = validateCredentials([
6
+ {
7
+ url: 'http://example.test',
8
+ username: 'admin',
9
+ password: 'pwd',
10
+ role: '系统管理员',
11
+ systemName: '基础平台',
12
+ },
13
+ ]);
14
+ expect(result).toHaveLength(1);
15
+ expect(result[0]).toMatchObject({
16
+ url: 'http://example.test',
17
+ username: 'admin',
18
+ password: 'pwd',
19
+ role: '系统管理员',
20
+ systemName: '基础平台',
21
+ });
22
+ });
23
+ it('允许无 role / systemName 的条目', () => {
24
+ const result = validateCredentials([
25
+ { url: 'http://example.test', username: 'admin', password: 'pwd' },
26
+ ]);
27
+ expect(result[0]).toEqual({
28
+ url: 'http://example.test',
29
+ username: 'admin',
30
+ password: 'pwd',
31
+ });
32
+ expect(result[0]).not.toHaveProperty('role');
33
+ expect(result[0]).not.toHaveProperty('systemName');
34
+ });
35
+ it('非数组报错', () => {
36
+ expect(() => validateCredentials({ url: 'x' })).toThrow(/数组/);
37
+ });
38
+ it('空数组报错', () => {
39
+ expect(() => validateCredentials([])).toThrow(/至少|1 条/);
40
+ });
41
+ it('条目缺 url 报错', () => {
42
+ expect(() => validateCredentials([{ username: 'admin', password: 'pwd' }])).toThrow(/url/);
43
+ });
44
+ it('条目缺 username 报错', () => {
45
+ expect(() => validateCredentials([{ url: 'http://example.test', password: 'pwd' }])).toThrow(/username/);
46
+ });
47
+ it('条目缺 password 报错', () => {
48
+ expect(() => validateCredentials([{ url: 'http://example.test', username: 'admin' }])).toThrow(/password/);
49
+ });
50
+ it('条目空字符串字段(空白)报错', () => {
51
+ expect(() => validateCredentials([{ url: ' ', username: 'admin', password: 'pwd' }])).toThrow(/url/);
52
+ });
53
+ it('同 (url, role) 重复报错', () => {
54
+ expect(() => validateCredentials([
55
+ { url: 'http://example.test', username: 'a', password: 'p1', role: '管理员' },
56
+ { url: 'http://example.test', username: 'b', password: 'p2', role: '管理员' },
57
+ ])).toThrow(/重复/);
58
+ });
59
+ it('同 url 但 role 不同允许', () => {
60
+ const result = validateCredentials([
61
+ { url: 'http://example.test', username: 'a', password: 'p1', role: '管理员' },
62
+ { url: 'http://example.test', username: 'b', password: 'p2', role: '业务员' },
63
+ ]);
64
+ expect(result).toHaveLength(2);
65
+ });
66
+ it('同 url 都没 role 视作 (url, "") 也会触发重复', () => {
67
+ expect(() => validateCredentials([
68
+ { url: 'http://example.test', username: 'a', password: 'p1' },
69
+ { url: 'http://example.test', username: 'b', password: 'p2' },
70
+ ])).toThrow(/重复/);
71
+ });
72
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=projects-index.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projects-index.test.d.ts","sourceRoot":"","sources":["../../../test/validation/projects-index.test.ts"],"names":[],"mappings":""}