qa-flowkit 0.4.0-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.
- package/.qa-ai/adapters/aider/.aider/README.md +25 -0
- package/.qa-ai/adapters/aider/.aider.conf.yml +6 -0
- package/.qa-ai/adapters/claude/agents/qa-workflow-orchestrator.md +18 -0
- package/.qa-ai/adapters/claude/commands/qa-add-tests.md +42 -0
- package/.qa-ai/adapters/claude/commands/qa-automation-plan.md +43 -0
- package/.qa-ai/adapters/claude/commands/qa-clean.md +42 -0
- package/.qa-ai/adapters/claude/commands/qa-config.md +51 -0
- package/.qa-ai/adapters/claude/commands/qa-coverage.md +46 -0
- package/.qa-ai/adapters/claude/commands/qa-doctor.md +11 -0
- package/.qa-ai/adapters/claude/commands/qa-full-flow.md +59 -0
- package/.qa-ai/adapters/claude/commands/qa-gate.md +36 -0
- package/.qa-ai/adapters/claude/commands/qa-help.md +30 -0
- package/.qa-ai/adapters/claude/commands/qa-init.md +70 -0
- package/.qa-ai/adapters/claude/commands/qa-status.md +56 -0
- package/.qa-ai/adapters/claude/commands/qa-update-tests.md +47 -0
- package/.qa-ai/adapters/claude/commands/qa-validate-features.md +36 -0
- package/.qa-ai/adapters/cline/.cline/README.md +25 -0
- package/.qa-ai/adapters/cline/.clinerules +9 -0
- package/.qa-ai/adapters/codex/README.md +44 -0
- package/.qa-ai/adapters/codex/prompts/implement-project.md +15 -0
- package/.qa-ai/adapters/continue/README.md +26 -0
- package/.qa-ai/adapters/continue/checks/qa-feature-conventions.md +15 -0
- package/.qa-ai/adapters/gemini/GEMINI.md +40 -0
- package/.qa-ai/adapters/generic/AGENTS.md +100 -0
- package/.qa-ai/adapters/goose/recipes/qa-flowkit.yaml +20 -0
- package/.qa-ai/adapters/opencode/README.md +57 -0
- package/.qa-ai/adapters/opencode/agents/qa-workflow.md +18 -0
- package/.qa-ai/adapters/opencode/commands/qa-add-tests.md +42 -0
- package/.qa-ai/adapters/opencode/commands/qa-automation-plan.md +43 -0
- package/.qa-ai/adapters/opencode/commands/qa-clean.md +42 -0
- package/.qa-ai/adapters/opencode/commands/qa-config.md +51 -0
- package/.qa-ai/adapters/opencode/commands/qa-coverage.md +46 -0
- package/.qa-ai/adapters/opencode/commands/qa-doctor.md +13 -0
- package/.qa-ai/adapters/opencode/commands/qa-full-flow.md +59 -0
- package/.qa-ai/adapters/opencode/commands/qa-gate.md +36 -0
- package/.qa-ai/adapters/opencode/commands/qa-help.md +30 -0
- package/.qa-ai/adapters/opencode/commands/qa-init.md +70 -0
- package/.qa-ai/adapters/opencode/commands/qa-status.md +56 -0
- package/.qa-ai/adapters/opencode/commands/qa-update-tests.md +47 -0
- package/.qa-ai/adapters/opencode/commands/qa-validate-features.md +36 -0
- package/.qa-ai/agents/README.md +39 -0
- package/.qa-ai/agents/api-testing-agent.md +73 -0
- package/.qa-ai/agents/automation-feasibility-agent.md +128 -0
- package/.qa-ai/agents/gherkin-test-design-agent.md +110 -0
- package/.qa-ai/agents/jira-task-agent.md +92 -0
- package/.qa-ai/agents/pr-agent.md +101 -0
- package/.qa-ai/agents/qa-context-intake-agent.md +75 -0
- package/.qa-ai/agents/qa-workflow-orchestrator.md +113 -0
- package/.qa-ai/agents/release-gate-agent.md +50 -0
- package/.qa-ai/agents/requirements-intake-agent.md +79 -0
- package/.qa-ai/agents/requirements-normalization-agent.md +80 -0
- package/.qa-ai/agents/specialists/available/appium.md +59 -0
- package/.qa-ai/agents/specialists/available/cypress.md +68 -0
- package/.qa-ai/agents/specialists/available/generic-test-design.md +117 -0
- package/.qa-ai/agents/specialists/available/jira.md +108 -0
- package/.qa-ai/agents/specialists/available/karate.md +97 -0
- package/.qa-ai/agents/specialists/available/playwright-api.md +87 -0
- package/.qa-ai/agents/specialists/available/playwright-ui.md +87 -0
- package/.qa-ai/agents/specialists/available/postman.md +108 -0
- package/.qa-ai/agents/specialists/available/rest-assured.md +103 -0
- package/.qa-ai/agents/specialists/available/selenium.md +91 -0
- package/.qa-ai/agents/specialists/available/testrail.md +85 -0
- package/.qa-ai/agents/specialists/available/webdriverio.md +81 -0
- package/.qa-ai/agents/test-design-system-agent.md +33 -0
- package/.qa-ai/agents/testrail-coverage-agent.md +84 -0
- package/.qa-ai/agents/testrail-sync-agent.md +96 -0
- package/.qa-ai/agents/webdriverio-implementation-agent.md +84 -0
- package/.qa-ai/presets/manual-only.yaml +65 -0
- package/.qa-ai/presets/selenium-jest-browserstack.yaml +72 -0
- package/.qa-ai/presets/webdriverio-playwright-api.yaml +85 -0
- package/.qa-ai/rules/api-testing.rules.md +7 -0
- package/.qa-ai/rules/approval.rules.md +8 -0
- package/.qa-ai/rules/automation.rules.md +7 -0
- package/.qa-ai/rules/gherkin.rules.md +12 -0
- package/.qa-ai/rules/testrail.rules.md +10 -0
- package/.qa-ai/rules/webdriverio.rules.md +9 -0
- package/.qa-ai/scripts/bootstrap-agent-adapters.mjs +127 -0
- package/.qa-ai/scripts/clean.mjs +243 -0
- package/.qa-ai/scripts/config.mjs +202 -0
- package/.qa-ai/scripts/doctor.mjs +383 -0
- package/.qa-ai/scripts/init.mjs +447 -0
- package/.qa-ai/scripts/lib/markdown-table.mjs +76 -0
- package/.qa-ai/scripts/lib/project-config.mjs +184 -0
- package/.qa-ai/scripts/lib/qa-next-steps.mjs +578 -0
- package/.qa-ai/scripts/lib/release-gate.mjs +66 -0
- package/.qa-ai/scripts/lib/test-design.mjs +92 -0
- package/.qa-ai/scripts/lib/test-management-mapping.mjs +73 -0
- package/.qa-ai/scripts/lib/utils.mjs +331 -0
- package/.qa-ai/scripts/qa-help.mjs +44 -0
- package/.qa-ai/scripts/smoke-npm-pack.mjs +187 -0
- package/.qa-ai/scripts/smoke-test.mjs +465 -0
- package/.qa-ai/scripts/sync-agent-adapters.mjs +121 -0
- package/.qa-ai/scripts/test-validators.mjs +334 -0
- package/.qa-ai/scripts/validate-active-specialists.mjs +106 -0
- package/.qa-ai/scripts/validate-features.mjs +277 -0
- package/.qa-ai/scripts/validate-release-gate.mjs +105 -0
- package/.qa-ai/scripts/validate-sync-plan.mjs +186 -0
- package/.qa-ai/scripts/validate-target.mjs +104 -0
- package/.qa-ai/scripts/validate-test-design.mjs +117 -0
- package/.qa-ai/scripts/validate-traceability.mjs +183 -0
- package/.qa-ai/templates/automation-feasibility-report.template.md +21 -0
- package/.qa-ai/templates/automation-implementation-plan.template.md +23 -0
- package/.qa-ai/templates/feature.template +13 -0
- package/.qa-ai/templates/jira-automation-task.template.md +25 -0
- package/.qa-ai/templates/pr-template.md +60 -0
- package/.qa-ai/templates/release-gate.template.yaml +16 -0
- package/.qa-ai/templates/requirement-analysis.template.md +17 -0
- package/.qa-ai/templates/test-design-proposal.template.md +26 -0
- package/.qa-ai/templates/test-design-system.template.md +15 -0
- package/.qa-ai/templates/test-management-mapping.template.json +18 -0
- package/.qa-ai/templates/testrail-coverage-analysis.template.md +17 -0
- package/.qa-ai/templates/testrail-sync-plan.template.md +22 -0
- package/.qa-ai/templates/traceability-matrix.template.md +4 -0
- package/.qa-ai/workflows/automation-analysis.md +23 -0
- package/.qa-ai/workflows/cleanup.md +52 -0
- package/.qa-ai/workflows/context-intake.md +66 -0
- package/.qa-ai/workflows/full-flow.md +55 -0
- package/.qa-ai/workflows/implementation.md +24 -0
- package/.qa-ai/workflows/intake.md +3 -0
- package/.qa-ai/workflows/pr.md +3 -0
- package/.qa-ai/workflows/release-gate.md +22 -0
- package/.qa-ai/workflows/test-design-system.md +33 -0
- package/.qa-ai/workflows/test-design.md +23 -0
- package/.qa-ai/workflows/testrail-sync.md +23 -0
- package/CHANGELOG.md +108 -0
- package/CODE_OF_CONDUCT.md +11 -0
- package/CONTRIBUTING.md +39 -0
- package/LICENSE +21 -0
- package/README.es.md +602 -0
- package/README.md +633 -0
- package/ROADMAP.md +107 -0
- package/SECURITY.md +18 -0
- package/bin/qa-flowkit.mjs +214 -0
- package/docs/qa-ai/agent-compatibility.md +100 -0
- package/docs/qa-ai/architecture.md +130 -0
- package/docs/qa-ai/backlog.md +393 -0
- package/docs/qa-ai/cleanup.md +104 -0
- package/docs/qa-ai/customizing-agents.md +148 -0
- package/docs/qa-ai/getting-started.md +385 -0
- package/docs/qa-ai/implementation-guide-for-codex.md +210 -0
- package/docs/qa-ai/npm-migration-plan.md +50 -0
- package/docs/qa-ai/open-source-release-checklist.md +17 -0
- package/docs/qa-ai/qa-help.md +76 -0
- package/docs/qa-ai/release-gate.md +60 -0
- package/docs/qa-ai/terminal-transcripts.md +316 -0
- package/docs/qa-ai/test-design-dual-mode.md +75 -0
- package/docs/qa-ai/troubleshooting.md +740 -0
- package/docs/qa-ai/workflow.md +147 -0
- package/package.json +72 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
commaList,
|
|
5
|
+
copyDirSafe,
|
|
6
|
+
manifestEntry,
|
|
7
|
+
manifestPath,
|
|
8
|
+
parseArgs,
|
|
9
|
+
pathExists,
|
|
10
|
+
recordManifestEntries,
|
|
11
|
+
relativeTo,
|
|
12
|
+
logHeader
|
|
13
|
+
} from './lib/utils.mjs';
|
|
14
|
+
|
|
15
|
+
const cwd = process.cwd();
|
|
16
|
+
const args = parseArgs(process.argv);
|
|
17
|
+
const force = Boolean(args.force);
|
|
18
|
+
const adaptersRoot = path.join(cwd, '.qa-ai', 'adapters');
|
|
19
|
+
|
|
20
|
+
const adapterMap = {
|
|
21
|
+
generic: { source: 'generic', target: '.' },
|
|
22
|
+
claude: { source: 'claude', target: '.claude' },
|
|
23
|
+
codex: { source: 'codex', target: '.codex' },
|
|
24
|
+
opencode: { source: 'opencode', target: '.opencode' },
|
|
25
|
+
cline: { source: 'cline', target: '.' },
|
|
26
|
+
continue: { source: 'continue', target: '.continue' },
|
|
27
|
+
aider: { source: 'aider', target: '.' },
|
|
28
|
+
goose: { source: 'goose', target: '.goose' },
|
|
29
|
+
gemini: { source: 'gemini', target: '.' }
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function printHelp() {
|
|
33
|
+
console.log(`Usage: node .qa-ai/scripts/sync-agent-adapters.mjs [options]
|
|
34
|
+
|
|
35
|
+
Options:
|
|
36
|
+
--adapters <list> Comma-separated adapters to sync, or "all" (default: all)
|
|
37
|
+
--adapter <name> Repeatable single adapter name
|
|
38
|
+
--force Overwrite existing adapter files
|
|
39
|
+
--help Show this help
|
|
40
|
+
|
|
41
|
+
Supported adapters: ${Object.keys(adapterMap).join(', ')}
|
|
42
|
+
`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function selectedAdapterNames() {
|
|
46
|
+
const requested = [...commaList(args.adapters), ...commaList(args.adapter)].map((name) => name.toLowerCase());
|
|
47
|
+
if (requested.length === 0 || requested.includes('all')) return Object.keys(adapterMap);
|
|
48
|
+
if (requested.includes('none')) return [];
|
|
49
|
+
return [...new Set(requested)];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function main() {
|
|
53
|
+
if (args.help) {
|
|
54
|
+
printHelp();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
logHeader('Sync agent adapters');
|
|
59
|
+
const names = selectedAdapterNames();
|
|
60
|
+
const unknown = names.filter((name) => !(name in adapterMap));
|
|
61
|
+
if (unknown.length > 0) {
|
|
62
|
+
console.error(`Unknown adapter(s): ${unknown.join(', ')}`);
|
|
63
|
+
console.error(`Supported adapters: ${Object.keys(adapterMap).join(', ')}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (names.length === 0) {
|
|
68
|
+
console.log('No adapters selected.');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let missing = 0;
|
|
73
|
+
const manifestEntries = [];
|
|
74
|
+
for (const name of names) {
|
|
75
|
+
const adapter = adapterMap[name];
|
|
76
|
+
const source = path.join(adaptersRoot, adapter.source);
|
|
77
|
+
const target = path.join(cwd, adapter.target);
|
|
78
|
+
|
|
79
|
+
if (!await pathExists(source)) {
|
|
80
|
+
missing += 1;
|
|
81
|
+
console.log(`[FAIL] Adapter ${name}: missing template ${relativeTo(cwd, source)}`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const results = await copyDirSafe(source, target, { force });
|
|
86
|
+
const dirResults = results.filter((result) => result.type === 'dir' && result.created);
|
|
87
|
+
const fileResults = results.filter((result) => result.type === 'file');
|
|
88
|
+
console.log(`Adapter ${name}:`);
|
|
89
|
+
for (const result of dirResults) {
|
|
90
|
+
console.log(` created ${relativeTo(cwd, result.path)}`);
|
|
91
|
+
manifestEntries.push(await manifestEntry(cwd, result.path, {
|
|
92
|
+
type: 'dir',
|
|
93
|
+
category: 'adapter',
|
|
94
|
+
source: `adapter:${name}`
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
for (const result of fileResults) {
|
|
98
|
+
console.log(` ${result.copied ? 'copied ' : 'skipped'} ${relativeTo(cwd, result.path)}`);
|
|
99
|
+
if (result.copied) {
|
|
100
|
+
manifestEntries.push(await manifestEntry(cwd, result.path, {
|
|
101
|
+
type: 'file',
|
|
102
|
+
category: 'adapter',
|
|
103
|
+
source: `adapter:${name}`
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (missing > 0) {
|
|
110
|
+
console.log(`\nFAILED - ${missing} adapter template(s) missing.`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const manifest = await recordManifestEntries(cwd, manifestEntries);
|
|
115
|
+
if (manifest) console.log(`\nupdated ${relativeTo(cwd, manifestPath(cwd))}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
main().catch((error) => {
|
|
119
|
+
console.error(error);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
});
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { inspectQaWorkflow, normalizeQaTrack } from './lib/qa-next-steps.mjs';
|
|
7
|
+
import { validateReleaseGateData } from './lib/release-gate.mjs';
|
|
8
|
+
import { validateTestDesignProposal, validateTestDesignSystem } from './lib/test-design.mjs';
|
|
9
|
+
import { parseMarkdownTable } from './lib/markdown-table.mjs';
|
|
10
|
+
import { validateTestManagementMapping } from './lib/test-management-mapping.mjs';
|
|
11
|
+
|
|
12
|
+
function assertIncludes(haystack, needle) {
|
|
13
|
+
assert.ok(
|
|
14
|
+
haystack.some((item) => item.includes(needle)),
|
|
15
|
+
`Expected an error containing: ${needle}\nActual errors:\n${haystack.join('\n')}`
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function testValidTable() {
|
|
20
|
+
const result = parseMarkdownTable([
|
|
21
|
+
'| ID | Proposed action | Approval status |',
|
|
22
|
+
'|---|---|---|',
|
|
23
|
+
'| TC-001 | Propose create | Pending approval |',
|
|
24
|
+
''
|
|
25
|
+
].join('\n'), {
|
|
26
|
+
label: 'Sync plan table',
|
|
27
|
+
requiredColumns: ['ID', 'Proposed action', 'Approval status']
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
assert.deepEqual(result.errors, []);
|
|
31
|
+
assert.deepEqual(result.header, ['ID', 'Proposed action', 'Approval status']);
|
|
32
|
+
assert.equal(result.rows.length, 1);
|
|
33
|
+
assert.equal(result.rows[0].values.id, 'TC-001');
|
|
34
|
+
assert.equal(result.rows[0].values['proposed action'], 'Propose create');
|
|
35
|
+
assert.equal(result.rows[0].values['approval status'], 'Pending approval');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function testMissingSeparator() {
|
|
39
|
+
const result = parseMarkdownTable([
|
|
40
|
+
'| ID | Proposed action |',
|
|
41
|
+
'| TC-001 | Propose create |',
|
|
42
|
+
''
|
|
43
|
+
].join('\n'), {
|
|
44
|
+
label: 'Sync plan table',
|
|
45
|
+
requiredColumns: ['ID']
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
assertIncludes(result.errors, 'must have a Markdown separator row');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function testMissingRequiredColumn() {
|
|
52
|
+
const result = parseMarkdownTable([
|
|
53
|
+
'| ID | Proposed action |',
|
|
54
|
+
'|---|---|',
|
|
55
|
+
'| TC-001 | Propose create |',
|
|
56
|
+
''
|
|
57
|
+
].join('\n'), {
|
|
58
|
+
label: 'Sync plan table',
|
|
59
|
+
requiredColumns: ['ID', 'Approval status']
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
assertIncludes(result.errors, 'missing required column "Approval status"');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function testWrongCellCount() {
|
|
66
|
+
const result = parseMarkdownTable([
|
|
67
|
+
'| ID | Proposed action |',
|
|
68
|
+
'|---|---|',
|
|
69
|
+
'| TC-001 | Propose create | Extra |',
|
|
70
|
+
''
|
|
71
|
+
].join('\n'), {
|
|
72
|
+
label: 'Sync plan table'
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
assertIncludes(result.errors, 'row has 3 cell(s), expected 2');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function testEmptyRow() {
|
|
79
|
+
const result = parseMarkdownTable([
|
|
80
|
+
'| ID | Proposed action |',
|
|
81
|
+
'|---|---|',
|
|
82
|
+
'| | |',
|
|
83
|
+
''
|
|
84
|
+
].join('\n'), {
|
|
85
|
+
label: 'Sync plan table'
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
assertIncludes(result.errors, 'row is empty');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function testEmptyMappingIsValid() {
|
|
92
|
+
assert.deepEqual(validateTestManagementMapping({}, { source: 'mapping.json' }), []);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function testValidMappingEntry() {
|
|
96
|
+
const errors = validateTestManagementMapping({
|
|
97
|
+
'TC-001': {
|
|
98
|
+
externalId: 'C123',
|
|
99
|
+
section: 'Login',
|
|
100
|
+
suite: 'Regression',
|
|
101
|
+
status: 'planned',
|
|
102
|
+
lastReviewedAt: '2026-05-25',
|
|
103
|
+
notes: 'Created from QA FlowKit proposal.'
|
|
104
|
+
}
|
|
105
|
+
}, { source: 'mapping.json' });
|
|
106
|
+
|
|
107
|
+
assert.deepEqual(errors, []);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function testMappingEntryMustBeObject() {
|
|
111
|
+
const errors = validateTestManagementMapping({
|
|
112
|
+
'TC-001': 'C123'
|
|
113
|
+
}, { source: 'mapping.json' });
|
|
114
|
+
|
|
115
|
+
assertIncludes(errors, 'entry "TC-001" must be an object');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function testMappingRejectsUnsupportedField() {
|
|
119
|
+
const errors = validateTestManagementMapping({
|
|
120
|
+
'TC-001': {
|
|
121
|
+
externalId: 'C123',
|
|
122
|
+
owner: 'qa'
|
|
123
|
+
}
|
|
124
|
+
}, { source: 'mapping.json' });
|
|
125
|
+
|
|
126
|
+
assertIncludes(errors, 'unsupported field "owner"');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function testMappingRejectsDuplicateExternalId() {
|
|
130
|
+
const errors = validateTestManagementMapping({
|
|
131
|
+
'TC-001': { externalId: 'C123' },
|
|
132
|
+
'TC-002': { externalId: 'C123' }
|
|
133
|
+
}, { source: 'mapping.json' });
|
|
134
|
+
|
|
135
|
+
assertIncludes(errors, 'externalId "C123" is used by both');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function testMappingRejectsSecretLikeFields() {
|
|
139
|
+
const errors = validateTestManagementMapping({
|
|
140
|
+
'TC-001': {
|
|
141
|
+
externalId: 'C123',
|
|
142
|
+
apiToken: 'github_pat_1234567890abcdefghijklmnop'
|
|
143
|
+
}
|
|
144
|
+
}, { source: 'mapping.json' });
|
|
145
|
+
|
|
146
|
+
assertIncludes(errors, 'unsupported field "apiToken"');
|
|
147
|
+
assertIncludes(errors, 'appears to contain a secret');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function testMappingTemplateIsValid() {
|
|
151
|
+
const templatePath = path.resolve('.qa-ai/templates/test-management-mapping.template.json');
|
|
152
|
+
const parsed = JSON.parse(await fs.readFile(templatePath, 'utf8'));
|
|
153
|
+
assert.deepEqual(validateTestManagementMapping(parsed, { source: 'test-management-mapping.template.json' }), []);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function testNormalizeQaTrack() {
|
|
157
|
+
assert.equal(normalizeQaTrack('fast'), 'quick');
|
|
158
|
+
assert.equal(normalizeQaTrack('enterprise'), 'enterprise');
|
|
159
|
+
assert.equal(normalizeQaTrack('unknown-value'), 'standard');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function testQaHelpWithoutConfig() {
|
|
163
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'qa-ai-help-'));
|
|
164
|
+
const report = await inspectQaWorkflow(tempDir);
|
|
165
|
+
assert.equal(report.initialized, false);
|
|
166
|
+
assert.ok(report.recommendations.some((item) => item.command.includes('init.mjs')));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function testReleaseGatePass() {
|
|
170
|
+
const result = validateReleaseGateData({
|
|
171
|
+
decision: 'PASS',
|
|
172
|
+
approver: 'QA Lead',
|
|
173
|
+
coverage_summary: 'All validators passed.',
|
|
174
|
+
open_risks: ['None documented'],
|
|
175
|
+
evidence_paths: ['qa-ai-output/traceability-matrix.md', 'qa-ai-output/pr-summary.md']
|
|
176
|
+
});
|
|
177
|
+
assert.deepEqual(result.errors, []);
|
|
178
|
+
assert.equal(result.decision, 'PASS');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function testReleaseGatePendingCanBeAllowed() {
|
|
182
|
+
const draft = {
|
|
183
|
+
decision: 'PENDING',
|
|
184
|
+
coverage_summary: 'Draft review in progress.',
|
|
185
|
+
open_risks: ['Pending QA lead review'],
|
|
186
|
+
evidence_paths: ['qa-ai-output/pr-summary.md']
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
assert.notEqual(validateReleaseGateData(draft).errors.length, 0);
|
|
190
|
+
assert.deepEqual(validateReleaseGateData(draft, { allowPending: true }).errors, []);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function testTestDesignSystemSections() {
|
|
194
|
+
const valid = validateTestDesignSystem(`# System Test Design\n${[
|
|
195
|
+
'## Scope',
|
|
196
|
+
'## Architecture alignment',
|
|
197
|
+
'## Testability risks',
|
|
198
|
+
'## Cross-RF coverage strategy',
|
|
199
|
+
'## Shared fixtures and data',
|
|
200
|
+
'## Non-functional focus',
|
|
201
|
+
'## Open questions'
|
|
202
|
+
].join('\n\n')}\n`);
|
|
203
|
+
assert.equal(valid.ok, true);
|
|
204
|
+
const invalid = validateTestDesignSystem('# System Test Design\n## Scope\n');
|
|
205
|
+
assert.equal(invalid.ok, false);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function testSpanishTestDesignSections() {
|
|
209
|
+
const system = validateTestDesignSystem(`# Diseno de pruebas de sistema\n${[
|
|
210
|
+
'## Alcance',
|
|
211
|
+
'## Alineacion con arquitectura',
|
|
212
|
+
'## Riesgos de testabilidad',
|
|
213
|
+
'## Estrategia de cobertura entre RFs',
|
|
214
|
+
'## Fixtures y datos compartidos',
|
|
215
|
+
'## Enfoque no funcional',
|
|
216
|
+
'## Preguntas abiertas'
|
|
217
|
+
].join('\n\n')}\n`);
|
|
218
|
+
assert.equal(system.ok, true);
|
|
219
|
+
|
|
220
|
+
const proposal = validateTestDesignProposal(`# Propuesta de diseno de pruebas\n${[
|
|
221
|
+
'## RF oficial',
|
|
222
|
+
'RF-101',
|
|
223
|
+
'## Alcance',
|
|
224
|
+
'## Pruebas propuestas',
|
|
225
|
+
'## Pruebas existentes para reutilizar',
|
|
226
|
+
'## Pruebas existentes que requieren modificacion',
|
|
227
|
+
'## Nuevas pruebas a crear',
|
|
228
|
+
'## Ambiguedades que requieren decision del usuario',
|
|
229
|
+
'## Solicitud de aprobacion'
|
|
230
|
+
].join('\n\n')}\n`);
|
|
231
|
+
assert.equal(proposal.ok, true);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function testTestDesignProposalSections() {
|
|
235
|
+
const valid = validateTestDesignProposal(`# Test Design Proposal\n${[
|
|
236
|
+
'## Official RF ID',
|
|
237
|
+
'RF-101',
|
|
238
|
+
'## Scope',
|
|
239
|
+
'## Proposed tests',
|
|
240
|
+
'## Existing tests to reuse',
|
|
241
|
+
'## Existing tests requiring modification',
|
|
242
|
+
'## New tests to create',
|
|
243
|
+
'## Ambiguities requiring user decision',
|
|
244
|
+
'## Approval request'
|
|
245
|
+
].join('\n\n')}\n`);
|
|
246
|
+
assert.equal(valid.ok, true);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function testReleaseGateWaivedRequiresApprover() {
|
|
250
|
+
const result = validateReleaseGateData({
|
|
251
|
+
decision: 'WAIVED',
|
|
252
|
+
coverage_summary: 'Partial coverage accepted.',
|
|
253
|
+
open_risks: ['Known gap in API tests'],
|
|
254
|
+
evidence_paths: ['qa-ai-output/pr-summary.md']
|
|
255
|
+
});
|
|
256
|
+
assert.ok(result.errors.some((error) => error.includes('approver')));
|
|
257
|
+
assert.ok(result.errors.some((error) => error.includes('waived_reason')));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function testQaHelpQuickTrackPendingGherkin() {
|
|
261
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'qa-ai-help-'));
|
|
262
|
+
await fs.mkdir(path.join(tempDir, '.qa-ai'), { recursive: true });
|
|
263
|
+
await fs.mkdir(path.join(tempDir, 'qa-ai-output'), { recursive: true });
|
|
264
|
+
await fs.writeFile(
|
|
265
|
+
path.join(tempDir, 'qa-ai.config.yaml'),
|
|
266
|
+
[
|
|
267
|
+
'project:',
|
|
268
|
+
' qaTrack: quick',
|
|
269
|
+
'knowledge:',
|
|
270
|
+
' enabled: false',
|
|
271
|
+
'tools:',
|
|
272
|
+
' testManagement: none',
|
|
273
|
+
' issueTracker: none',
|
|
274
|
+
'automation:',
|
|
275
|
+
' ui:',
|
|
276
|
+
' framework: none',
|
|
277
|
+
' api:',
|
|
278
|
+
' framework: none',
|
|
279
|
+
'gherkin:',
|
|
280
|
+
' featurePath: features',
|
|
281
|
+
'traceability:',
|
|
282
|
+
' matrixPath: qa-ai-output/traceability-matrix.md',
|
|
283
|
+
''
|
|
284
|
+
].join('\n'),
|
|
285
|
+
'utf8'
|
|
286
|
+
);
|
|
287
|
+
await fs.writeFile(
|
|
288
|
+
path.join(tempDir, 'qa-ai-output', 'requirement-analysis.md'),
|
|
289
|
+
'# Requirement Analysis\n',
|
|
290
|
+
'utf8'
|
|
291
|
+
);
|
|
292
|
+
await fs.writeFile(
|
|
293
|
+
path.join(tempDir, 'qa-ai-output', 'normalized-requirements.md'),
|
|
294
|
+
'# Normalized Requirements\n',
|
|
295
|
+
'utf8'
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
const report = await inspectQaWorkflow(tempDir);
|
|
299
|
+
assert.equal(report.track, 'quick');
|
|
300
|
+
assert.ok(!report.pendingPhaseIds.includes('tm-coverage'));
|
|
301
|
+
assert.ok(!report.pendingPhaseIds.includes('feasibility'));
|
|
302
|
+
assert.equal(report.pendingPhaseIds[0], 'gherkin');
|
|
303
|
+
assert.ok(report.recommendations.some((item) => item.title.includes('Gherkin')));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function main() {
|
|
307
|
+
testNormalizeQaTrack();
|
|
308
|
+
testTestDesignSystemSections();
|
|
309
|
+
testTestDesignProposalSections();
|
|
310
|
+
testSpanishTestDesignSections();
|
|
311
|
+
testReleaseGatePass();
|
|
312
|
+
testReleaseGatePendingCanBeAllowed();
|
|
313
|
+
testReleaseGateWaivedRequiresApprover();
|
|
314
|
+
await testQaHelpWithoutConfig();
|
|
315
|
+
await testQaHelpQuickTrackPendingGherkin();
|
|
316
|
+
testValidTable();
|
|
317
|
+
testMissingSeparator();
|
|
318
|
+
testMissingRequiredColumn();
|
|
319
|
+
testWrongCellCount();
|
|
320
|
+
testEmptyRow();
|
|
321
|
+
testEmptyMappingIsValid();
|
|
322
|
+
testValidMappingEntry();
|
|
323
|
+
testMappingEntryMustBeObject();
|
|
324
|
+
testMappingRejectsUnsupportedField();
|
|
325
|
+
testMappingRejectsDuplicateExternalId();
|
|
326
|
+
testMappingRejectsSecretLikeFields();
|
|
327
|
+
await testMappingTemplateIsValid();
|
|
328
|
+
console.log('Validator unit tests passed.');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
main().catch((error) => {
|
|
332
|
+
console.error(error);
|
|
333
|
+
process.exit(1);
|
|
334
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
activeSpecialists,
|
|
5
|
+
specialistCatalog
|
|
6
|
+
} from './lib/project-config.mjs';
|
|
7
|
+
import {
|
|
8
|
+
loadQaAiConfig,
|
|
9
|
+
logHeader,
|
|
10
|
+
parseArgs,
|
|
11
|
+
pathExists,
|
|
12
|
+
readText,
|
|
13
|
+
relativeTo,
|
|
14
|
+
resolveRepoPath
|
|
15
|
+
} from './lib/utils.mjs';
|
|
16
|
+
|
|
17
|
+
const cwd = process.cwd();
|
|
18
|
+
const args = parseArgs(process.argv);
|
|
19
|
+
|
|
20
|
+
function printHelp() {
|
|
21
|
+
console.log(`Usage: node .qa-ai/scripts/validate-active-specialists.mjs [options]
|
|
22
|
+
|
|
23
|
+
Options:
|
|
24
|
+
--allow-missing Return success when qa-ai.config.yaml or active.md is missing
|
|
25
|
+
--help Show this help
|
|
26
|
+
`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function listedSpecialistIds(content) {
|
|
30
|
+
return content
|
|
31
|
+
.split(/\r?\n/)
|
|
32
|
+
.map((line) => line.match(/^\s*-\s+`([^`]+)`:/)?.[1])
|
|
33
|
+
.filter(Boolean)
|
|
34
|
+
.sort();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function uniqueSorted(values) {
|
|
38
|
+
return [...new Set(values)].sort();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function main() {
|
|
42
|
+
if (args.help) {
|
|
43
|
+
printHelp();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
logHeader('QA AI active specialists validator');
|
|
48
|
+
const configInfo = await loadQaAiConfig(cwd);
|
|
49
|
+
const activePath = resolveRepoPath(cwd, '.qa-ai/agents/specialists/active.md', {
|
|
50
|
+
label: 'active specialists index'
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (!configInfo.exists) {
|
|
54
|
+
console.log('No qa-ai.config.yaml found.');
|
|
55
|
+
if (args['allow-missing']) return;
|
|
56
|
+
console.log('\nFAILED - active specialist validation requires qa-ai.config.yaml.');
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!await pathExists(activePath)) {
|
|
61
|
+
console.log('No active specialists index found at .qa-ai/agents/specialists/active.md.');
|
|
62
|
+
if (args['allow-missing']) return;
|
|
63
|
+
console.log('\nFAILED - run init or config import to generate active specialists.');
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const expected = activeSpecialists(configInfo.data).map(([id]) => id).sort();
|
|
68
|
+
const actual = listedSpecialistIds(await readText(activePath));
|
|
69
|
+
const errors = [];
|
|
70
|
+
|
|
71
|
+
for (const id of expected.filter((id) => !actual.includes(id))) {
|
|
72
|
+
errors.push(`Missing active specialist: ${id}`);
|
|
73
|
+
}
|
|
74
|
+
for (const id of actual.filter((id) => !expected.includes(id))) {
|
|
75
|
+
errors.push(`Stale active specialist: ${id}`);
|
|
76
|
+
}
|
|
77
|
+
for (const id of actual) {
|
|
78
|
+
if (!(id in specialistCatalog)) {
|
|
79
|
+
errors.push(`Unknown active specialist: ${id}`);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const sourcePath = resolveRepoPath(cwd, `.qa-ai/agents/specialists/available/${id}.md`, {
|
|
83
|
+
label: `specialist source "${id}"`
|
|
84
|
+
});
|
|
85
|
+
if (!await pathExists(sourcePath)) {
|
|
86
|
+
errors.push(`Missing specialist source for ${id}: ${relativeTo(cwd, sourcePath)}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (uniqueSorted(actual).length !== actual.length) {
|
|
91
|
+
errors.push('Duplicate specialist entries found in active.md.');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (errors.length > 0) {
|
|
95
|
+
for (const error of errors) console.log(`[FAIL] ${error}`);
|
|
96
|
+
console.log(`\nFAILED - ${errors.length} active specialist validation error(s).`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log(`[PASS] ${relativeTo(cwd, activePath)} matches qa-ai.config.yaml.`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
main().catch((error) => {
|
|
104
|
+
console.error(error);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
});
|